1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::System::CustomerUser;
10
11use strict;
12use warnings;
13
14use Kernel::System::VariableCheck qw(:all);
15
16use parent qw(Kernel::System::EventHandler);
17
18our @ObjectDependencies = (
19    'Kernel::Config',
20    'Kernel::Language',
21    'Kernel::System::Cache',
22    'Kernel::System::CustomerCompany',
23    'Kernel::System::DB',
24    'Kernel::System::DynamicField',
25    'Kernel::System::DynamicField::Backend',
26    'Kernel::System::Encode',
27    'Kernel::System::Log',
28    'Kernel::System::Main',
29    'Kernel::System::Valid',
30);
31
32=head1 NAME
33
34Kernel::System::CustomerUser - customer user lib
35
36=head1 DESCRIPTION
37
38All customer user functions. E. g. to add and update customer users.
39
40=head1 PUBLIC INTERFACE
41
42=head2 new()
43
44Don't use the constructor directly, use the ObjectManager instead:
45
46    my $CustomerUserObject = $Kernel::OM->Get('Kernel::System::CustomerUser');
47
48=cut
49
50sub new {
51    my ( $Type, %Param ) = @_;
52
53    # allocate new hash for object
54    my $Self = {};
55    bless( $Self, $Type );
56
57    $Self->{CacheType} = 'CustomerUser';
58    $Self->{CacheTTL}  = 60 * 60 * 24 * 20;
59
60    # get config object
61    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
62
63    # load generator customer preferences module
64    my $GeneratorModule = $ConfigObject->Get('CustomerPreferences')->{Module}
65        || 'Kernel::System::CustomerUser::Preferences::DB';
66
67    # get main object
68    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
69
70    if ( $MainObject->Require($GeneratorModule) ) {
71        $Self->{PreferencesObject} = $GeneratorModule->new();
72    }
73
74    # load customer user backend module
75    SOURCE:
76    for my $Count ( '', 1 .. 10 ) {
77
78        next SOURCE if !$ConfigObject->Get("CustomerUser$Count");
79
80        my $GenericModule = $ConfigObject->Get("CustomerUser$Count")->{Module};
81        if ( !$MainObject->Require($GenericModule) ) {
82            $MainObject->Die("Can't load backend module $GenericModule! $@");
83        }
84
85        $Self->{"CustomerUser$Count"} = $GenericModule->new(
86            Count             => $Count,
87            PreferencesObject => $Self->{PreferencesObject},
88            CustomerUserMap   => $ConfigObject->Get("CustomerUser$Count"),
89        );
90    }
91
92    # init of event handler
93    $Self->EventHandlerInit(
94        Config => 'CustomerUser::EventModulePost',
95    );
96
97    return $Self;
98}
99
100=head2 CustomerSourceList()
101
102return customer source list
103
104    my %List = $CustomerUserObject->CustomerSourceList(
105        ReadOnly => 0 # optional, 1 returns only RO backends, 0 returns writable, if not passed returns all backends
106    );
107
108=cut
109
110sub CustomerSourceList {
111    my ( $Self, %Param ) = @_;
112
113    # get config object
114    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
115
116    my %Data;
117    SOURCE:
118    for my $Count ( '', 1 .. 10 ) {
119
120        next SOURCE if !$ConfigObject->Get("CustomerUser$Count");
121        if ( defined $Param{ReadOnly} ) {
122            my $CustomerBackendConfig = $ConfigObject->Get("CustomerUser$Count");
123            if ( $Param{ReadOnly} ) {
124                next SOURCE if !$CustomerBackendConfig->{ReadOnly} || $CustomerBackendConfig->{Module} !~ /LDAP/i;
125            }
126            else {
127                next SOURCE if $CustomerBackendConfig->{ReadOnly} || $CustomerBackendConfig->{Module} =~ /LDAP/i;
128            }
129        }
130        $Data{"CustomerUser$Count"} = $ConfigObject->Get("CustomerUser$Count")->{Name}
131            || "No Name $Count";
132    }
133    return %Data;
134}
135
136=head2 CustomerSearch()
137
138to search users
139
140    # text search
141    my %List = $CustomerUserObject->CustomerSearch(
142        Search => '*some*', # also 'hans+huber' possible
143        Valid  => 1,        # (optional) default 1
144        Limit  => 100,      # (optional) overrides limit of the config
145    );
146
147    # username search
148    my %List = $CustomerUserObject->CustomerSearch(
149        UserLogin => '*some*',
150        Valid     => 1,         # (optional) default 1
151    );
152
153    # email search
154    my %List = $CustomerUserObject->CustomerSearch(
155        PostMasterSearch => 'email@example.com',
156        Valid            => 1,                    # (optional) default 1
157    );
158
159    # search by CustomerID
160    my %List = $CustomerUserObject->CustomerSearch(
161        CustomerID       => 'CustomerID123',
162        Valid            => 1,                # (optional) default 1
163    );
164
165=cut
166
167sub CustomerSearch {
168    my ( $Self, %Param ) = @_;
169
170    # remove leading and ending spaces
171    if ( $Param{Search} ) {
172        $Param{Search} =~ s/^\s+//;
173        $Param{Search} =~ s/\s+$//;
174    }
175
176    # Get dynamic fiekd object.
177    my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField');
178
179    my $DynamicFieldConfigs = $DynamicFieldObject->DynamicFieldListGet(
180        ObjectType => 'CustomerUser',
181        Valid      => 1,
182    );
183
184    my %DynamicFieldLookup = map { $_->{Name} => $_ } @{$DynamicFieldConfigs};
185
186    # Get dynamic field backend object.
187    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
188
189    my %Data;
190    SOURCE:
191    for my $Count ( '', 1 .. 10 ) {
192
193        next SOURCE if !$Self->{"CustomerUser$Count"};
194
195        # search dynamic field values, if configured
196        my $Map = $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map};
197        if ( IsArrayRefWithData($Map) ) {
198
199            # fetch dynamic field names that are configured in Map
200            # only these will be considered for any other search config
201            # [ 'DynamicField_Name_X', undef, 'Name_X', 0, 0, 'dynamic_field', undef, 0, undef, undef, ],
202            my %DynamicFieldNames = map { $_->[2] => 1 } grep { $_->[5] eq 'dynamic_field' } @{$Map};
203
204            if ( IsHashRefWithData( \%DynamicFieldNames ) ) {
205                my $FoundDynamicFieldObjectIDs;
206                my $SearchFields;
207                my $SearchParam;
208
209                # check which of the dynamic fields configured in Map are also
210                # configured in SearchFields
211
212                # param Search
213                if ( defined $Param{Search} && length $Param{Search} ) {
214                    $SearchFields = $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{CustomerUserSearchFields};
215                    $SearchParam  = $Param{Search};
216                }
217
218                # param PostMasterSearch
219                elsif ( defined $Param{PostMasterSearch} && length $Param{PostMasterSearch} ) {
220                    $SearchFields
221                        = $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{CustomerUserPostMasterSearchFields};
222                    $SearchParam = $Param{PostMasterSearch};
223                }
224
225                # search dynamic field values
226                if ( IsArrayRefWithData($SearchFields) ) {
227                    my @SearchDynamicFieldNames = grep { exists $DynamicFieldNames{$_} } @{$SearchFields};
228                    my @SearchDynamicFieldIDs;
229
230                    my %FoundDynamicFieldObjectIDs;
231                    FIELDNAME:
232                    for my $FieldName (@SearchDynamicFieldNames) {
233
234                        my $DynamicFieldConfig = $DynamicFieldLookup{$FieldName};
235
236                        next FIELDNAME if !IsHashRefWithData($DynamicFieldConfig);
237
238                        my $DynamicFieldValues = $DynamicFieldBackendObject->ValueSearch(
239                            DynamicFieldConfig => $DynamicFieldConfig,
240                            Search             => $SearchParam,
241                        );
242
243                        if ( IsArrayRefWithData($DynamicFieldValues) ) {
244                            for my $DynamicFieldValue ( @{$DynamicFieldValues} ) {
245                                $FoundDynamicFieldObjectIDs{ $DynamicFieldValue->{ObjectID} } = 1;
246                            }
247                        }
248                    }
249
250                    $FoundDynamicFieldObjectIDs = [ keys %FoundDynamicFieldObjectIDs ];
251                }
252
253                # execute backend search for found object IDs
254                # this data is being merged with the following CustomerSearch call
255                if ( IsArrayRefWithData($FoundDynamicFieldObjectIDs) ) {
256
257                    my $ObjectNames = $DynamicFieldObject->ObjectMappingGet(
258                        ObjectID   => $FoundDynamicFieldObjectIDs,
259                        ObjectType => 'CustomerUser',
260                    );
261
262                    OBJECTNAME:
263                    for my $ObjectName ( values %{$ObjectNames} ) {
264                        next OBJECTNAME if exists $Data{$ObjectName};
265
266                        my %SearchParam = %Param;
267                        delete $SearchParam{Search};
268                        delete $SearchParam{PostMasterSearch};
269
270                        $SearchParam{UserLogin} = $ObjectName;
271
272                        my %SubData = $Self->{"CustomerUser$Count"}->CustomerSearch(%SearchParam);
273
274                        # UserLogin search does a wild-card search, but in this case only the
275                        # exact matching user login is relevant
276                        if ( IsHashRefWithData( \%SubData ) && exists $SubData{$ObjectName} ) {
277                            %Data = (
278                                $ObjectName => $SubData{$ObjectName},
279                                %Data
280                            );
281                        }
282                    }
283                }
284            }
285        }
286
287        # get customer search result of backend and merge it
288        my %SubData = $Self->{"CustomerUser$Count"}->CustomerSearch(%Param);
289
290        %Data = ( %SubData, %Data );
291    }
292    return %Data;
293}
294
295=head2 CustomerSearchDetail()
296
297To find customer user in the system.
298
299The search criteria are logically AND connected.
300When a list is passed as criteria, the individual members are OR connected.
301When an undef or a reference to an empty array is passed, then the search criteria
302is ignored.
303
304Returns either a list, as an arrayref, or a count of found customer user ids.
305The count of results is returned when the parameter C<Result = 'COUNT'> is passed.
306
307    my $CustomerUserIDsRef = $CustomerUserObject->CustomerSearchDetail(
308
309        # all search fields possible which are defined in CustomerUser::EnhancedSearchFields
310        UserLogin     => 'example*',                                    # (optional)
311        UserFirstname => 'Firstn*',                                     # (optional)
312
313        # special parameters
314        CustomerCompanySearchCustomerIDs => [ 'example.com' ],          # (optional)
315        ExcludeUserLogins                => [ 'example', 'doejohn' ],   # (optional)
316
317        # array parameters are used with logical OR operator (all values are possible which
318        are defined in the config selection hash for the field)
319        UserCountry              => [ 'Austria', 'Germany', ],          # (optional)
320
321        # DynamicFields
322        #   At least one operator must be specified. Operators will be connected with AND,
323        #       values in an operator with OR.
324        #   You can also pass more than one argument to an operator: ['value1', 'value2']
325        DynamicField_FieldNameX => {
326            Equals            => 123,
327            Like              => 'value*',                # "equals" operator with wildcard support
328            GreaterThan       => '2001-01-01 01:01:01',
329            GreaterThanEquals => '2001-01-01 01:01:01',
330            SmallerThan       => '2002-02-02 02:02:02',
331            SmallerThanEquals => '2002-02-02 02:02:02',
332        }
333
334        OrderBy => [ 'UserLogin', 'UserCustomerID' ],                   # (optional)
335        # ignored if the result type is 'COUNT'
336        # default: [ 'UserLogin' ]
337        # (all search fields possible which are defined in
338        CustomerUser::EnhancedSearchFields)
339
340        # Additional information for OrderBy:
341        # The OrderByDirection can be specified for each OrderBy attribute.
342        # The pairing is made by the array indices.
343
344        OrderByDirection => [ 'Down', 'Up' ],                          # (optional)
345        # ignored if the result type is 'COUNT'
346        # (Down | Up) Default: [ 'Down' ]
347
348        Result => 'ARRAY' || 'COUNT',                                  # (optional)
349        # default: ARRAY, returns an array of change ids
350        # COUNT returns a scalar with the number of found changes
351
352        Limit => 100,                                                  # (optional)
353        # ignored if the result type is 'COUNT'
354    );
355
356Returns:
357
358Result: 'ARRAY'
359
360    @CustomerUserIDs = ( 1, 2, 3 );
361
362Result: 'COUNT'
363
364    $CustomerUserIDs = 10;
365
366=cut
367
368sub CustomerSearchDetail {
369    my ( $Self, %Param ) = @_;
370
371    # get all general search fields (without a restriction to a source)
372    my @AllSearchFields = $Self->CustomerUserSearchFields();
373
374    # generate a hash with the customer user sources which must be searched
375    my %SearchCustomerUserSources;
376
377    SOURCE:
378    for my $Count ( '', 1 .. 10 ) {
379        next SOURCE if !$Self->{"CustomerUser$Count"};
380
381        # get the search fields for the current source
382        my @SourceSearchFields = $Self->CustomerUserSearchFields(
383            Source => "CustomerUser$Count",
384        );
385        my %LookupSourceSearchFields = map { $_->{Name} => 1 } @SourceSearchFields;
386
387        # check if all search param exists in the search fields from the current source
388        SEARCHFIELD:
389        for my $SearchField (@AllSearchFields) {
390
391            next SEARCHFIELD if !$Param{ $SearchField->{Name} };
392
393            next SOURCE if !$LookupSourceSearchFields{ $SearchField->{Name} };
394        }
395        $SearchCustomerUserSources{"CustomerUser$Count"} = \@SourceSearchFields;
396    }
397
398    # set the default behaviour for the return type
399    $Param{Result} ||= 'ARRAY';
400
401    if ( $Param{Result} eq 'COUNT' ) {
402
403        my $IDsCount = 0;
404
405        SOURCE:
406        for my $Source ( sort keys %SearchCustomerUserSources ) {
407            next SOURCE if !$Self->{$Source};
408
409            my $SubIDsCount = $Self->{$Source}->CustomerSearchDetail(
410                %Param,
411                SearchFields => $SearchCustomerUserSources{$Source},
412            );
413
414            return if !defined $SubIDsCount;
415
416            $IDsCount += $SubIDsCount || 0;
417        }
418        return $IDsCount;
419    }
420    else {
421
422        my @IDs;
423
424        my $ResultCount = 0;
425
426        SOURCE:
427        for my $Source ( sort keys %SearchCustomerUserSources ) {
428            next SOURCE if !$Self->{$Source};
429
430            my $SubIDs = $Self->{$Source}->CustomerSearchDetail(
431                %Param,
432                SearchFields => $SearchCustomerUserSources{$Source},
433            );
434
435            return if !defined $SubIDs;
436
437            next SOURCE if !IsArrayRefWithData($SubIDs);
438
439            push @IDs, @{$SubIDs};
440
441            $ResultCount++;
442        }
443
444        # if we have more then one search results from diffrent sources, we need a resorting
445        # because of the merged single results
446        if ( $ResultCount > 1 ) {
447
448            my @UserDataList;
449
450            for my $ID (@IDs) {
451
452                my %UserData = $Self->CustomerUserDataGet(
453                    User => $ID,
454                );
455                push @UserDataList, \%UserData;
456            }
457
458            my $OrderBy = 'UserLogin';
459            if ( IsArrayRefWithData( $Param{OrderBy} ) ) {
460                $OrderBy = $Param{OrderBy}->[0];
461            }
462
463            if ( IsArrayRefWithData( $Param{OrderByDirection} ) && $Param{OrderByDirection}->[0] eq 'Up' ) {
464                @UserDataList = sort { lc( $a->{$OrderBy} ) cmp lc( $b->{$OrderBy} ) } @UserDataList;
465            }
466            else {
467                @UserDataList = sort { lc( $b->{$OrderBy} ) cmp lc( $a->{$OrderBy} ) } @UserDataList;
468            }
469
470            if ( $Param{Limit} && scalar @UserDataList > $Param{Limit} ) {
471                splice @UserDataList, $Param{Limit};
472            }
473
474            @IDs = map { $_->{UserLogin} } @UserDataList;
475        }
476
477        return \@IDs;
478    }
479}
480
481=head2 CustomerUserSearchFields()
482
483Get a list of the defined search fields (optional only the relevant fields for the given source).
484
485    my @SeachFields = $CustomerUserObject->CustomerUserSearchFields(
486        Source => 'CustomerUser', # optional, but important in the CustomerSearchDetail to get the right database fields
487    );
488
489Returns an array of hash references.
490
491    @SeachFields = (
492        {
493            Name          => 'UserEmail',
494            Label         => 'Email',
495            Type          => 'Input',
496            DatabaseField => 'mail',
497        },
498        {
499            Name           => 'UserCountry',
500            Label          => 'Country',
501            Type           => 'Selection',
502            SelectionsData => {
503                'Germany'        => 'Germany',
504                'United Kingdom' => 'United Kingdom',
505                'United States'  => 'United States',
506                ...
507            },
508            DatabaseField => 'country',
509        },
510        {
511            Name          => 'DynamicField_SkypeAccountName',
512            Label         => '',
513            Type          => 'DynamicField',
514            DatabaseField => 'SkypeAccountName',
515        },
516    );
517
518=cut
519
520sub CustomerUserSearchFields {
521    my ( $Self, %Param ) = @_;
522
523    # Get the search fields from all customer user maps (merge from all maps together).
524    my @SearchFields;
525
526    my %SearchFieldsExists;
527
528    SOURCE:
529    for my $Count ( '', 1 .. 10 ) {
530        next SOURCE if !$Self->{"CustomerUser$Count"};
531        next SOURCE if $Param{Source} && $Param{Source} ne "CustomerUser$Count";
532
533        ENTRY:
534        for my $Entry ( @{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} } ) {
535
536            my $SearchFieldName = $Entry->[0];
537
538            next ENTRY if $SearchFieldsExists{$SearchFieldName};
539            next ENTRY if $SearchFieldName =~ m{(Password|Pw)\d*$}smxi;
540
541            # Remeber the already collected search field name.
542            $SearchFieldsExists{$SearchFieldName} = 1;
543
544            my %FieldConfig = $Self->GetFieldConfig(
545                FieldName => $SearchFieldName,
546                Source    => $Param{Source},     # to get the right database field for the given source
547            );
548
549            next SEARCHFIELDNAME if !%FieldConfig;
550
551            my %SearchFieldData = (
552                %FieldConfig,
553                Name => $SearchFieldName,
554            );
555
556            my %SelectionsData = $Self->GetFieldSelections(
557                FieldName => $SearchFieldName,
558            );
559
560            if ( $SearchFieldData{StorageType} eq 'dynamic_field' ) {
561                $SearchFieldData{Type} = 'DynamicField';
562            }
563            elsif (%SelectionsData) {
564                $SearchFieldData{Type}           = 'Selection';
565                $SearchFieldData{SelectionsData} = \%SelectionsData;
566            }
567            else {
568                $SearchFieldData{Type} = 'Input';
569            }
570
571            push @SearchFields, \%SearchFieldData;
572        }
573    }
574
575    return @SearchFields;
576}
577
578=head2 GetFieldConfig()
579
580This function collect some field config information from the customer user map.
581
582    my %FieldConfig = $CustomerUserObject->GetFieldConfig(
583        FieldName => 'UserEmail',
584        Source    => 'CustomerUser', # optional
585    );
586
587Returns some field config information:
588
589    my %FieldConfig = (
590        Label         => 'Email',
591        DatabaseField => 'email',
592        StorageType   => 'var',
593    );
594
595=cut
596
597sub GetFieldConfig {
598    my ( $Self, %Param ) = @_;
599
600    if ( !$Param{FieldName} ) {
601        $Kernel::OM->Get('Kernel::System::Log')->Log(
602            Priority => 'error',
603            Message  => "Need FieldName!"
604        );
605        return;
606    }
607
608    SOURCE:
609    for my $Count ( '', 1 .. 10 ) {
610        next SOURCE if !$Self->{"CustomerUser$Count"};
611        next SOURCE if $Param{Source} && $Param{Source} ne "CustomerUser$Count";
612
613        # Search the right field and return some config information from the field.
614        ENTRY:
615        for my $Entry ( @{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} } ) {
616            next ENTRY if $Param{FieldName} ne $Entry->[0];
617
618            my %FieldConfig = (
619                Label         => $Entry->[1],
620                DatabaseField => $Entry->[2],
621                StorageType   => $Entry->[5],
622            );
623
624            return %FieldConfig;
625        }
626    }
627
628    return;
629}
630
631=head2 GetFieldSelections()
632
633This function collect the selections for the given field name, if the field has some selections.
634
635    my %SelectionsData = $CustomerUserObject->GetFieldSelections(
636        FieldName => 'UserTitle',
637    );
638
639Returns the selections for the given field name (merged from all sources) or a empty hash:
640
641    my %SelectionData = (
642        'Mr.'  => 'Mr.',
643        'Mrs.' => 'Mrs.',
644    );
645
646=cut
647
648sub GetFieldSelections {
649    my ( $Self, %Param ) = @_;
650
651    # check needed stuff
652    if ( !$Param{FieldName} ) {
653        $Kernel::OM->Get('Kernel::System::Log')->Log(
654            Priority => 'error',
655            Message  => "Need FieldName!"
656        );
657        return;
658    }
659
660    my %SelectionsData;
661
662    SOURCE:
663    for my $Count ( '', 1 .. 10 ) {
664        next SOURCE if !$Self->{"CustomerUser$Count"};
665        next SOURCE if !$Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Selections}->{ $Param{FieldName} };
666
667        %SelectionsData = (
668            %SelectionsData, %{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Selections}->{ $Param{FieldName} } }
669        );
670    }
671
672    # Make sure the encoding stamp is set.
673    for my $Key ( sort keys %SelectionsData ) {
674        $SelectionsData{$Key} = $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( $SelectionsData{$Key} );
675    }
676
677    # Default handling for field 'ValidID'.
678    if ( !%SelectionsData && $Param{FieldName} =~ /^ValidID/i ) {
679        %SelectionsData = $Kernel::OM->Get('Kernel::System::Valid')->ValidList();
680    }
681
682    return %SelectionsData;
683}
684
685=head2 CustomerIDList()
686
687return a list of with all known unique CustomerIDs of the registered customers users (no SearchTerm),
688or a filtered list where the CustomerIDs must contain a search term.
689
690    my @CustomerIDs = $CustomerUserObject->CustomerIDList(
691        SearchTerm  => 'somecustomer',    # optional
692        Valid       => 1,                 # optional
693    );
694
695=cut
696
697sub CustomerIDList {
698    my ( $Self, %Param ) = @_;
699
700    my @Data;
701    SOURCE:
702    for my $Count ( '', 1 .. 10 ) {
703
704        next SOURCE if !$Self->{"CustomerUser$Count"};
705
706        # get customer list result of backend and merge it
707        push @Data, $Self->{"CustomerUser$Count"}->CustomerIDList(%Param);
708    }
709
710    # make entries unique
711    my %Tmp;
712    @Tmp{@Data} = undef;
713    @Data = sort { lc $a cmp lc $b } keys %Tmp;
714
715    return @Data;
716}
717
718=head2 CustomerName()
719
720get customer user name
721
722    my $Name = $CustomerUserObject->CustomerName(
723        UserLogin => 'some-login',
724    );
725
726=cut
727
728sub CustomerName {
729    my ( $Self, %Param ) = @_;
730
731    SOURCE:
732    for my $Count ( '', 1 .. 10 ) {
733
734        next SOURCE if !$Self->{"CustomerUser$Count"};
735
736        # Get customer name and return it.
737        my $Name = $Self->{"CustomerUser$Count"}->CustomerName(%Param);
738        if ($Name) {
739            return $Name;
740        }
741    }
742    return;
743}
744
745=head2 CustomerIDs()
746
747get customer user customer ids
748
749    my @CustomerIDs = $CustomerUserObject->CustomerIDs(
750        User => 'some-login',
751    );
752
753=cut
754
755sub CustomerIDs {
756    my ( $Self, %Param ) = @_;
757
758    # check needed stuff
759    if ( !$Param{User} ) {
760        $Kernel::OM->Get('Kernel::System::Log')->Log(
761            Priority => 'error',
762            Message  => 'Need User!'
763        );
764        return;
765    }
766
767    # get customer ids (stop after first source with results)
768    my @CustomerIDs;
769    SOURCE:
770    for my $Count ( '', 1 .. 10 ) {
771
772        next SOURCE if !$Self->{"CustomerUser$Count"};
773
774        # get customer ids from source
775        my @SourceCustomerIDs = $Self->{"CustomerUser$Count"}->CustomerIDs(%Param);
776        next SOURCE if !@SourceCustomerIDs;
777
778        @CustomerIDs = @SourceCustomerIDs;
779        last SOURCE;
780    }
781
782    # create hash with existing customer ids
783    my %CustomerIDs = map { $_ => 1 } @CustomerIDs;
784
785    # get related customer ids
786    my @RelatedCustomerIDs = $Self->CustomerUserCustomerMemberList(
787        CustomerUserID => $Param{User},
788    );
789
790    # add related customer ids if not found in source
791    RELATEDCUSTOMERID:
792    for my $RelatedCustomerID (@RelatedCustomerIDs) {
793        next RELATEDCUSTOMERID if $CustomerIDs{$RelatedCustomerID};
794
795        push @CustomerIDs, $RelatedCustomerID;
796    }
797
798    # return customer ids
799    return @CustomerIDs;
800}
801
802=head2 CustomerUserDataGet()
803
804get user data (UserLogin, UserFirstname, UserLastname, UserEmail, ...)
805
806    my %User = $CustomerUserObject->CustomerUserDataGet(
807        User => 'franz',
808    );
809
810=cut
811
812sub CustomerUserDataGet {
813    my ( $Self, %Param ) = @_;
814
815    return if !$Param{User};
816
817    # fetch dynamic field configurations for CustomerUser.
818    my $DynamicFieldConfigs = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
819        ObjectType => 'CustomerUser',
820        Valid      => 1,
821    );
822
823    my %DynamicFieldLookup = map { $_->{Name} => $_ } @{$DynamicFieldConfigs};
824
825    # Get needed objects.
826    my $ConfigObject              = $Kernel::OM->Get('Kernel::Config');
827    my $CustomerCompanyObject     = $Kernel::OM->Get('Kernel::System::CustomerCompany');
828    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
829
830    SOURCE:
831    for my $Count ( '', 1 .. 10 ) {
832
833        next SOURCE if !$Self->{"CustomerUser$Count"};
834
835        my %Customer = $Self->{"CustomerUser$Count"}->CustomerUserDataGet(%Param);
836        next SOURCE if !%Customer;
837
838        # generate the full name and save it in the hash
839        my $UserFullname = $Self->CustomerName(%Customer);
840
841        # save the generated fullname in the hash.
842        $Customer{UserFullname} = $UserFullname;
843
844        # add preferences defaults
845        my $Config = $ConfigObject->Get('CustomerPreferencesGroups');
846        if ($Config) {
847            KEY:
848            for my $Key ( sort keys %{$Config} ) {
849
850                next KEY if !defined $Config->{$Key}->{DataSelected};
851                next KEY if defined $Customer{ $Config->{$Key}->{PrefKey} };
852
853                # set default data
854                $Customer{ $Config->{$Key}->{PrefKey} } = $Config->{$Key}->{DataSelected};
855            }
856        }
857
858        # check if customer company support is enabled and get company data
859        my %Company;
860        if (
861            $ConfigObject->Get("CustomerCompany")
862            && $ConfigObject->Get("CustomerUser$Count")->{CustomerCompanySupport}
863            )
864        {
865            %Company = $CustomerCompanyObject->CustomerCompanyGet(
866                CustomerID => $Customer{UserCustomerID},
867            );
868
869            $Company{CustomerCompanyValidID} = $Company{ValidID};
870        }
871
872        # fetch dynamic field values
873        if ( IsArrayRefWithData( $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} ) ) {
874            CUSTOMERUSERFIELD:
875            for my $CustomerUserField ( @{ $Self->{"CustomerUser$Count"}->{CustomerUserMap}->{Map} } ) {
876                next CUSTOMERUSERFIELD if $CustomerUserField->[5] ne 'dynamic_field';
877                next CUSTOMERUSERFIELD if !$DynamicFieldLookup{ $CustomerUserField->[2] };
878
879                my $Value = $DynamicFieldBackendObject->ValueGet(
880                    DynamicFieldConfig => $DynamicFieldLookup{ $CustomerUserField->[2] },
881                    ObjectName         => $Customer{UserID},
882                );
883
884                $Customer{ $CustomerUserField->[0] } = $Value;
885            }
886        }
887
888        # return customer data
889        return (
890            %Company,
891            %Customer,
892            Source        => "CustomerUser$Count",
893            Config        => $ConfigObject->Get("CustomerUser$Count"),
894            CompanyConfig => $ConfigObject->Get( $Company{Source} // 'CustomerCompany' ),
895        );
896    }
897
898    return;
899}
900
901=head2 CustomerUserAdd()
902
903to add new customer users
904
905    my $UserLogin = $CustomerUserObject->CustomerUserAdd(
906        Source         => 'CustomerUser', # CustomerUser source config
907        UserFirstname  => 'Huber',
908        UserLastname   => 'Manfred',
909        UserCustomerID => 'A124',
910        UserLogin      => 'mhuber',
911        UserPassword   => 'some-pass', # not required
912        UserEmail      => 'email@example.com',
913        ValidID        => 1,
914        UserID         => 123,
915    );
916
917=cut
918
919sub CustomerUserAdd {
920    my ( $Self, %Param ) = @_;
921
922    # check data source
923    if ( !$Param{Source} ) {
924        $Param{Source} = 'CustomerUser';
925    }
926
927    # check if user exists
928    if ( $Param{UserLogin} ) {
929        my %User = $Self->CustomerUserDataGet( User => $Param{UserLogin} );
930        if (%User) {
931            $Kernel::OM->Get('Kernel::System::Log')->Log(
932                Priority => 'error',
933                Message  => $Kernel::OM->Get('Kernel::Language')
934                    ->Translate( 'Customer user "%s" already exists.', $Param{UserLogin} ),
935            );
936            return;
937        }
938    }
939
940    # store customer user data
941    my $Result = $Self->{ $Param{Source} }->CustomerUserAdd(%Param);
942    return if !$Result;
943
944    # trigger event
945    $Self->EventHandler(
946        Event => 'CustomerUserAdd',
947        Data  => {
948            UserLogin => $Param{UserLogin},
949            NewData   => \%Param,
950        },
951        UserID => $Param{UserID},
952    );
953
954    return $Result;
955
956}
957
958=head2 CustomerUserUpdate()
959
960to update customer users
961
962    $CustomerUserObject->CustomerUserUpdate(
963        Source        => 'CustomerUser', # CustomerUser source config
964        ID            => 'mh'            # current user login
965        UserLogin     => 'mhuber',       # new user login
966        UserFirstname => 'Huber',
967        UserLastname  => 'Manfred',
968        UserPassword  => 'some-pass',    # not required
969        UserEmail     => 'email@example.com',
970        ValidID       => 1,
971        UserID        => 123,
972    );
973
974=cut
975
976sub CustomerUserUpdate {
977    my ( $Self, %Param ) = @_;
978
979    # check needed stuff
980    if ( !$Param{UserLogin} ) {
981        $Kernel::OM->Get('Kernel::System::Log')->Log(
982            Priority => 'error',
983            Message  => "Need UserLogin!"
984        );
985        return;
986    }
987
988    # check for UserLogin-renaming and if new UserLogin already exists...
989    if ( $Param{ID} && ( lc $Param{UserLogin} ne lc $Param{ID} ) ) {
990        my %User = $Self->CustomerUserDataGet( User => $Param{UserLogin} );
991        if (%User) {
992            $Kernel::OM->Get('Kernel::System::Log')->Log(
993                Priority => 'error',
994                Message  => $Kernel::OM->Get('Kernel::Language')
995                    ->Translate( 'Customer user "%s" already exists.', $Param{UserLogin} ),
996            );
997            return;
998        }
999    }
1000
1001    # check if user exists
1002    my %User = $Self->CustomerUserDataGet( User => $Param{ID} || $Param{UserLogin} );
1003    if ( !%User ) {
1004        $Kernel::OM->Get('Kernel::System::Log')->Log(
1005            Priority => 'error',
1006            Message  => "No such user '$Param{UserLogin}'!",
1007        );
1008        return;
1009    }
1010
1011    my $Result = $Self->{ $User{Source} }->CustomerUserUpdate(%Param);
1012    return if !$Result;
1013
1014    # trigger event
1015    $Self->EventHandler(
1016        Event => 'CustomerUserUpdate',
1017        Data  => {
1018            UserLogin => $Param{ID} || $Param{UserLogin},
1019            NewData   => \%Param,
1020            OldData   => \%User,
1021        },
1022        UserID => $Param{UserID},
1023    );
1024
1025    return $Result;
1026}
1027
1028=head2 SetPassword()
1029
1030to set customer users passwords
1031
1032    $CustomerUserObject->SetPassword(
1033        UserLogin => 'some-login',
1034        PW        => 'some-new-password'
1035    );
1036
1037=cut
1038
1039sub SetPassword {
1040    my ( $Self, %Param ) = @_;
1041
1042    # check needed stuff
1043    if ( !$Param{UserLogin} ) {
1044        $Kernel::OM->Get('Kernel::System::Log')->Log(
1045            Priority => 'error',
1046            Message  => 'User UserLogin!'
1047        );
1048        return;
1049    }
1050
1051    # check if user exists
1052    my %User = $Self->CustomerUserDataGet( User => $Param{UserLogin} );
1053    if ( !%User ) {
1054        $Kernel::OM->Get('Kernel::System::Log')->Log(
1055            Priority => 'error',
1056            Message  => "No such user '$Param{UserLogin}'!",
1057        );
1058        return;
1059    }
1060    return $Self->{ $User{Source} }->SetPassword(%Param);
1061}
1062
1063=head2 GenerateRandomPassword()
1064
1065generate a random password
1066
1067    my $Password = $CustomerUserObject->GenerateRandomPassword();
1068
1069    or
1070
1071    my $Password = $CustomerUserObject->GenerateRandomPassword(
1072        Size => 16,
1073    );
1074
1075=cut
1076
1077sub GenerateRandomPassword {
1078    my ( $Self, %Param ) = @_;
1079
1080    return $Self->{CustomerUser}->GenerateRandomPassword(%Param);
1081}
1082
1083=head2 SetPreferences()
1084
1085set customer user preferences
1086
1087    $CustomerUserObject->SetPreferences(
1088        Key    => 'UserComment',
1089        Value  => 'some comment',
1090        UserID => 'some-login',
1091    );
1092
1093=cut
1094
1095sub SetPreferences {
1096    my ( $Self, %Param ) = @_;
1097
1098    # check needed stuff
1099    if ( !$Param{UserID} ) {
1100        $Kernel::OM->Get('Kernel::System::Log')->Log(
1101            Priority => 'error',
1102            Message  => 'Need UserID!'
1103        );
1104        return;
1105    }
1106
1107    # Don't allow overwriting of native user data.
1108    my %Blacklisted = (
1109        UserID         => 1,
1110        UserLogin      => 1,
1111        UserPassword   => 1,
1112        UserFirstname  => 1,
1113        UserLastname   => 1,
1114        UserFullname   => 1,
1115        UserStreet     => 1,
1116        UserCity       => 1,
1117        UserZip        => 1,
1118        UserCountry    => 1,
1119        UserComment    => 1,
1120        UserCustomerID => 1,
1121        UserTitle      => 1,
1122        UserEmail      => 1,
1123        ChangeTime     => 1,
1124        ChangeBy       => 1,
1125        CreateTime     => 1,
1126        CreateBy       => 1,
1127        UserPhone      => 1,
1128        UserMobile     => 1,
1129        UserFax        => 1,
1130        UserMailString => 1,
1131        ValidID        => 1,
1132    );
1133
1134    return 0 if $Blacklisted{ $Param{Key} };
1135
1136    # check if user exists
1137    my %User = $Self->CustomerUserDataGet( User => $Param{UserID} );
1138    if ( !%User ) {
1139        $Kernel::OM->Get('Kernel::System::Log')->Log(
1140            Priority => 'error',
1141            Message  => "No such user '$Param{UserID}'!",
1142        );
1143        return;
1144    }
1145
1146    # call new api (2.4.8 and higher)
1147    if ( $Self->{ $User{Source} }->can('SetPreferences') ) {
1148        return $Self->{ $User{Source} }->SetPreferences(%Param);
1149    }
1150
1151    # call old api
1152    return $Self->{PreferencesObject}->SetPreferences(%Param);
1153}
1154
1155=head2 GetPreferences()
1156
1157get customer user preferences
1158
1159    my %Preferences = $CustomerUserObject->GetPreferences(
1160        UserID => 'some-login',
1161    );
1162
1163=cut
1164
1165sub GetPreferences {
1166    my ( $Self, %Param ) = @_;
1167
1168    # check needed stuff
1169    if ( !$Param{UserID} ) {
1170        $Kernel::OM->Get('Kernel::System::Log')->Log(
1171            Priority => 'error',
1172            Message  => 'Need UserID!'
1173        );
1174        return;
1175    }
1176
1177    # check if user exists
1178    my %User = $Self->CustomerUserDataGet( User => $Param{UserID} );
1179    if ( !%User ) {
1180        $Kernel::OM->Get('Kernel::System::Log')->Log(
1181            Priority => 'error',
1182            Message  => "No such user '$Param{UserID}'!",
1183        );
1184        return;
1185    }
1186
1187    # call new api (2.4.8 and higher)
1188    if ( $Self->{ $User{Source} }->can('GetPreferences') ) {
1189        return $Self->{ $User{Source} }->GetPreferences(%Param);
1190    }
1191
1192    # call old api
1193    return $Self->{PreferencesObject}->GetPreferences(%Param);
1194}
1195
1196=head2 SearchPreferences()
1197
1198search in user preferences
1199
1200    my %UserList = $CustomerUserObject->SearchPreferences(
1201        Key   => 'UserSomeKey',
1202        Value => 'SomeValue',   # optional, limit to a certain value/pattern
1203    );
1204
1205=cut
1206
1207sub SearchPreferences {
1208    my ( $Self, %Param ) = @_;
1209
1210    my %Data;
1211    SOURCE:
1212    for my $Count ( '', 1 .. 10 ) {
1213
1214        next SOURCE if !$Self->{"CustomerUser$Count"};
1215
1216        # get customer search result of backend and merge it
1217        # call new api (2.4.8 and higher)
1218        my %SubData;
1219        if ( $Self->{"CustomerUser$Count"}->can('SearchPreferences') ) {
1220            %SubData = $Self->{"CustomerUser$Count"}->SearchPreferences(%Param);
1221        }
1222
1223        # call old api
1224        else {
1225            %SubData = $Self->{PreferencesObject}->SearchPreferences(%Param);
1226        }
1227        %Data = ( %SubData, %Data );
1228    }
1229
1230    return %Data;
1231}
1232
1233=head2 TokenGenerate()
1234
1235generate a random token
1236
1237    my $Token = $CustomerUserObject->TokenGenerate(
1238        UserID => 123,
1239    );
1240
1241=cut
1242
1243sub TokenGenerate {
1244    my ( $Self, %Param ) = @_;
1245
1246    # check needed stuff
1247    if ( !$Param{UserID} ) {
1248        $Kernel::OM->Get('Kernel::System::Log')->Log(
1249            Priority => 'error',
1250            Message  => "Need UserID!"
1251        );
1252        return;
1253    }
1254
1255    my $Token = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
1256        Length => 14,
1257    );
1258
1259    # save token in preferences
1260    $Self->SetPreferences(
1261        Key    => 'UserToken',
1262        Value  => $Token,
1263        UserID => $Param{UserID},
1264    );
1265
1266    return $Token;
1267}
1268
1269=head2 TokenCheck()
1270
1271check password token
1272
1273    my $Valid = $CustomerUserObject>TokenCheck(
1274        Token  => $Token,
1275        UserID => 123,
1276    );
1277
1278=cut
1279
1280sub TokenCheck {
1281    my ( $Self, %Param ) = @_;
1282
1283    # check needed stuff
1284    if ( !$Param{Token} || !$Param{UserID} ) {
1285        $Kernel::OM->Get('Kernel::System::Log')->Log(
1286            Priority => 'error',
1287            Message  => "Need Token and UserID!"
1288        );
1289        return;
1290    }
1291
1292    # get preferences token
1293    my %Preferences = $Self->GetPreferences(
1294        UserID => $Param{UserID},
1295    );
1296
1297    # check requested vs. stored token
1298    return if !$Preferences{UserToken};
1299    return if $Preferences{UserToken} ne $Param{Token};
1300
1301    # reset password token
1302    $Self->SetPreferences(
1303        Key    => 'UserToken',
1304        Value  => '',
1305        UserID => $Param{UserID},
1306    );
1307
1308    return 1;
1309}
1310
1311=head2 CustomerUserCacheClear()
1312
1313clear cache of customer user data
1314
1315    $CustomerUserObject->CustomerUserCacheClear(
1316        UserLogin => 'mhuber',
1317    );
1318
1319=cut
1320
1321sub CustomerUserCacheClear {
1322    my ( $Self, %Param ) = @_;
1323
1324    SOURCE:
1325    for my $Count ( '', 1 .. 10 ) {
1326
1327        next SOURCE if !$Self->{"CustomerUser$Count"};
1328        $Self->{"CustomerUser$Count"}->_CustomerUserCacheClear(
1329            UserLogin => $Param{UserLogin},
1330        );
1331    }
1332
1333    return 1;
1334}
1335
1336=head2 CustomerUserCustomerMemberAdd()
1337
1338to add a customer user to a customer
1339
1340    my $Success = $CustomerUserObject->CustomerUserCustomerMemberAdd(
1341        CustomerUserID => 123,
1342        CustomerID     => 123,
1343        Active         => 1,        # optional
1344        UserID         => 123,
1345    );
1346
1347=cut
1348
1349sub CustomerUserCustomerMemberAdd {
1350    my ( $Self, %Param ) = @_;
1351
1352    # check needed stuff
1353    for my $Argument (qw(CustomerUserID CustomerID UserID)) {
1354        if ( !$Param{$Argument} ) {
1355            $Kernel::OM->Get('Kernel::System::Log')->Log(
1356                Priority => 'error',
1357                Message  => "Need $Argument!",
1358            );
1359            return;
1360        }
1361    }
1362
1363    # delete affected caches
1364    my $CacheKey = 'Cache::CustomerUserCustomerMemberList::';
1365    $Kernel::OM->Get('Kernel::System::Cache')->Delete(
1366        Type => $Self->{CacheType},
1367        Key  => $CacheKey . 'CustomerUserID::' . $Param{CustomerUserID},
1368    );
1369    $Kernel::OM->Get('Kernel::System::Cache')->Delete(
1370        Type => $Self->{CacheType},
1371        Key  => $CacheKey . 'CustomerID::' . $Param{CustomerID},
1372    );
1373
1374    # get database object
1375    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1376
1377    # delete existing relation
1378    return if !$DBObject->Do(
1379        SQL => 'DELETE FROM customer_user_customer
1380            WHERE user_id = ?
1381            AND customer_id = ?',
1382        Bind => [ \$Param{CustomerUserID}, \$Param{CustomerID} ],
1383    );
1384
1385    # return if relation is not active
1386    return 1 if !$Param{Active};
1387
1388    # insert new relation
1389    return if !$DBObject->Do(
1390        SQL => '
1391            INSERT INTO customer_user_customer (user_id, customer_id, create_time, create_by,
1392            change_time, change_by)
1393            VALUES (?, ?, current_timestamp, ?, current_timestamp, ?)',
1394        Bind => [ \$Param{CustomerUserID}, \$Param{CustomerID}, \$Param{UserID}, \$Param{UserID}, ],
1395    );
1396
1397    return 1;
1398}
1399
1400=head2 CustomerUserCustomerMemberList()
1401
1402get related customer IDs of a customer user
1403
1404    my @CustomerIDs = $CustomerUserObject->CustomerUserCustomerMemberList(
1405        CustomerUserID => 123,
1406    );
1407
1408Returns:
1409    @CustomerIDs = (
1410        '123',
1411        '456',
1412    );
1413
1414get related customer users of a customer ID
1415
1416    my @CustomerUsers = $CustomerUserObject->CustomerUserCustomerMemberList(
1417        CustomerID => 123,
1418    );
1419
1420Returns:
1421    @CustomerUsers = (
1422        '123',
1423        '456',
1424    );
1425
1426=cut
1427
1428sub CustomerUserCustomerMemberList {
1429    my ( $Self, %Param ) = @_;
1430
1431    # check needed stuff
1432    if ( !$Param{CustomerUserID} && !$Param{CustomerID} ) {
1433        $Kernel::OM->Get('Kernel::System::Log')->Log(
1434            Priority => 'error',
1435            Message  => 'Got no CustomerUserID or CustomerID!',
1436        );
1437        return;
1438    }
1439
1440    # get needed objects
1441    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1442    my $CacheKey = 'Cache::CustomerUserCustomerMemberList::';
1443
1444    if ( $Param{CustomerUserID} ) {
1445
1446        # check if this result is present (in cache)
1447        $CacheKey .= 'CustomerUserID::' . $Param{CustomerUserID};
1448        my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
1449            Type => $Self->{CacheType},
1450            Key  => $CacheKey,
1451        );
1452        return @{$Cache} if $Cache;
1453
1454        # get customer ids
1455        return if !$DBObject->Prepare(
1456            SQL =>
1457                'SELECT customer_id
1458                FROM customer_user_customer
1459                WHERE user_id = ?
1460                ORDER BY customer_id',
1461            Bind => [ \$Param{CustomerUserID}, ],
1462        );
1463
1464        # fetch the result
1465        my @CustomerIDs;
1466        while ( my @Row = $DBObject->FetchrowArray() ) {
1467            push @CustomerIDs, $Row[0];
1468        }
1469
1470        # cache the result
1471        $Kernel::OM->Get('Kernel::System::Cache')->Set(
1472            Type  => $Self->{CacheType},
1473            TTL   => $Self->{CacheTTL},
1474            Key   => $CacheKey,
1475            Value => \@CustomerIDs,
1476
1477        );
1478
1479        return @CustomerIDs;
1480    }
1481    else {
1482
1483        # check if this result is present (in cache)
1484        $CacheKey .= 'CustomerID::' . $Param{CustomerID};
1485        my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
1486            Type => $Self->{CacheType},
1487            Key  => $CacheKey,
1488        );
1489        return @{$Cache} if $Cache;
1490
1491        # get customer users
1492        return if !$DBObject->Prepare(
1493            SQL =>
1494                'SELECT user_id
1495                FROM customer_user_customer WHERE
1496                customer_id = ?
1497                ORDER BY user_id',
1498            Bind => [ \$Param{CustomerID}, ],
1499        );
1500
1501        # fetch the result
1502        my @CustomerUserIDs;
1503        while ( my @Row = $DBObject->FetchrowArray() ) {
1504            push @CustomerUserIDs, $Row[0];
1505        }
1506
1507        # cache the result
1508        $Kernel::OM->Get('Kernel::System::Cache')->Set(
1509            Type  => $Self->{CacheType},
1510            TTL   => $Self->{CacheTTL},
1511            Key   => $CacheKey,
1512            Value => \@CustomerUserIDs,
1513        );
1514
1515        return @CustomerUserIDs;
1516    }
1517}
1518
1519sub DESTROY {
1520    my $Self = shift;
1521
1522    # execute all transaction events
1523    $Self->EventHandlerTransaction();
1524
1525    return 1;
1526}
1527
15281;
1529
1530=head1 TERMS AND CONDITIONS
1531
1532This software is part of the OTRS project (L<https://otrs.org/>).
1533
1534This software comes with ABSOLUTELY NO WARRANTY. For details, see
1535the enclosed file COPYING for license information (GPL). If you
1536did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
1537
1538=cut
1539