diff --git a/Custom/Kernel/System/CustomerUser.pm b/Custom/Kernel/System/CustomerUser.pm new file mode 100644 index 0000000..74323b2 --- /dev/null +++ b/Custom/Kernel/System/CustomerUser.pm @@ -0,0 +1,1588 @@ +# -- +# OTOBO is a web-based ticketing system for service organisations. +# -- +# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ +# Copyright (C) 2019-2023 Rother OSS GmbH, https://otobo.de/ +# -- +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# -- + +package Kernel::System::CustomerUser; + +use strict; +use warnings; + +use parent qw(Kernel::System::EventHandler); + +use Kernel::System::VariableCheck qw(:all); + +our @ObjectDependencies = ( + 'Kernel::Config', + 'Kernel::Language', + 'Kernel::System::Cache', + 'Kernel::System::CustomerCompany', + 'Kernel::System::DB', + 'Kernel::System::DynamicField', + 'Kernel::System::DynamicField::Backend', + 'Kernel::System::Encode', + 'Kernel::System::Log', + 'Kernel::System::Main', + 'Kernel::System::Valid', +); + +=head1 NAME + +Kernel::System::CustomerUser - customer user lib + +=head1 DESCRIPTION + +All customer user functions. E. g. to add and update customer users. + +=head1 PUBLIC INTERFACE + +=head2 new() + +Don't use the constructor directly, use the ObjectManager instead: + + my $CustomerUserObject = $Kernel::OM->Get('Kernel::System::CustomerUser'); + +=cut + +sub new { + my ( $Type, %Param ) = @_; + + # allocate new hash for object + my $Self = {}; + bless( $Self, $Type ); + + $Self->{CacheType} = 'CustomerUser'; + $Self->{CacheTTL} = 60 * 60 * 24 * 20; + + # get config object + my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); + + # load generator customer preferences module + my $GeneratorModule = $ConfigObject->Get('CustomerPreferences')->{Module} + || 'Kernel::System::CustomerUser::Preferences::DB'; + + # get main object + my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); + + if ( $MainObject->Require($GeneratorModule) ) { + $Self->{PreferencesObject} = $GeneratorModule->new(); + } + + # load customer user backend module + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$ConfigObject->Get("CustomerUser$Count"); + + my $GenericModule = $ConfigObject->Get("CustomerUser$Count")->{Module}; + if ( !$MainObject->Require($GenericModule) ) { + $MainObject->Die("Can't load backend module $GenericModule! $@"); + } + + $Self->{"CustomerUser$Count"} = $GenericModule->new( + Count => $Count, + PreferencesObject => $Self->{PreferencesObject}, + CustomerUserMap => $ConfigObject->Get("CustomerUser$Count"), + ); + } + + # init of event handler + $Self->EventHandlerInit( + Config => 'CustomerUser::EventModulePost', + ); + + return $Self; +} + +=head2 CustomerSourceList() + +return customer source list + + my %List = $CustomerUserObject->CustomerSourceList( + ReadOnly => 0 # optional, 1 returns only RO backends, 0 returns writable, if not passed returns all backends + ); + +=cut + +sub CustomerSourceList { + my ( $Self, %Param ) = @_; + + # get config object + my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); + + my %Data; + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$ConfigObject->Get("CustomerUser$Count"); + if ( defined $Param{ReadOnly} ) { + my $CustomerBackendConfig = $ConfigObject->Get("CustomerUser$Count"); + if ( $Param{ReadOnly} ) { + next SOURCE if !$CustomerBackendConfig->{ReadOnly} || $CustomerBackendConfig->{Module} !~ /LDAP/i; + } + else { + next SOURCE if $CustomerBackendConfig->{ReadOnly} || $CustomerBackendConfig->{Module} =~ /LDAP/i; + } + } + $Data{"CustomerUser$Count"} = $ConfigObject->Get("CustomerUser$Count")->{Name} + || "No Name $Count"; + } + return %Data; +} + +=head2 CustomerSearch() + +to search users + + # text search + my %List = $CustomerUserObject->CustomerSearch( + Search => '*some*', # also 'hans+huber' possible + Valid => 1, # (optional) default 1 + Limit => 100, # (optional) overrides limit of the config +# RotherOSS / ConsoleCommands + Source => ['ldap'], # (optional) limit search to certain backends +# EO ConsoleCommands + ); + + # username search + my %List = $CustomerUserObject->CustomerSearch( + UserLogin => '*some*', + Valid => 1, # (optional) default 1 + ); + + # email search + my %List = $CustomerUserObject->CustomerSearch( + PostMasterSearch => 'email@example.com', + Valid => 1, # (optional) default 1 + ); + + # search by CustomerID + my %List = $CustomerUserObject->CustomerSearch( + CustomerID => 'CustomerID123', + Valid => 1, # (optional) default 1 + ); + +=cut + +sub CustomerSearch { + my ( $Self, %Param ) = @_; + +# RotherOSS / ConsoleCommands + if ( $Param{Source} && !IsArrayRefWithData( $Param{Source} ) ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'debug', + Message => "Source filter is empty, ignoring it.", + ); + } + +# EO ConsoleCommands + # remove leading and ending spaces + if ( $Param{Search} ) { + $Param{Search} =~ s/^\s+//; + $Param{Search} =~ s/\s+$//; + } + + # Get dynamic field object. + my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField'); + + my $DynamicFieldConfigs = $DynamicFieldObject->DynamicFieldListGet( + ObjectType => 'CustomerUser', + Valid => 1, + ); + + my %DynamicFieldLookup = map { $_->{Name} => $_ } @{$DynamicFieldConfigs}; + + # Get dynamic field backend object. + my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); + + my %Data; + SOURCE: + for my $Count ( '', 1 .. 10 ) { + +# RotherOSS / ConsoleCommands + next SOURCE if $Param{Source} && !grep { $_ eq "CustomerUser$Count" } $Param{Source}->@*; +# EO ConsoleCommands + next SOURCE if !$Self->{"CustomerUser$Count"}; + + # search dynamic field values, if configured + my $Map = $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map}; + if ( IsArrayRefWithData($Map) ) { + + # fetch dynamic field names that are configured in Map + # only these will be considered for any other search config + # [ 'DynamicField_Name_X', undef, 'Name_X', 0, 0, 'dynamic_field', undef, 0, undef, undef, ], + my %DynamicFieldNames = map { $_->[2] => 1 } grep { $_->[5] eq 'dynamic_field' } @{$Map}; + + if ( IsHashRefWithData( \%DynamicFieldNames ) ) { + my $FoundDynamicFieldObjectIDs; + my $SearchFields; + my $SearchParam; + + # check which of the dynamic fields configured in Map are also + # configured in SearchFields + + # param Search + if ( defined $Param{Search} && length $Param{Search} ) { + $SearchFields = $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{CustomerUserSearchFields}; + $SearchParam = $Param{Search}; + } + + # param PostMasterSearch + elsif ( defined $Param{PostMasterSearch} && length $Param{PostMasterSearch} ) { + $SearchFields = $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{CustomerUserPostMasterSearchFields}; + $SearchParam = $Param{PostMasterSearch}; + } + + # search dynamic field values + if ( IsArrayRefWithData($SearchFields) ) { + my @SearchDynamicFieldNames = grep { exists $DynamicFieldNames{$_} } @{$SearchFields}; + + my %FoundDynamicFieldObjectIDs; + FIELDNAME: + for my $FieldName (@SearchDynamicFieldNames) { + + my $DynamicFieldConfig = $DynamicFieldLookup{$FieldName}; + + next FIELDNAME if !IsHashRefWithData($DynamicFieldConfig); + + my $DynamicFieldValues = $DynamicFieldBackendObject->ValueSearch( + DynamicFieldConfig => $DynamicFieldConfig, + Search => $SearchParam, + ); + + if ( IsArrayRefWithData($DynamicFieldValues) ) { + for my $DynamicFieldValue ( @{$DynamicFieldValues} ) { + $FoundDynamicFieldObjectIDs{ $DynamicFieldValue->{ObjectID} } = 1; + } + } + } + + $FoundDynamicFieldObjectIDs = [ keys %FoundDynamicFieldObjectIDs ]; + } + + # execute backend search for found object IDs + # this data is being merged with the following CustomerSearch call + if ( IsArrayRefWithData($FoundDynamicFieldObjectIDs) ) { + + my $ObjectNames = $DynamicFieldObject->ObjectMappingGet( + ObjectID => $FoundDynamicFieldObjectIDs, + ObjectType => 'CustomerUser', + ); + + OBJECTNAME: + for my $ObjectName ( values %{$ObjectNames} ) { + next OBJECTNAME if exists $Data{$ObjectName}; + + my %SearchParam = %Param; + delete $SearchParam{Search}; + delete $SearchParam{PostMasterSearch}; + + $SearchParam{UserLogin} = $ObjectName; + + my %SubData = $Self->{"CustomerUser$Count"}->CustomerSearch(%SearchParam); + + # UserLogin search does a wild-card search, but in this case only the + # exact matching user login is relevant + if ( IsHashRefWithData( \%SubData ) && exists $SubData{$ObjectName} ) { + %Data = ( + $ObjectName => $SubData{$ObjectName}, + %Data + ); + } + } + } + } + } + + # get customer search result of backend and merge it + my %SubData = $Self->{"CustomerUser$Count"}->CustomerSearch(%Param); + + %Data = ( %SubData, %Data ); + } + return %Data; +} + +=head2 CustomerSearchDetail() + +To find customer users in the system. + +The search criteria are logically AND connected. +When a list is passed as criteria, the individual members are OR connected. +When an undef or a reference to an empty array is passed, then the search criteria +is ignored. + +Returns either a list, as an arrayref, or a count of found customer user ids. +The count of results is returned when the parameter C is passed. + + my $CustomerUserIDsRef = $CustomerUserObject->CustomerSearchDetail( + + # all fields which are defined in a CustomerUserMap, except password fields, are searchable + UserLogin => 'example*', # (optional) + UserFirstname => 'Firstn*', # (optional) + + # special parameters + CustomerCompanySearchCustomerIDs => [ 'example.com' ], # (optional) + ExcludeUserLogins => [ 'example', 'doejohn' ], # (optional) + + # array parameters are used with logical OR operator (all values are possible which + are defined in the config selection hash for the field) + UserCountry => [ 'Austria', 'Germany', ], # (optional) + + # DynamicFields + # At least one operator must be specified. Operators will be connected with AND, + # values in an operator with OR. + # You can also pass more than one argument to an operator: ['value1', 'value2'] + DynamicField_FieldNameX => { + Equals => 123, + Like => 'value*', # "equals" operator with wildcard support + GreaterThan => '2001-01-01 01:01:01', + GreaterThanEquals => '2001-01-01 01:01:01', + SmallerThan => '2002-02-02 02:02:02', + SmallerThanEquals => '2002-02-02 02:02:02', + } + + OrderBy => [ 'UserLogin', 'UserCustomerID' ], # (optional) + # ignored if the result type is 'COUNT' + # default: [ 'UserLogin' ] + # (all fields which are defined in a CustomerUserMap can be used for ordering) + + # Additional information for OrderBy: + # The OrderByDirection can be specified for each OrderBy attribute. + # The pairing is made by the array indices. + + OrderByDirection => [ 'Down', 'Up' ], # (optional) + # ignored if the result type is 'COUNT' + # (Down | Up) Default: [ 'Down' ] + + Result => 'ARRAY' || 'COUNT', # (optional) + # default: ARRAY, returns an array of change ids + # COUNT returns a scalar with the number of found changes + + Limit => 100, # (optional) + # ignored if the result type is 'COUNT' + ); + +Returns: + +Result: 'ARRAY' + + @CustomerUserIDs = ( 1, 2, 3 ); + +Result: 'COUNT' + + $CustomerUserIDs = 10; + +=cut + +sub CustomerSearchDetail { + my ( $Self, %Param ) = @_; + + # get all general search fields (without a restriction to a source) + my @AllSearchFields = $Self->CustomerUserSearchFields(); + + # generate a hash with the customer user sources which must be searched + my %SearchCustomerUserSources; + + SOURCE: + for my $Count ( '', 1 .. 10 ) { + next SOURCE if !$Self->{"CustomerUser$Count"}; + + # get the search fields for the current source + my @SourceSearchFields = $Self->CustomerUserSearchFields( + Source => "CustomerUser$Count", + ); + my %LookupSourceSearchFields = map { $_->{Name} => 1 } @SourceSearchFields; + + # check if all search param exists in the search fields from the current source + SEARCHFIELD: + for my $SearchField (@AllSearchFields) { + + next SEARCHFIELD if !$Param{ $SearchField->{Name} }; + + next SOURCE if !$LookupSourceSearchFields{ $SearchField->{Name} }; + } + $SearchCustomerUserSources{"CustomerUser$Count"} = \@SourceSearchFields; + } + + # set the default behaviour for the return type + $Param{Result} ||= 'ARRAY'; + + if ( $Param{Result} eq 'COUNT' ) { + + my $IDsCount = 0; + + SOURCE: + for my $Source ( sort keys %SearchCustomerUserSources ) { + next SOURCE if !$Self->{$Source}; + + my $SubIDsCount = $Self->{$Source}->CustomerSearchDetail( + %Param, + SearchFields => $SearchCustomerUserSources{$Source}, + ); + + return if !defined $SubIDsCount; + + $IDsCount += $SubIDsCount || 0; + } + return $IDsCount; + } + else { + + my @IDs; + + my $ResultCount = 0; + + SOURCE: + for my $Source ( sort keys %SearchCustomerUserSources ) { + next SOURCE if !$Self->{$Source}; + + my $SubIDs = $Self->{$Source}->CustomerSearchDetail( + %Param, + SearchFields => $SearchCustomerUserSources{$Source}, + ); + + return if !defined $SubIDs; + + next SOURCE if !IsArrayRefWithData($SubIDs); + + push @IDs, @{$SubIDs}; + + $ResultCount++; + } + + # if we have more then one search results from diffrent sources, we need a resorting + # because of the merged single results + if ( $ResultCount > 1 ) { + + my @UserDataList; + + for my $ID (@IDs) { + + my %UserData = $Self->CustomerUserDataGet( + User => $ID, + ); + push @UserDataList, \%UserData; + } + + my $OrderBy = 'UserLogin'; + if ( IsArrayRefWithData( $Param{OrderBy} ) ) { + $OrderBy = $Param{OrderBy}->[0]; + } + + if ( IsArrayRefWithData( $Param{OrderByDirection} ) && $Param{OrderByDirection}->[0] eq 'Up' ) { + @UserDataList = sort { lc( $a->{$OrderBy} ) cmp lc( $b->{$OrderBy} ) } @UserDataList; + } + else { + @UserDataList = sort { lc( $b->{$OrderBy} ) cmp lc( $a->{$OrderBy} ) } @UserDataList; + } + + if ( $Param{Limit} && scalar @UserDataList > $Param{Limit} ) { + splice @UserDataList, $Param{Limit}; + } + + @IDs = map { $_->{UserLogin} } @UserDataList; + } + + return \@IDs; + } +} + +=head2 CustomerUserSearchFields() + +Get a list of the defined search fields (optional only the relevant fields for the given source). + + my @SeachFields = $CustomerUserObject->CustomerUserSearchFields( + Source => 'CustomerUser', # optional, but important in the CustomerSearchDetail to get the right database fields + ); + +Returns an array of hash references. + + @SeachFields = ( + { + Name => 'UserEmail', + Label => 'Email', + Type => 'Input', + DatabaseField => 'mail', + }, + { + Name => 'UserCountry', + Label => 'Country', + Type => 'Selection', + SelectionsData => { + 'Germany' => 'Germany', + 'United Kingdom' => 'United Kingdom', + 'United States' => 'United States', + ... + }, + DatabaseField => 'country', + }, + { + Name => 'DynamicField_SkypeAccountName', + Label => '', + Type => 'DynamicField', + DatabaseField => 'SkypeAccountName', + }, + ); + +=cut + +sub CustomerUserSearchFields { + my ( $Self, %Param ) = @_; + + # Get the search fields from all customer user maps (merge from all maps together). + my @SearchFields; + + my %SearchFieldsExists; + + SOURCE: + for my $Count ( '', 1 .. 10 ) { + next SOURCE if !$Self->{"CustomerUser$Count"}; + next SOURCE if $Param{Source} && $Param{Source} ne "CustomerUser$Count"; + + ENTRY: + for my $Entry ( @{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} } ) { + + my $SearchFieldName = $Entry->[0]; + + next ENTRY if $SearchFieldsExists{$SearchFieldName}; + next ENTRY if $SearchFieldName =~ m{(Password|Pw)\d*$}smxi; + + # Remember the already collected search field name. + $SearchFieldsExists{$SearchFieldName} = 1; + + my %FieldConfig = $Self->GetFieldConfig( + FieldName => $SearchFieldName, + Source => $Param{Source}, # to get the right database field for the given source + ); + + next ENTRY if !%FieldConfig; + + my %SearchFieldData = ( + %FieldConfig, + Name => $SearchFieldName, + ); + + my %SelectionsData = $Self->GetFieldSelections( + FieldName => $SearchFieldName, + ); + + if ( $SearchFieldData{StorageType} eq 'dynamic_field' ) { + $SearchFieldData{Type} = 'DynamicField'; + } + elsif (%SelectionsData) { + $SearchFieldData{Type} = 'Selection'; + $SearchFieldData{SelectionsData} = \%SelectionsData; + } + else { + $SearchFieldData{Type} = 'Input'; + } + + push @SearchFields, \%SearchFieldData; + } + } + + return @SearchFields; +} + +=head2 GetFieldConfig() + +This function collect some field config information from the customer user map. + + my %FieldConfig = $CustomerUserObject->GetFieldConfig( + FieldName => 'UserEmail', + Source => 'CustomerUser', # optional + ); + +Returns some field config information: + + my %FieldConfig = ( + Label => 'Email', + DatabaseField => 'email', + StorageType => 'var', + ); + +=cut + +sub GetFieldConfig { + my ( $Self, %Param ) = @_; + + if ( !$Param{FieldName} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "Need FieldName!" + ); + return; + } + + SOURCE: + for my $Count ( '', 1 .. 10 ) { + next SOURCE if !$Self->{"CustomerUser$Count"}; + next SOURCE if $Param{Source} && $Param{Source} ne "CustomerUser$Count"; + + # Search the right field and return some config information from the field. + ENTRY: + for my $Entry ( @{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} } ) { + next ENTRY if $Param{FieldName} ne $Entry->[0]; + + my %FieldConfig = ( + Label => $Entry->[1], + DatabaseField => $Entry->[2], + StorageType => $Entry->[5], + ); + + return %FieldConfig; + } + } + + return; +} + +=head2 GetFieldSelections() + +This function collect the selections for the given field name, if the field has some selections. + + my %SelectionsData = $CustomerUserObject->GetFieldSelections( + FieldName => 'UserTitle', + ); + +Returns the selections for the given field name (merged from all sources) or a empty hash: + + my %SelectionData = ( + 'Mr.' => 'Mr.', + 'Mrs.' => 'Mrs.', + ); + +=cut + +sub GetFieldSelections { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{FieldName} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "Need FieldName!" + ); + return; + } + + my %SelectionsData; + + SOURCE: + for my $Count ( '', 1 .. 10 ) { + next SOURCE if !$Self->{"CustomerUser$Count"}; + next SOURCE if !$Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Selections}->{ $Param{FieldName} }; + + %SelectionsData = ( + %SelectionsData, %{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Selections}->{ $Param{FieldName} } } + ); + } + + # Make sure the encoding stamp is set. + for my $Key ( sort keys %SelectionsData ) { + $SelectionsData{$Key} = $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( $SelectionsData{$Key} ); + } + + # Default handling for field 'ValidID'. + if ( !%SelectionsData && $Param{FieldName} =~ /^ValidID/i ) { + %SelectionsData = $Kernel::OM->Get('Kernel::System::Valid')->ValidList(); + } + + return %SelectionsData; +} + +=head2 CustomerIDList() + +return a list of with all known unique CustomerIDs of the registered customers users (no SearchTerm), +or a filtered list where the CustomerIDs must contain a search term. + + my @CustomerIDs = $CustomerUserObject->CustomerIDList( + SearchTerm => 'somecustomer', # optional + Valid => 1, # optional + ); + +=cut + +sub CustomerIDList { + my ( $Self, %Param ) = @_; + + my @Data; + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$Self->{"CustomerUser$Count"}; + + # get customer list result of backend and merge it + push @Data, $Self->{"CustomerUser$Count"}->CustomerIDList(%Param); + } + + # make entries unique + my %Tmp; + @Tmp{@Data} = undef; + @Data = sort { lc $a cmp lc $b } keys %Tmp; + + return @Data; +} + +=head2 CustomerName() + +get customer user name + + my $Name = $CustomerUserObject->CustomerName( + UserLogin => 'some-login', + ); + +=cut + +sub CustomerName { + my ( $Self, %Param ) = @_; + + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$Self->{"CustomerUser$Count"}; + + # Get customer name and return it. + my $Name = $Self->{"CustomerUser$Count"}->CustomerName(%Param); + if ($Name) { + return $Name; + } + } + return; +} + +=head2 CustomerIDs() + +get customer user customer ids + + my @CustomerIDs = $CustomerUserObject->CustomerIDs( + User => 'some-login', + ); + +=cut + +sub CustomerIDs { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{User} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => 'Need User!' + ); + return; + } + + # get customer ids (stop after first source with results) + my @CustomerIDs; + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$Self->{"CustomerUser$Count"}; + + # get customer ids from source + my @SourceCustomerIDs = $Self->{"CustomerUser$Count"}->CustomerIDs(%Param); + next SOURCE if !@SourceCustomerIDs; + + @CustomerIDs = @SourceCustomerIDs; + last SOURCE; + } + + # create hash with existing customer ids + my %CustomerIDs = map { $_ => 1 } @CustomerIDs; + + # get related customer ids + my @RelatedCustomerIDs = $Self->CustomerUserCustomerMemberList( + CustomerUserID => $Param{User}, + ); + + # add related customer ids if not found in source + RELATEDCUSTOMERID: + for my $RelatedCustomerID (@RelatedCustomerIDs) { + next RELATEDCUSTOMERID if $CustomerIDs{$RelatedCustomerID}; + + push @CustomerIDs, $RelatedCustomerID; + } + + # return customer ids + return @CustomerIDs; +} + +=head2 CustomerUserDataGet() + +get user data (UserLogin, UserFirstname, UserLastname, UserEmail, ...) + + my %User = $CustomerUserObject->CustomerUserDataGet( + User => 'franz', + ); + +=cut + +sub CustomerUserDataGet { + my ( $Self, %Param ) = @_; + + return if !$Param{User}; + + # fetch dynamic field configurations for CustomerUser. + my $DynamicFieldConfigs = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet( + ObjectType => 'CustomerUser', + Valid => 1, + ); + + my %DynamicFieldLookup = map { $_->{Name} => $_ } @{$DynamicFieldConfigs}; + + # Get needed objects. + my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); + my $CustomerCompanyObject = $Kernel::OM->Get('Kernel::System::CustomerCompany'); + my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); + + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$Self->{"CustomerUser$Count"}; + + my %Customer = $Self->{"CustomerUser$Count"}->CustomerUserDataGet(%Param); + next SOURCE if !%Customer; + + # generate the full name and save it in the hash + my $UserFullname = $Self->CustomerName(%Customer); + + # save the generated fullname in the hash. + $Customer{UserFullname} = $UserFullname; + + # add preferences defaults + my $Config = $ConfigObject->Get('CustomerPreferencesGroups'); + if ($Config) { + KEY: + for my $Key ( sort keys %{$Config} ) { + + next KEY if !defined $Config->{$Key}->{DataSelected}; + next KEY if defined $Customer{ $Config->{$Key}->{PrefKey} }; + + # set default data + $Customer{ $Config->{$Key}->{PrefKey} } = $Config->{$Key}->{DataSelected}; + } + } + + # check if customer company support is enabled and get company data + my %Company; + if ( + $ConfigObject->Get("CustomerCompany") + && $ConfigObject->Get("CustomerUser$Count")->{CustomerCompanySupport} + ) + { + %Company = $CustomerCompanyObject->CustomerCompanyGet( + CustomerID => $Customer{UserCustomerID}, + ); + + $Company{CustomerCompanyValidID} = $Company{ValidID}; + } + + # fetch dynamic field values + if ( IsArrayRefWithData( $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} ) ) { + CUSTOMERUSERFIELD: + for my $CustomerUserField ( @{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} } ) { + next CUSTOMERUSERFIELD if $CustomerUserField->[5] ne 'dynamic_field'; + next CUSTOMERUSERFIELD if !$DynamicFieldLookup{ $CustomerUserField->[2] }; + + my $Value = $DynamicFieldBackendObject->ValueGet( + DynamicFieldConfig => $DynamicFieldLookup{ $CustomerUserField->[2] }, + ObjectName => $Customer{UserID}, + ); + + $Customer{ $CustomerUserField->[0] } = $Value; + } + } + + # return customer data + return ( + %Company, + %Customer, + Source => "CustomerUser$Count", + Config => $ConfigObject->Get("CustomerUser$Count"), + CompanyConfig => $ConfigObject->Get( $Company{Source} // 'CustomerCompany' ), + ); + } + + return; +} + +=head2 CustomerUserAdd() + +to add new customer users + + my $UserLogin = $CustomerUserObject->CustomerUserAdd( + Source => 'CustomerUser', # CustomerUser source config + UserFirstname => 'Huber', + UserLastname => 'Manfred', + UserCustomerID => 'A124', + UserLogin => 'mhuber', + UserPassword => 'some-pass', # not required + UserEmail => 'email@example.com', + ValidID => 1, + UserID => 123, + ); + +=cut + +sub CustomerUserAdd { + my ( $Self, %Param ) = @_; + + # check data source + if ( !$Param{Source} ) { + $Param{Source} = 'CustomerUser'; + } + + # check if user exists + if ( $Param{UserLogin} ) { + my %User = $Self->CustomerUserDataGet( User => $Param{UserLogin} ); + if (%User) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => $Kernel::OM->Get('Kernel::Language')->Translate( 'Customer user "%s" already exists.', $Param{UserLogin} ), + ); + return; + } + } + + # store customer user data + my $Result = $Self->{ $Param{Source} }->CustomerUserAdd(%Param); + return if !$Result; + + # trigger event + $Self->EventHandler( + Event => 'CustomerUserAdd', + Data => { + UserLogin => $Param{UserLogin}, + NewData => \%Param, + }, + UserID => $Param{UserID}, + ); + + return $Result; + +} + +=head2 CustomerUserUpdate() + +to update customer users + + $CustomerUserObject->CustomerUserUpdate( + Source => 'CustomerUser', # CustomerUser source config + ID => 'mh' # current user login + UserLogin => 'mhuber', # new user login + UserFirstname => 'Huber', + UserLastname => 'Manfred', + UserPassword => 'some-pass', # not required + UserEmail => 'email@example.com', + ValidID => 1, + UserID => 123, + ); + +=cut + +sub CustomerUserUpdate { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{UserLogin} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "Need UserLogin!" + ); + return; + } + + # check for UserLogin-renaming and if new UserLogin already exists... + if ( $Param{ID} && ( lc $Param{UserLogin} ne lc $Param{ID} ) ) { + my %User = $Self->CustomerUserDataGet( User => $Param{UserLogin} ); + if (%User) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => $Kernel::OM->Get('Kernel::Language')->Translate( 'Customer user "%s" already exists.', $Param{UserLogin} ), + ); + return; + } + } + + # check if user exists + my %User = $Self->CustomerUserDataGet( User => $Param{ID} || $Param{UserLogin} ); + if ( !%User ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "No such user '$Param{UserLogin}'!", + ); + return; + } + + my $Result = $Self->{ $User{Source} }->CustomerUserUpdate(%Param); + return if !$Result; + + # trigger event + $Self->EventHandler( + Event => 'CustomerUserUpdate', + Data => { + UserLogin => $Param{ID} || $Param{UserLogin}, + NewData => \%Param, + OldData => \%User, + }, + UserID => $Param{UserID}, + ); + + return $Result; +} + +=head2 SetPassword() + +to set customer users passwords + + $CustomerUserObject->SetPassword( + UserLogin => 'some-login', + PW => 'some-new-password' + ); + +=cut + +sub SetPassword { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{UserLogin} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => 'User UserLogin!' + ); + return; + } + + # check if user exists + my %User = $Self->CustomerUserDataGet( User => $Param{UserLogin} ); + if ( !%User ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "No such user '$Param{UserLogin}'!", + ); + return; + } + return $Self->{ $User{Source} }->SetPassword(%Param); +} + +=head2 GenerateRandomPassword() + +generate a random password + + my $Password = $CustomerUserObject->GenerateRandomPassword(); + + or + + my $Password = $CustomerUserObject->GenerateRandomPassword( + Size => 16, + ); + +=cut + +sub GenerateRandomPassword { + my ( $Self, %Param ) = @_; + + return $Self->{CustomerUser}->GenerateRandomPassword(%Param); +} + +=head2 SetPreferences() + +set customer user preferences + + $CustomerUserObject->SetPreferences( + Key => 'UserComment', + Value => 'some comment', + UserID => 'some-login', + ); + +=cut + +sub SetPreferences { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{UserID} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => 'Need UserID!' + ); + return; + } + + # Don't allow overwriting of native user data. + my %Blacklisted = ( + UserID => 1, + UserLogin => 1, + UserPassword => 1, + UserFirstname => 1, + UserLastname => 1, + UserFullname => 1, + UserStreet => 1, + UserCity => 1, + UserZip => 1, + UserCountry => 1, + UserComment => 1, + UserCustomerID => 1, + UserTitle => 1, + UserEmail => 1, + ChangeTime => 1, + ChangeBy => 1, + CreateTime => 1, + CreateBy => 1, + UserPhone => 1, + UserMobile => 1, + UserFax => 1, + UserMailString => 1, + ValidID => 1, + ); + + return 0 if $Blacklisted{ $Param{Key} }; + + # check if user exists + my %User = $Self->CustomerUserDataGet( User => $Param{UserID} ); + if ( !%User ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "No such user '$Param{UserID}'!", + ); + return; + } + + # call new api (2.4.8 and higher) + if ( $Self->{ $User{Source} }->can('SetPreferences') ) { + return $Self->{ $User{Source} }->SetPreferences(%Param); + } + + # call old api + return $Self->{PreferencesObject}->SetPreferences(%Param); +} + +=head2 GetPreferences() + +get customer user preferences + + my %Preferences = $CustomerUserObject->GetPreferences( + UserID => 'some-login', + ); + +=cut + +sub GetPreferences { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{UserID} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => 'Need UserID!' + ); + return; + } + + # check if user exists + my %User = $Self->CustomerUserDataGet( User => $Param{UserID} ); + if ( !%User ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "No such user '$Param{UserID}'!", + ); + return; + } + + # call new api (2.4.8 and higher) + if ( $Self->{ $User{Source} }->can('GetPreferences') ) { + return $Self->{ $User{Source} }->GetPreferences(%Param); + } + + # call old api + return $Self->{PreferencesObject}->GetPreferences(%Param); +} + +=head2 SearchPreferences() + +search in user preferences + + my %UserList = $CustomerUserObject->SearchPreferences( + Key => 'UserSomeKey', + Value => 'SomeValue', # optional, limit to a certain value/pattern + ); + +=cut + +sub SearchPreferences { + my ( $Self, %Param ) = @_; + + my %Data; + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$Self->{"CustomerUser$Count"}; + + # get customer search result of backend and merge it + # call new api (2.4.8 and higher) + my %SubData; + if ( $Self->{"CustomerUser$Count"}->can('SearchPreferences') ) { + %SubData = $Self->{"CustomerUser$Count"}->SearchPreferences(%Param); + } + + # call old api + else { + %SubData = $Self->{PreferencesObject}->SearchPreferences(%Param); + } + %Data = ( %SubData, %Data ); + } + + return %Data; +} + +=head2 TokenGenerate() + +generate a random token + + my $Token = $CustomerUserObject->TokenGenerate( + UserID => 123, + ); + +=cut + +sub TokenGenerate { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{UserID} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "Need UserID!" + ); + return; + } + + my $Token = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString( + Length => 14, + ); + + # save token in preferences + $Self->SetPreferences( + Key => 'UserToken', + Value => $Token, + UserID => $Param{UserID}, + ); + + return $Token; +} + +=head2 TokenCheck() + +check password token + + my $Valid = $CustomerUserObject>TokenCheck( + Token => $Token, + UserID => 123, + ); + +=cut + +sub TokenCheck { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{Token} || !$Param{UserID} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "Need Token and UserID!" + ); + return; + } + + # get preferences token + my %Preferences = $Self->GetPreferences( + UserID => $Param{UserID}, + ); + + # check requested vs. stored token + return if !$Preferences{UserToken}; + return if $Preferences{UserToken} ne $Param{Token}; + + # reset password token + $Self->SetPreferences( + Key => 'UserToken', + Value => '', + UserID => $Param{UserID}, + ); + + return 1; +} + +=head2 CustomerUserCacheClear() + +clear cache of customer user data + + $CustomerUserObject->CustomerUserCacheClear( + UserLogin => 'mhuber', + ); + +=cut + +sub CustomerUserCacheClear { + my ( $Self, %Param ) = @_; + + SOURCE: + for my $Count ( '', 1 .. 10 ) { + + next SOURCE if !$Self->{"CustomerUser$Count"}; + $Self->{"CustomerUser$Count"}->_CustomerUserCacheClear( + UserLogin => $Param{UserLogin}, + ); + } + + return 1; +} + +=head2 CustomerUserCustomerMemberAdd() + +to add a customer user to a customer + + my $Success = $CustomerUserObject->CustomerUserCustomerMemberAdd( + CustomerUserID => 123, + CustomerID => 123, + Active => 1, # optional + UserID => 123, + ); + +=cut + +sub CustomerUserCustomerMemberAdd { + my ( $Self, %Param ) = @_; + + # check needed stuff + for my $Argument (qw(CustomerUserID CustomerID UserID)) { + if ( !$Param{$Argument} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "Need $Argument!", + ); + return; + } + } + + # delete affected caches + my $CacheKey = 'Cache::CustomerUserCustomerMemberList::'; + $Kernel::OM->Get('Kernel::System::Cache')->Delete( + Type => $Self->{CacheType}, + Key => $CacheKey . 'CustomerUserID::' . $Param{CustomerUserID}, + ); + $Kernel::OM->Get('Kernel::System::Cache')->Delete( + Type => $Self->{CacheType}, + Key => $CacheKey . 'CustomerID::' . $Param{CustomerID}, + ); + + # get database object + my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); + + # delete existing relation + return if !$DBObject->Do( + SQL => 'DELETE FROM customer_user_customer + WHERE user_id = ? + AND customer_id = ?', + Bind => [ \$Param{CustomerUserID}, \$Param{CustomerID} ], + ); + + # return if relation is not active + return 1 if !$Param{Active}; + + # insert new relation + return if !$DBObject->Do( + SQL => ' + INSERT INTO customer_user_customer (user_id, customer_id, create_time, create_by, + change_time, change_by) + VALUES (?, ?, current_timestamp, ?, current_timestamp, ?)', + Bind => [ \$Param{CustomerUserID}, \$Param{CustomerID}, \$Param{UserID}, \$Param{UserID}, ], + ); + + return 1; +} + +=head2 CustomerUserCustomerMemberList() + +get related customer IDs of a customer user + + my @CustomerIDs = $CustomerUserObject->CustomerUserCustomerMemberList( + CustomerUserID => 123, + ); + +Returns: + @CustomerIDs = ( + '123', + '456', + ); + +get related customer users of a customer ID + + my @CustomerUsers = $CustomerUserObject->CustomerUserCustomerMemberList( + CustomerID => 123, + ); + +Returns: + @CustomerUsers = ( + '123', + '456', + ); + +=cut + +sub CustomerUserCustomerMemberList { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{CustomerUserID} && !$Param{CustomerID} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => 'Got no CustomerUserID or CustomerID!', + ); + return; + } + + # get needed objects + my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); + my $CacheKey = 'Cache::CustomerUserCustomerMemberList::'; + + if ( $Param{CustomerUserID} ) { + + # check if this result is present (in cache) + $CacheKey .= 'CustomerUserID::' . $Param{CustomerUserID}; + my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get( + Type => $Self->{CacheType}, + Key => $CacheKey, + ); + return @{$Cache} if $Cache; + + # get customer ids + return if !$DBObject->Prepare( + SQL => + 'SELECT customer_id + FROM customer_user_customer + WHERE user_id = ? + ORDER BY customer_id', + Bind => [ \$Param{CustomerUserID}, ], + ); + + # fetch the result + my @CustomerIDs; + while ( my @Row = $DBObject->FetchrowArray() ) { + push @CustomerIDs, $Row[0]; + } + + # cache the result + $Kernel::OM->Get('Kernel::System::Cache')->Set( + Type => $Self->{CacheType}, + TTL => $Self->{CacheTTL}, + Key => $CacheKey, + Value => \@CustomerIDs, + + ); + + return @CustomerIDs; + } + else { + + # check if this result is present (in cache) + $CacheKey .= 'CustomerID::' . $Param{CustomerID}; + my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get( + Type => $Self->{CacheType}, + Key => $CacheKey, + ); + return @{$Cache} if $Cache; + + # get customer users + return if !$DBObject->Prepare( + SQL => + 'SELECT user_id + FROM customer_user_customer WHERE + customer_id = ? + ORDER BY user_id', + Bind => [ \$Param{CustomerID}, ], + ); + + # fetch the result + my @CustomerUserIDs; + while ( my @Row = $DBObject->FetchrowArray() ) { + push @CustomerUserIDs, $Row[0]; + } + + # cache the result + $Kernel::OM->Get('Kernel::System::Cache')->Set( + Type => $Self->{CacheType}, + TTL => $Self->{CacheTTL}, + Key => $CacheKey, + Value => \@CustomerUserIDs, + ); + + return @CustomerUserIDs; + } +} + +=head2 DeleteOnePreference() + +get customer user preferences + + my %Preferences = $CustomerUserObject->DeleteOnePreference( + UserID => 'some-login', + Key => 'PreferenceKey', + ); + +=cut + +sub DeleteOnePreference { + my ( $Self, %Param ) = @_; + + # check needed stuff + if ( !$Param{UserID} ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => 'Need UserID!' + ); + return; + } + + # check if user exists + my %User = $Self->CustomerUserDataGet( User => $Param{UserID} ); + if ( !%User ) { + $Kernel::OM->Get('Kernel::System::Log')->Log( + Priority => 'error', + Message => "No such user '$Param{UserID}'!", + ); + return; + } + + # call new api (2.4.8 and higher) + if ( $Self->{ $User{Source} }->can('DeleteOnePreference') ) { + return $Self->{ $User{Source} }->DeleteOnePreference(%Param); + } + + # call old api + return $Self->{PreferencesObject}->DeleteOnePreference(%Param); +} + +sub DESTROY { + my $Self = shift; + + # execute all transaction events + $Self->EventHandlerTransaction(); + + return 1; +} + +1; diff --git a/Kernel/System/Console/Command/Admin/CustomerUser/List.pm b/Kernel/System/Console/Command/Admin/CustomerUser/List.pm new file mode 100644 index 0000000..82b08a8 --- /dev/null +++ b/Kernel/System/Console/Command/Admin/CustomerUser/List.pm @@ -0,0 +1,75 @@ +# -- +# OTOBO is a web-based ticketing system for service organisations. +# -- +# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ +# Copyright (C) 2019-2023 Rother OSS GmbH, https://otobo.de/ +# -- +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# -- + +package Kernel::System::Console::Command::Admin::CustomerUser::Add; + +use strict; +use warnings; + +use parent qw(Kernel::System::Console::BaseCommand); + +our @ObjectDependencies = ( + 'Kernel::System::CustomerUser', +); + +sub Configure { + my ( $Self, %Param ) = @_; + + $Self->Description('List or search for customer users.'); + $Self->AddOption( + Name => 'search-term', + Description => "Term to search by for customer users.", + Required => 0, + HasValue => 1, + ValueRegex => qr/.*/smx, + ); + $Self->AddOption( + Name => 'source', + Description => "Filter by one or more comma-separated sources.", + Required => 0, + HasValue => 1, + ValueRegex => qr/.*/smx, + ); + $Self->AddOption( + Name => 'output-format', + Description => "Define output format - default is 'plain'.", + Required => 0, + HasValue => 1, + ValueRegex => qr/(json|plain|markdown|xml)/smx, + ); + $Self->AddOption( + Name => 'output-columns', + Description => "Comma-separated output columns. Possible are login, email, customercompany, title, firstname, lastname, phone, fax, mobile, street, zip, city, country, comments, valid. Default are login and email.", + Required => 0, + HasValue => 1, + ValueRegex => qr/((login|email|customercompany|title|firstname|lastname|phone|fax|mobile|street|zip|city|country|comments|valid),?)*/smx, + ); + + return; +} + +sub Run { + my ( $Self, %Param ) = @_; + + $Self->Print("Fetching customer users...\n"); + + my $CustomerUserObject = $Kernel::OM->Get('Kernel::System::CustomerUser'); + + $Self->Print("Done.\n"); + return $Self->ExitCodeOk(); +} + +1;