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::ProcessManagement::Transition;
10
11use strict;
12use warnings;
13
14use Kernel::System::VariableCheck qw(:all);
15
16our @ObjectDependencies = (
17    'Kernel::Config',
18    'Kernel::System::Log',
19    'Kernel::System::Main',
20);
21
22=head1 NAME
23
24Kernel::System::ProcessManagement::Transition - Transition lib
25
26=head1 DESCRIPTION
27
28All Process Management Transition functions.
29
30=head1 PUBLIC INTERFACE
31
32=head2 new()
33
34Don't use the constructor directly, use the ObjectManager instead:
35
36    my $TransitionObject = $Kernel::OM->Get('Kernel::System::ProcessManagement::Transition');
37
38=cut
39
40sub new {
41    my ( $Type, %Param ) = @_;
42
43    # allocate new hash for object
44    my $Self = {};
45    bless( $Self, $Type );
46
47    # get config object
48    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
49
50    # get the debug parameters
51    $Self->{TransitionDebug} = $ConfigObject->Get('ProcessManagement::Transition::Debug::Enabled') || 0;
52    $Self->{TransitionDebugLogPriority}
53        = $ConfigObject->Get('ProcessManagement::Transition::Debug::LogPriority') || 'debug';
54
55    my $TransitionDebugConfigFilters = $ConfigObject->Get('ProcessManagement::Transition::Debug::Filter') || {};
56
57    for my $FilterName ( sort keys %{$TransitionDebugConfigFilters} ) {
58
59        my %Filter = %{ $TransitionDebugConfigFilters->{$FilterName} };
60
61        for my $FilterItem ( sort keys %Filter ) {
62            $Self->{TransitionDebugFilters}->{$FilterItem} = $Filter{$FilterItem};
63        }
64    }
65
66    return $Self;
67}
68
69=head2 TransitionGet()
70
71    Get Transition info
72
73    my $Transition = $TransitionObject->TransitionGet(
74        TransitionEntityID => 'T1',
75    );
76
77    Returns:
78
79    $Transition = {
80        Name       => 'Transition 1',
81        CreateTime => '08-02-2012 13:37:00',
82        ChangeBy   => '2',
83        ChangeTime => '09-02-2012 13:37:00',
84        CreateBy   => '3',
85        Condition  => {
86            Type   => 'and',
87            Cond1  => {
88                Type   => 'and',
89                Fields => {
90                    DynamicField_Make    => [ '2' ],
91                    DynamicField_VWModel => {
92                        Type  => 'String',
93                        Match => 'Golf',
94                    },
95                    DynamicField_A => {
96                        Type  => 'Hash',
97                        Match => {
98                            Value  => 1,
99                        },
100                    },
101                    DynamicField_B => {
102                        Type  => 'Regexp',
103                        Match => qr{ [\n\r\f] }xms
104                    },
105                    DynamicField_C => {
106                        Type  => 'Module',
107                        Match =>
108                            'Kernel::System::ProcessManagement::TransitionValidation::MyModule',
109                    },
110                    Queue =>  {
111                        Type  => 'Array',
112                        Match => [ 'Raw' ],
113                    },
114                    # ...
115                }
116            }
117            # ...
118        },
119    };
120
121=cut
122
123sub TransitionGet {
124    my ( $Self, %Param ) = @_;
125
126    for my $Needed (qw(TransitionEntityID)) {
127        if ( !defined $Param{$Needed} ) {
128            $Kernel::OM->Get('Kernel::System::Log')->Log(
129                Priority => 'error',
130                Message  => "Need $Needed!",
131            );
132            return;
133        }
134    }
135
136    my $Transition = $Kernel::OM->Get('Kernel::Config')->Get('Process::Transition');
137
138    if ( !IsHashRefWithData($Transition) ) {
139        $Kernel::OM->Get('Kernel::System::Log')->Log(
140            Priority => 'error',
141            Message  => 'Need Transition config!',
142        );
143        return;
144    }
145
146    if ( !IsHashRefWithData( $Transition->{ $Param{TransitionEntityID} } ) ) {
147        $Kernel::OM->Get('Kernel::System::Log')->Log(
148            Priority => 'error',
149            Message  => "No data for Transition '$Param{TransitionEntityID}' found!",
150        );
151        return;
152    }
153
154    return $Transition->{ $Param{TransitionEntityID} };
155}
156
157=head2 TransitionCheck()
158
159    Checks if one or more Transition Conditions are true
160
161    my $TransitionCheck = $TransitionObject->TransitionCheck(
162        TransitionEntityID => 'T1',
163        or
164        TransitionEntityID => ['T1', 'T2', 'T3'],
165        Data       => {
166            Queue         => 'Raw',
167            DynamicField1 => 'Value',
168            Subject       => 'Testsubject',
169            ...
170        },
171    );
172
173    If called on a single TransitionEntityID
174    Returns:
175    $Checked = 1; # 0
176
177    If called on an array of TransitionEntityIDs
178    Returns:
179    $Checked = 'T1' # 0 if no Transition was true
180
181=cut
182
183sub TransitionCheck {
184    my ( $Self, %Param ) = @_;
185
186    # Check if we have TransitionEntityID and Data.
187    for my $Needed (qw(TransitionEntityID Data)) {
188        if ( !defined $Param{$Needed} ) {
189            $Kernel::OM->Get('Kernel::System::Log')->Log(
190                Priority => 'error',
191                Message  => "Need $Needed!",
192            );
193            return;
194        }
195    }
196
197    # Check if TransitionEntityID is not empty (either Array or String with length).
198    if ( !length $Param{TransitionEntityID} ) {
199        $Kernel::OM->Get('Kernel::System::Log')->Log(
200            Priority => 'error',
201            Message  => "Need TransitionEntityID or TransitionEntityID array!",
202        );
203        return;
204    }
205
206    # Check if debug filters apply (ticket).
207    if ( $Self->{TransitionDebug} ) {
208
209        DEBUGFILTER:
210        for my $DebugFilter ( sort keys %{ $Self->{TransitionDebugFilters} } ) {
211            next DEBUGFILTER if $DebugFilter eq 'TransitionEntityID';
212            next DEBUGFILTER if !$Self->{TransitionDebugFilters}->{$DebugFilter};
213            next DEBUGFILTER if ref $Param{Data} ne 'HASH';
214
215            if ( $DebugFilter =~ m{<OTRS_TICKET_([^>]+)>}msx ) {
216                my $TicketParam = $1;
217
218                if (
219                    defined $Param{Data}->{$TicketParam}
220                    && $Param{Data}->{$TicketParam}
221                    )
222                {
223                    if ( ref $Param{Data}->{$TicketParam} eq 'ARRAY' ) {
224                        for my $Item ( @{ $Param{Data}->{$TicketParam} } ) {
225
226                            # If matches for one item go to next filter (debug keeps active).
227                            if ( $Self->{TransitionDebugFilters}->{$DebugFilter} eq $Item ) {
228                                next DEBUGFILTER;
229                            }
230                        }
231
232                        # If no matches then deactivate debug.
233                        $Self->{TransitionDebug} = 0;
234                        last DEBUGFILTER;
235                    }
236
237                    elsif (
238                        $Self->{TransitionDebugFilters}->{$DebugFilter} ne
239                        $Param{Data}->{$TicketParam}
240                        )
241                    {
242                        $Self->{TransitionDebug} = 0;
243                        last DEBUGFILTER;
244                    }
245
246                    elsif ( !defined $Param{Data}->{$TicketParam} ) {
247                        $Self->{TransitionDebug} = 0;
248                        last DEBUGFILTER;
249                    }
250                }
251            }
252        }
253    }
254
255    # If we got just a string, make $Param{TransitionEntityID} an Array with the string on position
256    #   0 to facilitate handling
257    if ( !IsArrayRefWithData( $Param{TransitionEntityID} ) ) {
258        $Param{TransitionEntityID} = [ $Param{TransitionEntityID} ];
259    }
260
261    # Check if we have Data to check against transitions conditions.
262    if ( !IsHashRefWithData( $Param{Data} ) ) {
263        $Kernel::OM->Get('Kernel::System::Log')->Log(
264            Priority => 'error',
265            Message  => "Data has no values!",
266        );
267        return;
268    }
269
270    # Get all transitions.
271    my $Transitions = $Kernel::OM->Get('Kernel::Config')->Get('Process::Transition');
272
273    # Check if there are Transitions.
274    if ( !IsHashRefWithData($Transitions) ) {
275        $Kernel::OM->Get('Kernel::System::Log')->Log(
276            Priority => 'error',
277            Message  => 'Need transition config!',
278        );
279        return;
280    }
281
282    $Self->{TransitionDebugOrig} = $Self->{TransitionDebug};
283
284    # Loop through all submitted TransitionEntityID's.
285    TRANSITIONENTITYID:
286    for my $TransitionEntityID ( @{ $Param{TransitionEntityID} } ) {
287
288        $Self->{TransitionDebug} = $Self->{TransitionDebugOrig};
289
290        # Check if debug filters apply (Transition) (only if TransitionDebug is active).
291        if (
292            $Self->{TransitionDebug}
293            && defined $Self->{TransitionDebugFilters}->{'TransitionEntityID'}
294            && $Self->{TransitionDebugFilters}->{'TransitionEntityID'} ne $TransitionEntityID
295            )
296        {
297            $Self->{TransitionDebug} = 0;
298        }
299
300        # Check if the submitted TransitionEntityID has a config.
301        if ( !IsHashRefWithData( $Transitions->{$TransitionEntityID} ) ) {
302            $Kernel::OM->Get('Kernel::System::Log')->Log(
303                Priority => 'error',
304                Message  => "No config data for transition $TransitionEntityID found!",
305            );
306            return;
307        }
308
309        # Check if we have TransitionConditions.
310        if ( !IsHashRefWithData( $Transitions->{$TransitionEntityID}->{Condition} ) ) {
311            $Kernel::OM->Get('Kernel::System::Log')->Log(
312                Priority => 'error',
313                Message  => "No conditions for transition $TransitionEntityID found!",
314            );
315            return;
316        }
317
318        my $ConditionLinking = $Transitions->{$TransitionEntityID}->{ConditionLinking} || '';
319
320        # If we don't have a ConditionLinking set it to 'and' by default compatibility with OTRS 3.3.x
321        if ( !$ConditionLinking ) {
322            $ConditionLinking = $Transitions->{$TransitionEntityID}->{Condition}->{Type} || 'and';
323        }
324        if (
325            !$Transitions->{$TransitionEntityID}->{Condition}->{ConditionLinking}
326            && !$Transitions->{$TransitionEntityID}->{Condition}->{Type}
327            )
328        {
329
330            $Self->DebugLog(
331                MessageType => 'Custom',
332                Message =>
333                    "Transition:'$Transitions->{$TransitionEntityID}->{Name}' No Condition Linking"
334                    . " as Condition->Type or Condition->ConditionLinking was found, using 'and' as"
335                    . " default!",
336            );
337        }
338
339        # If there is something else than 'and', 'or', 'xor' log defect Transition Config.
340        if (
341            $ConditionLinking ne 'and'
342            && $ConditionLinking ne 'or'
343            && $ConditionLinking ne 'xor'
344            )
345        {
346            $Kernel::OM->Get('Kernel::System::Log')->Log(
347                Priority => 'error',
348                Message  => "Invalid Condition->Type in $TransitionEntityID!",
349            );
350            return;
351        }
352        my ( $ConditionSuccess, $ConditionFail ) = ( 0, 0 );
353
354        CONDITIONNAME:
355        for my $ConditionName (
356            sort { $a cmp $b }
357            keys %{ $Transitions->{$TransitionEntityID}->{Condition} }
358            )
359        {
360
361            next CONDITIONNAME if $ConditionName eq 'Type' || $ConditionName eq 'ConditionLinking';
362
363            # Get the condition.
364            my $ActualCondition = $Transitions->{$TransitionEntityID}->{Condition}->{$ConditionName};
365
366            # Check if we have Fields in our Condition.
367            if ( !IsHashRefWithData( $ActualCondition->{Fields} ) )
368            {
369                $Kernel::OM->Get('Kernel::System::Log')->Log(
370                    Priority => 'error',
371                    Message  => "No Fields in Transition $TransitionEntityID->Condition->$ConditionName"
372                        . " found!",
373                );
374                return;
375            }
376
377            # If we don't have a Condition->$ConditionName->Type, set it to 'and' by default.
378            my $CondType = $ActualCondition->{Type} || 'and';
379            if ( !$ActualCondition->{Type} ) {
380                $Self->DebugLog(
381                    MessageType => 'Custom',
382                    Message =>
383                        "Transition:'$Transitions->{$TransitionEntityID}->{Name}' Condition:'$ConditionName'"
384                        . " No Condition Type found, using 'and' as default",
385                );
386            }
387
388            # If there is something else than 'and', 'or', 'xor' log defect Transition Config.
389            if ( $CondType ne 'and' && $CondType ne 'or' && $CondType ne 'xor' ) {
390                $Kernel::OM->Get('Kernel::System::Log')->Log(
391                    Priority => 'error',
392                    Message  => "Invalid Condition->$ConditionName->Type in $TransitionEntityID!",
393                );
394                return;
395            }
396
397            my ( $FieldSuccess, $FieldFail ) = ( 0, 0 );
398
399            FIELDLNAME:
400            for my $FieldName ( sort keys %{ $ActualCondition->{Fields} } ) {
401
402                # If we have just a String transform it into string check condition.
403                if ( ref $ActualCondition->{Fields}->{$FieldName} eq '' ) {
404                    $ActualCondition->{Fields}->{$FieldName} = {
405                        Type  => 'String',
406                        Match => $ActualCondition->{Fields}->{$FieldName},
407                    };
408                }
409
410                # If we have an Array ref in "Fields" we deal with just values
411                #   -> transform it into a { Type => 'Array', Match => [1,2,3,4] } structure
412                #   to unify testing later on.
413                if ( ref $ActualCondition->{Fields}->{$FieldName} eq 'ARRAY' ) {
414                    $ActualCondition->{Fields}->{$FieldName} = {
415                        Type  => 'Array',
416                        Match => $ActualCondition->{Fields}->{$FieldName},
417                    };
418                }
419
420                # If we don't have a Condition->$ConditionName->Fields->Field->Type set it to
421                #   'String' by default.
422                my $FieldType = $ActualCondition->{Fields}->{$FieldName}->{Type} || 'String';
423                if ( !$ActualCondition->{Fields}->{$FieldName}->{Type} ) {
424                    $Self->DebugLog(
425                        MessageType => 'Custom',
426                        Message =>
427                            "Transition:'$Transitions->{$TransitionEntityID}->{Name}'"
428                            . " Condition:'$ConditionName' Field:'$FieldName'"
429                            . " No Field Type found, using 'String' as default",
430                    );
431                }
432
433                # If there is something else than 'String', 'Regexp', 'Hash', 'Array', 'Module'
434                #   log defect Transition Config.
435                if (
436                    $FieldType ne 'String'
437                    && $FieldType ne 'Hash'
438                    && $FieldType ne 'Array'
439                    && $FieldType ne 'Regexp'
440                    && $FieldType ne 'Module'
441                    )
442                {
443                    $Kernel::OM->Get('Kernel::System::Log')->Log(
444                        Priority => 'error',
445                        Message  => "Invalid Condition->Type in $TransitionEntityID!",
446                    );
447                    return;
448                }
449
450                if ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'String' ) {
451
452                    # if our Check contains anything else than a string we can't check
453                    #   Special Condition: if Match contains '0' we can check.
454                    if (
455                        (
456                            !$ActualCondition->{Fields}->{$FieldName}->{Match}
457                            && $ActualCondition->{Fields}->{$FieldName}->{Match} ne '0'
458                        )
459                        || ref $ActualCondition->{Fields}->{$FieldName}->{Match}
460                        )
461                    {
462                        $Kernel::OM->Get('Kernel::System::Log')->Log(
463                            Priority => 'error',
464                            Message =>
465                                "$TransitionEntityID->Condition->$ConditionName->Fields->$FieldName Match must"
466                                . " be a String if Type is set to String!",
467                        );
468                        return;
469                    }
470
471                    my $Match;
472                    my $MatchValue;
473
474                    # Make sure there is data to compare.
475                    if (
476                        defined $Param{Data}->{$FieldName}
477                        && defined $ActualCondition->{Fields}->{$FieldName}->{Match}
478                        )
479                    {
480
481                        # Check if field data is a string and compare directly.
482                        if (
483                            ref $Param{Data}->{$FieldName} eq ''
484                            && $ActualCondition->{Fields}->{$FieldName}->{Match} eq $Param{Data}->{$FieldName}
485                            )
486                        {
487                            $Match      = 1;
488                            $MatchValue = $Param{Data}->{$FieldName};
489                        }
490
491                        # Otherwise check if field data is and array and compare each element until
492                        #   one match.
493                        elsif ( ref $Param{Data}->{$FieldName} eq 'ARRAY' ) {
494
495                            ITEM:
496                            for my $Item ( @{ $Param{Data}->{$FieldName} } ) {
497                                if ( $ActualCondition->{Fields}->{$FieldName}->{Match} eq $Item ) {
498                                    $Match      = 1;
499                                    $MatchValue = "Item: [$Item]";
500                                    last ITEM;
501                                }
502                            }
503                        }
504                    }
505                    if ($Match) {
506                        $FieldSuccess++;
507
508                        $Self->DebugLog(
509                            MessageType    => 'Match',
510                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
511                            ConditionName  => $ConditionName,
512                            FieldName      => $FieldName,
513                            MatchType      => 'String',
514                            MatchValue     => $MatchValue,
515                            MatchCondition => $ActualCondition->{Fields}->{$FieldName}->{Match}
516                        );
517
518                        # Successful check if we just need one matching Condition to make this
519                        #   Transition valid.
520                        if ( $ConditionLinking eq 'or' && $CondType eq 'or' ) {
521
522                            $Self->DebugLog(
523                                MessageType      => 'Success',
524                                TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
525                                ConditionName    => $ConditionName,
526                                ConditionType    => $CondType,
527                                ConditionLinking => $ConditionLinking,
528                            );
529
530                            return $TransitionEntityID;
531                        }
532
533                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
534                    }
535                    else {
536                        $FieldFail++;
537
538                        my $UnmatchedValue = $Param{Data}->{$FieldName};
539                        if ( ref $Param{Data}->{$FieldName} eq 'ARRAY' ) {
540                            $UnmatchedValue = 'Any of [' . join( ', ', @{ $Param{Data}->{$FieldName} } ) . ']';
541                        }
542
543                        $Self->DebugLog(
544                            MessageType    => 'NoMatch',
545                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
546                            ConditionName  => $ConditionName,
547                            FieldName      => $FieldName,
548                            MatchType      => 'String',
549                            MatchValue     => $UnmatchedValue,
550                            MatchCondition => $ActualCondition->{Fields}->{$FieldName}->{Match}
551                        );
552
553                        # Failed check if we have all 'and' conditions.
554                        next TRANSITIONENTITYID if $ConditionLinking eq 'and' && $CondType eq 'and';
555
556                        # Try next Condition if all Condition Fields have to be true.
557                        next CONDITIONNAME if $CondType eq 'and';
558                    }
559                    next FIELDLNAME;
560                }
561                elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Array' ) {
562
563                  # 1. go through each Condition->$ConditionName->Fields->$Field->Value (map).
564                  # 2. assign the value to $CheckValue.
565                  # 3. grep through $Data->{$FieldName} to find the "toCheck" value inside the Data->{$FieldName} Array.
566                  # 4. Assign all found Values to @CheckResults.
567                    my $CheckValue;
568                    my @CheckResults = map {
569                        $CheckValue = $_;
570                        grep { $CheckValue eq $_ } @{ $Param{Data}->{$FieldName} }
571                        }
572                        @{ $ActualCondition->{Fields}->{$FieldName}->{Match} };
573
574                    # If the found amount is the same as the "toCheck" amount we succeeded.
575                    if (
576                        scalar @CheckResults
577                        == scalar @{ $ActualCondition->{Fields}->{$FieldName}->{Match} }
578                        )
579                    {
580                        $FieldSuccess++;
581
582                        $Self->DebugLog(
583                            MessageType    => 'Match',
584                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
585                            ConditionName  => $ConditionName,
586                            FieldName      => $FieldName,
587                            MatchType      => 'Array',
588                        );
589
590                        # Successful check if we just need one matching Condition to make this Transition valid.
591                        if ( $ConditionLinking eq 'or' && $CondType eq 'or' ) {
592
593                            $Self->DebugLog(
594                                MessageType      => 'Success',
595                                TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
596                                ConditionName    => $ConditionName,
597                                ConditionType    => $CondType,
598                                ConditionLinking => $ConditionLinking,
599
600                            );
601
602                            return $TransitionEntityID;
603                        }
604
605                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
606                    }
607                    else {
608                        $FieldFail++;
609
610                        $Self->DebugLog(
611                            MessageType    => 'NoMatch',
612                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
613                            ConditionName  => $ConditionName,
614                            FieldName      => $FieldName,
615                            MatchType      => 'Array',
616                        );
617
618                        # Failed check if we have all 'and' conditions.
619                        next TRANSITIONENTITYID if $ConditionLinking eq 'and' && $CondType eq 'and';
620
621                        # Try next Condition if all Condition Fields have to be true.
622                        next CONDITIONNAME if $CondType eq 'and';
623                    }
624                    next FIELDLNAME;
625                }
626                elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Hash' ) {
627
628                    # If our Check doesn't contain a hash.
629                    if ( ref $ActualCondition->{Fields}->{$FieldName}->{Match} ne 'HASH' ) {
630                        $Kernel::OM->Get('Kernel::System::Log')->Log(
631                            Priority => 'error',
632                            Message =>
633                                "$TransitionEntityID->Condition->$ConditionName->Fields->$FieldName Match must"
634                                . " be a Hash!",
635                        );
636                        return;
637                    }
638
639                    # If we have no data or Data isn't a hash, test failed.
640                    if (
641                        !$Param{Data}->{$FieldName}
642                        || ref $Param{Data}->{$FieldName} ne 'HASH'
643                        )
644                    {
645                        $FieldFail++;
646                        next FIELDLNAME;
647                    }
648
649                    # Find all Data Hash values that equal to the Condition Match Values.
650                    my @CheckResults = grep {
651                        $Param{Data}->{$FieldName}->{$_} eq
652                            $ActualCondition->{Fields}->{$FieldName}->{Match}->{$_}
653                    } keys %{ $ActualCondition->{Fields}->{$FieldName}->{Match} };
654
655                    # If the amount of Results equals the amount of Keys in our hash this part matched.
656                    if (
657                        scalar @CheckResults
658                        == scalar keys %{ $ActualCondition->{Fields}->{$FieldName}->{Match} }
659                        )
660                    {
661
662                        $FieldSuccess++;
663
664                        $Self->DebugLog(
665                            MessageType    => 'Match',
666                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
667                            ConditionName  => $ConditionName,
668                            FieldName      => $FieldName,
669                            MatchType      => 'Hash',
670                        );
671
672                        # Successful check if we just need one matching Condition to make this Transition valid.
673                        if ( $ConditionLinking eq 'or' && $CondType eq 'or' ) {
674
675                            $Self->DebugLog(
676                                MessageType      => 'Success',
677                                TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
678                                ConditionName    => $ConditionName,
679                                ConditionType    => $CondType,
680                                ConditionLinking => $ConditionLinking,
681                            );
682
683                            return $TransitionEntityID;
684                        }
685
686                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
687
688                    }
689                    else {
690                        $FieldFail++;
691
692                        $Self->DebugLog(
693                            MessageType    => 'NoMatch',
694                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
695                            ConditionName  => $ConditionName,
696                            FieldName      => $FieldName,
697                            MatchType      => 'Hash',
698                        );
699
700                        # Failed check if we have all 'and' conditions.
701                        next TRANSITIONENTITYID if $ConditionLinking eq 'and' && $CondType eq 'and';
702
703                        # Try next Condition if all Condition Fields have to be true.
704                        next CONDITIONNAME if $CondType eq 'and';
705                    }
706                    next FIELDLNAME;
707                }
708                elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Regexp' )
709                {
710
711                    # If our Check contains anything else then a string we can't check.
712                    if (
713                        !$ActualCondition->{Fields}->{$FieldName}->{Match}
714                        ||
715                        (
716                            ref $ActualCondition->{Fields}->{$FieldName}->{Match} ne 'Regexp'
717                            && ref $ActualCondition->{Fields}->{$FieldName}->{Match} ne ''
718                        )
719                        )
720                    {
721                        $Kernel::OM->Get('Kernel::System::Log')->Log(
722                            Priority => 'error',
723                            Message =>
724                                "$TransitionEntityID->Condition->$ConditionName->Fields->$FieldName Match must"
725                                . " be a Regular expression if Type is set to Regexp!",
726                        );
727                        return;
728                    }
729
730                    # Precompile Regexp if is a string.
731                    if ( ref $ActualCondition->{Fields}->{$FieldName}->{Match} eq '' ) {
732                        my $Match = $ActualCondition->{Fields}->{$FieldName}->{Match};
733
734                        eval {
735                            $ActualCondition->{Fields}->{$FieldName}->{Match} = qr{$Match};
736                        };
737                        if ($@) {
738                            $Kernel::OM->Get('Kernel::System::Log')->Log(
739                                Priority => 'error',
740                                Message  => $@,
741                            );
742                            return;
743                        }
744                    }
745
746                    my $Match;
747                    my $MatchValue;
748
749                    # Make sure there is data to compare.
750                    if ( $Param{Data}->{$FieldName} ) {
751
752                        # Check if field data is a string and compare directly.
753                        if (
754                            ref $Param{Data}->{$FieldName} eq ''
755                            && $Param{Data}->{$FieldName} =~ $ActualCondition->{Fields}->{$FieldName}->{Match}
756                            )
757                        {
758                            $Match      = 1;
759                            $MatchValue = $Param{Data}->{$FieldName};
760                        }
761
762                        # Otherwise check if field data is and array and compare each element until one match.
763                        elsif ( ref $Param{Data}->{$FieldName} eq 'ARRAY' ) {
764
765                            ITEM:
766                            for my $Item ( @{ $Param{Data}->{$FieldName} } ) {
767                                if ( $Item =~ $ActualCondition->{Fields}->{$FieldName}->{Match} ) {
768                                    $Match      = 1;
769                                    $MatchValue = "Item: [$Item]";
770                                    last ITEM;
771                                }
772                            }
773                        }
774                    }
775
776                    if ($Match) {
777                        $FieldSuccess++;
778
779                        $Self->DebugLog(
780                            MessageType    => 'Match',
781                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
782                            ConditionName  => $ConditionName,
783                            FieldName      => $FieldName,
784                            MatchType      => 'Regexp',
785                            MatchValue     => $MatchValue,
786                            MatchCondition => $ActualCondition->{Fields}->{$FieldName}->{Match}
787                        );
788
789                        # Successful check if we just need one matching Condition to make this Transition valid.
790                        if ( $ConditionLinking eq 'or' && $CondType eq 'or' ) {
791
792                            $Self->DebugLog(
793                                MessageType      => 'Success',
794                                TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
795                                ConditionName    => $ConditionName,
796                                ConditionType    => $CondType,
797                                ConditionLinking => $ConditionLinking,
798                            );
799
800                            return $TransitionEntityID;
801                        }
802
803                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
804                    }
805                    else {
806                        $FieldFail++;
807
808                        my $UnmatchedValue = $Param{Data}->{$FieldName};
809                        if ( ref $Param{Data}->{$FieldName} eq 'ARRAY' ) {
810                            $UnmatchedValue = 'Any of [' . join( ', ', @{ $Param{Data}->{$FieldName} } ) . ']';
811                        }
812
813                        $Self->DebugLog(
814                            MessageType    => 'NoMatch',
815                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
816                            ConditionName  => $ConditionName,
817                            FieldName      => $FieldName,
818                            MatchType      => 'Regexp',
819                            MatchValue     => $UnmatchedValue,
820                            MatchCondition => $ActualCondition->{Fields}->{$FieldName}->{Match}
821                        );
822
823                        # Failed check if we have all 'and' conditions.
824                        next TRANSITIONENTITYID if $ConditionLinking eq 'and' && $CondType eq 'and';
825
826                        # Try next Condition if all Condition Fields have to be true.
827                        next CONDITIONNAME if $CondType eq 'and';
828                    }
829                    next FIELDLNAME;
830                }
831                elsif ( $ActualCondition->{Fields}->{$FieldName}->{Type} eq 'Module' ) {
832
833                    # Load Validation Modules.
834                    # Default location for validation modules:
835                    #   Kernel/System/ProcessManagement/TransitionValidation/.
836                    if (
837                        !$Kernel::OM->Get('Kernel::System::Main')
838                        ->Require( $ActualCondition->{Fields}->{$FieldName}->{Match} )
839                        )
840                    {
841                        $Kernel::OM->Get('Kernel::System::Log')->Log(
842                            Priority => 'error',
843                            Message  => "Can't load "
844                                . $ActualCondition->{Fields}->{$FieldName}->{Type}
845                                . "Module for Transition->$TransitionEntityID->Condition->$ConditionName->"
846                                . "Fields->$FieldName validation!",
847                        );
848                        return;
849                    }
850
851                    # Create new ValidateModuleObject.
852                    my $ValidateModuleObject = $ActualCondition->{Fields}->{$FieldName}->{Match}->new();
853
854                    # Handle "Data" Param to ValidateModule's "Validate" subroutine.
855                    if ( $ValidateModuleObject->Validate( Data => $Param{Data} ) ) {
856                        $FieldSuccess++;
857
858                        $Self->DebugLog(
859                            MessageType    => 'Match',
860                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
861                            ConditionName  => $ConditionName,
862                            FieldName      => $FieldName,
863                            MatchType      => 'Module',
864                            Module         => $ActualCondition->{Fields}->{$FieldName}->{Type}
865                        );
866
867                        # Successful check if we just need one matching Condition to make this Transition valid.
868                        if ( $ConditionLinking eq 'or' && $CondType eq 'or' ) {
869
870                            $Self->DebugLog(
871                                MessageType      => 'Success',
872                                TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
873                                ConditionName    => $ConditionName,
874                                ConditionType    => $CondType,
875                                ConditionLinking => $ConditionLinking,
876                            );
877
878                            return $TransitionEntityID;
879                        }
880
881                        next CONDITIONNAME if $ConditionLinking ne 'or' && $CondType eq 'or';
882                    }
883                    else {
884                        $FieldFail++;
885
886                        $Self->DebugLog(
887                            MessageType    => 'NoMatch',
888                            TransitionName => $Transitions->{$TransitionEntityID}->{Name},
889                            ConditionName  => $ConditionName,
890                            FieldName      => $FieldName,
891                            MatchType      => 'Module',
892                            Module         => $ActualCondition->{Fields}->{$FieldName}->{Type}
893                        );
894
895                        # Failed check if we have all 'and' conditions.
896                        next TRANSITIONENTITYID if $ConditionLinking eq 'and' && $CondType eq 'and';
897
898                        # Try next Condition if all Condition Fields have to be true.
899                        next CONDITIONNAME if $CondType eq 'and';
900                    }
901                    next FIELDLNAME;
902                }
903            }
904
905            # FIELDLNAME End.
906            if ( $CondType eq 'and' ) {
907
908                # if we had no failing check this condition matched.
909                if ( !$FieldFail ) {
910
911                    # Successful check if we just need one matching Condition to make this Transition valid.
912                    if ( $ConditionLinking eq 'or' ) {
913
914                        $Self->DebugLog(
915                            MessageType      => 'Success',
916                            TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
917                            ConditionName    => $ConditionName,
918                            ConditionType    => $CondType,
919                            ConditionLinking => $ConditionLinking,
920                        );
921
922                        return $TransitionEntityID;
923                    }
924                    $ConditionSuccess++;
925                }
926                else {
927                    $ConditionFail++;
928
929                    # Failed check if we have all 'and' conditions.
930                    next TRANSITIONENTITYID if $ConditionLinking eq 'and';
931                }
932            }
933            elsif ( $CondType eq 'or' )
934            {
935
936                # If we had at least one successful check, this condition matched.
937                if ( $FieldSuccess > 0 ) {
938
939                    # Successful check if we just need one matching Condition to make this Transition valid.
940                    if ( $ConditionLinking eq 'or' ) {
941
942                        $Self->DebugLog(
943                            MessageType      => 'Success',
944                            TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
945                            ConditionName    => $ConditionName,
946                            ConditionType    => $CondType,
947                            ConditionLinking => $ConditionLinking,
948                        );
949
950                        return $TransitionEntityID;
951                    }
952                    $ConditionSuccess++;
953                }
954                else {
955                    $ConditionFail++;
956
957                    # Failed check if we have all 'and' conditions.
958                    next TRANSITIONENTITYID if $ConditionLinking eq 'and';
959                }
960            }
961            elsif ( $CondType eq 'xor' )
962            {
963
964                # if we had exactly one successful check, this condition matched.
965                if ( $FieldSuccess == 1 ) {
966
967                    # Successful check if we just need one matching Condition to make this Transition valid.
968                    if ( $ConditionLinking eq 'or' ) {
969
970                        $Self->DebugLog(
971                            MessageType      => 'Success',
972                            TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
973                            ConditionName    => $ConditionName,
974                            ConditionType    => $CondType,
975                            ConditionLinking => $ConditionLinking,
976                        );
977
978                        return $TransitionEntityID;
979                    }
980                    $ConditionSuccess++;
981                }
982                else {
983                    $ConditionFail++;
984                }
985            }
986        }
987
988        # CONDITIONNAME End.
989        if ( $ConditionLinking eq 'and' ) {
990
991            # If we had no failing conditions this transition matched.
992            if ( !$ConditionFail ) {
993
994                $Self->DebugLog(
995                    MessageType      => 'Success',
996                    TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
997                    ConditionLinking => $ConditionLinking,
998                );
999
1000                return $TransitionEntityID;
1001            }
1002        }
1003        elsif ( $ConditionLinking eq 'or' )
1004        {
1005
1006            # If we had at least one successful condition, this transition matched.
1007            if ( $ConditionSuccess > 0 ) {
1008
1009                $Self->DebugLog(
1010                    MessageType      => 'Success',
1011                    TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
1012                    ConditionLinking => $ConditionLinking,
1013                );
1014
1015                return $TransitionEntityID;
1016            }
1017        }
1018        elsif ( $ConditionLinking eq 'xor' )
1019        {
1020
1021            # If we had exactly one successful condition, this transition matched.
1022            if ( $ConditionSuccess == 1 ) {
1023
1024                $Self->DebugLog(
1025                    MessageType      => 'Success',
1026                    TransitionName   => $Transitions->{$TransitionEntityID}->{Name},
1027                    ConditionLinking => $ConditionLinking,
1028                );
1029
1030                return $TransitionEntityID;
1031            }
1032        }
1033    }
1034
1035    # TRANSITIONENTITYID End.
1036    # If no transition matched till here, we failed.
1037    return;
1038
1039}
1040
1041sub DebugLog {
1042    my ( $Self, %Param ) = @_;
1043
1044    return 1 if !$Self->{TransitionDebug};
1045
1046    my $Message = "Transition:'$Param{TransitionName}'";
1047
1048    if ( $Param{MessageType} eq 'Match' || $Param{MessageType} eq 'NoMatch' ) {
1049
1050        my $MatchedString = $Param{MessageType} eq 'Match' ? 'Matched' : 'Not matched';
1051
1052        $Message = " Condition:'$Param{ConditionName}' $MatchedString Field:'$Param{FieldName}'";
1053
1054        if ( $Param{MatchType} eq 'Module' ) {
1055            $Message .= " with Transition Validation Module: '$Param{Module}'";
1056        }
1057        else {
1058            $Message .= " as $Param{MatchType}";
1059        }
1060
1061        if ( $Param{MatchValue} && $Param{MatchCondition} ) {
1062            $Message .= " ($Param{MatchValue} matches $Param{MatchCondition})";
1063        }
1064    }
1065    elsif ( $Param{MessageType} eq 'Success' ) {
1066
1067        if ( $Param{ConditionName} && $Param{ConditionType} ) {
1068            $Message = " Condition:'$Param{ConditionName}' Success on Condition Linking:'$Param{ConditionLinking}'"
1069                . "  and Condition Type:'$Param{ConditionType}'";
1070        }
1071        else {
1072            $Message = " Success on Condition Linking:'$Param{ConditionLinking}'";
1073        }
1074    }
1075
1076    # for MessageType 'Custom' or any other, use the given message
1077    else {
1078        return if !$Param{Message};
1079        $Message = $Param{Message};
1080    }
1081
1082    $Kernel::OM->Get('Kernel::System::Log')->Log(
1083        Priority => $Self->{TransitionDebugLogPriority},
1084        Message  => $Message,
1085    );
1086
1087    return 1;
1088}
1089
10901;
1091
1092=head1 TERMS AND CONDITIONS
1093
1094This software is part of the OTRS project (L<https://otrs.org/>).
1095
1096This software comes with ABSOLUTELY NO WARRANTY. For details, see
1097the enclosed file COPYING for license information (GPL). If you
1098did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
1099
1100=cut
1101