Files
scripts/Perl OTRS/Kernel/System/Ticket/Event/TicketStatusLink.pm
2024-10-14 00:08:40 +02:00

618 lines
22 KiB
Perl

# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::System::Ticket::Event::TicketStatusLink;
use strict;
use warnings;
use Kernel::System::VariableCheck qw(:all);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Log',
'Kernel::System::Ticket',
'Kernel::System::User',
'Kernel::System::ITSMConfigItem',
'Kernel::System::GeneralCatalog',
'Kernel::System::LinkObject',
);
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
return $Self;
}
sub Run {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Needed (qw(Data Event Config)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
# check if functionality is enabled
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
return 1 if !$ConfigObject->Get('ITSMConfigItem::SetIncidentStateOnLink');
# get ticket id depending on event
my $TicketID;
if ( $Param{Event} eq 'LinkAdd' || $Param{Event} eq 'LinkDelete' ) {
for my $Needed (qw(ConfigItemID Comment)) {
if ( !$Param{Data}->{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed in Data!",
);
return;
}
}
if ( $Param{Event} eq 'LinkAdd' ) {
if ( !$Param{Data}->{Type} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need Type in Data!",
);
return;
}
}
# check if link event concerns a ticket
return 1 if $Param{Data}->{Comment} !~ m{ \A ( \d+ ) %%Ticket \z }xms;
$TicketID = $1;
}
else {
if ( !$Param{Data}->{TicketID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need TicketID in Data!",
);
return;
}
$TicketID = $Param{Data}->{TicketID};
}
my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
my %Ticket = $TicketObject->TicketGet(
TicketID => $TicketID,
UserID => 1,
);
return 1 if !%Ticket;
# check if ticket type is relevant (optional functionality)
my $OldTicketTypeRelevant;
my $NewTicketTypeRelevant;
my $TicketTypes = $ConfigObject->Get('ITSMConfigItem::LinkStatus::TicketTypes');
if ( $Param{Event} eq 'TicketTypeUpdate' ) {
# ticket types are not used, changes don't affect CI incident states
return 1 if !IsArrayRefWithData($TicketTypes);
# determine type of ticket before last update
my $OldTicketType;
my @HistoryLines = $TicketObject->HistoryGet(
TicketID => $TicketID,
UserID => 1,
);
LINE:
for my $Line ( reverse @HistoryLines ) {
next LINE if $Line->{HistoryType} ne 'TypeUpdate';
my @CommentParts = split '%%', $Line->{Name};
$OldTicketType = $CommentParts[3];
last LINE;
}
if ( !$OldTicketType ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Found no previous ticket type for ticket id $TicketID!",
);
return;
}
# only changes relevant->irrelevant and irrelevant->relevant are of interest
$OldTicketTypeRelevant = 1 if grep { $_ eq $OldTicketType } @{$TicketTypes};
$NewTicketTypeRelevant = 1 if grep { $_ eq $Ticket{Type} } @{$TicketTypes};
return 1 if ( $OldTicketTypeRelevant || '' ) eq ( $NewTicketTypeRelevant || '' );
}
# shortcut for other events (check if ticket type is in scope)
elsif ( IsArrayRefWithData($TicketTypes) ) {
return 1 if !$Ticket{Type};
return 1 if !grep { $_ eq $Ticket{Type} } @{$TicketTypes};
}
# check if ticket state is relevant
my $OldTicketStateRelevant;
my $NewTicketStateRelevant;
my @OpenStateTypes = ( 'new', 'open', 'pending reminder', 'pending auto' );
if ( $Param{Event} eq 'TicketStateUpdate' ) {
if ( !$Param{Data}->{OldTicketData} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need TicketID in OldTicketData!",
);
return;
}
# only changes open->close and close->open are of interest
$NewTicketStateRelevant = 1 if grep { $_ eq $Ticket{StateType} } @OpenStateTypes;
$OldTicketStateRelevant = 1 if grep { $_ eq $Param{Data}->{OldTicketData}->{StateType} } @OpenStateTypes;
return 1 if ( $NewTicketStateRelevant || '' ) eq ( $OldTicketStateRelevant || '' );
}
# shortcut for other events (check if ticket state is in scope)
else {
return 1 if !grep { $_ eq $Ticket{StateType} } @OpenStateTypes;
}
# shortcut for ticket events (check if ticket has any potentially relevant links)
if ( $Param{Event} eq 'TicketStateUpdate' || $Param{Event} eq 'TicketTypeUpdate' ) {
my %LinkKeyList = $Kernel::OM->Get('Kernel::System::LinkObject')->LinkKeyList(
Object1 => 'Ticket',
Key1 => $TicketID,
Object2 => 'ITSMConfigItem',
State => 'Valid',
UserID => 1,
);
return 1 if !IsHashRefWithData( \%LinkKeyList );
}
# prepare incident states and relevant link types per incident state
my $IncidentStates = $ConfigObject->Get('ITSMConfigItem::LinkStatus::IncidentStates');
my $LinkTypes = $ConfigObject->Get('ITSMConfigItem::LinkStatus::LinkTypes');
if ( !IsHashRefWithData($LinkTypes) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need configuration for 'ITSMConfigItem::LinkStatus::LinkTypes'!",
);
return;
}
my %LinkTypesByIncidentState;
if ( $Param{Event} eq 'LinkAdd' ) {
for my $LinkType ( sort keys %{$LinkTypes} ) {
if ( $LinkType eq $Param{Data}->{Type} ) {
push @{ $LinkTypesByIncidentState{ $LinkTypes->{$LinkType} } }, $LinkType;
}
}
return 1 if !IsHashRefWithData( \%LinkTypesByIncidentState );
}
else {
for my $LinkType ( sort keys %{$LinkTypes} ) {
push @{ $LinkTypesByIncidentState{ $LinkTypes->{$LinkType} } }, $LinkType;
}
}
# handle added or removed links
if ( $Param{Event} eq 'LinkAdd' || $Param{Event} eq 'LinkDelete' ) {
my $ConfigItemID = $Param{Data}->{ConfigItemID};
my $Version = $Kernel::OM->Get('Kernel::System::ITSMConfigItem')->VersionGet(
ConfigItemID => $ConfigItemID,
);
# optional: check if CI deployment state is relevant
my $DeploymentStates = $ConfigObject->Get('ITSMConfigItem::LinkStatus::DeploymentStates');
if ( IsArrayRefWithData($DeploymentStates) ) {
return 1 if !grep { $_ eq $Version->{DeplState} } @{$DeploymentStates};
}
# raise incident state if necessary
if ( $Param{Event} eq 'LinkAdd' ) {
return $Self->_CheckRaiseIncidentState(
Version => $Version,
IncidentStates => $IncidentStates,
LinkTypesByIncidentState => \%LinkTypesByIncidentState,
TicketID => $TicketID,
);
}
# lower incident state if necessary
return $Self->_CheckLowerIncidentState(
Version => $Version,
IncidentStates => $IncidentStates,
LinkTypesByIncidentState => \%LinkTypesByIncidentState,
TicketID => $TicketID,
);
}
# handle ticket closure/re-open and relevant/irrelevant type change
elsif ( $Param{Event} eq 'TicketStateUpdate' || $Param{Event} eq 'TicketTypeUpdate' ) {
# relevant ticket state and type updates are handled the same way
# both mean a ticket is relevant for CI incident state changes or not
# ticket re-open or change to relevant type = might have to raise incident state of CIs
# ticket closure or change to irrelevant type = might have to lower incident state of CIs
my $RelevantNow;
if (
( $Param{Event} eq 'TicketStateUpdate' && $NewTicketStateRelevant )
|| ( $Param{Event} eq 'TicketTypeUpdate' && $NewTicketTypeRelevant )
)
{
$RelevantNow = 1;
}
# loop through all link types and check links to see if there are possibly affected CIs
my $DeploymentStates = $ConfigObject->Get('ITSMConfigItem::LinkStatus::DeploymentStates');
INCIDENTSTATE:
for my $IncidentState ( @{$IncidentStates} ) {
LINKTYPE:
for my $LinkType ( @{ $LinkTypesByIncidentState{$IncidentState} } ) {
my @LinkedCIs = $Self->_CheckTicketLinks(
TicketID => $TicketID,
Type => $LinkType,
);
next LINKTYPE if !@LinkedCIs;
CONFIGITEMID:
for my $ConfigItemID (@LinkedCIs) {
my $Version = $Kernel::OM->Get('Kernel::System::ITSMConfigItem')->VersionGet(
ConfigItemID => $ConfigItemID,
);
# optional: check if CI deployment state is relevant
if ( IsArrayRefWithData($DeploymentStates) ) {
next CONFIGITEMID if !grep { $_ eq $Version->{DeplState} } @{$DeploymentStates};
}
# check current incident state vs state caused by link
# current incident state is lower than caused by link -> set
if ($RelevantNow) {
# shortcut: nothing to do if CI is already in highest incident state
next CONFIGITEMID if $Version->{InciState} eq $IncidentStates->[0];
# shortcut: nothing to do if CI is already in same incident state
next CONFIGITEMID if $Version->{InciState} eq $IncidentState;
# check if CI is in higher incident state already
TMPINCIDENTSTATE:
for my $TmpIncidentState ( @{$IncidentStates} ) {
# all further incident states are lower than current one -> set new state
last TMPINCIDENTSTATE if $TmpIncidentState eq $IncidentState;
next CONFIGITEMID if $Version->{InciState} eq $TmpIncidentState;
}
# set new incident state
$Self->_SetCIStatus(
Version => $Version,
IncidentState => $IncidentState,
TicketID => $TicketID,
);
}
# current incident state is same as caused by link -> re-calculate
else {
# shortcut: nothing to do if CI is already in lowest incident state
next CONFIGITEMID if $Version->{InciState} eq $IncidentStates->[-1];
# shortcut: nothing to do if CI is not in same incident state
next CONFIGITEMID if $Version->{InciState} ne $IncidentState;
# recalculate incident state exactly
$Self->_CheckLowerIncidentState(
Version => $Version,
IncidentStates => $IncidentStates,
LinkTypesByIncidentState => \%LinkTypesByIncidentState,
TicketID => $TicketID,
);
}
}
}
}
}
# unknown/irrelevant event
else {
return 1;
}
return 1;
}
# Rationale:
# Start at highest (worst) incident state and loop down to current incident state,
# checking for links on all types configured for the corresponding incident state.
# If any link for a higher incident state than the current one is found,
# set this state and exit.
# Due to top-to-bottom method, no higher incident state can occur later.
sub _CheckRaiseIncidentState {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Needed (qw(Version IncidentStates LinkTypesByIncidentState TicketID)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
# loop through target states and check if it needs to be changed
INCIDENTSTATE:
for my $IncidentState ( @{ $Param{IncidentStates} } ) {
# CI already in target state - nothing to do
return 1 if $Param{Version}->{InciState} eq $IncidentState;
# check if we have linked tickets that lead to incident state
LINKTYPE:
for my $LinkType ( @{ $Param{LinkTypesByIncidentState}->{$IncidentState} } ) {
next LINKTYPE if !$Self->_CheckConfigItemLinks(
ConfigItemID => $Param{Version}->{ConfigItemID},
Type => $LinkType,
);
# we have at least one linked ticket - set incident state and exit
return $Self->_SetCIStatus(
Version => $Param{Version},
IncidentState => $IncidentState,
TicketID => $Param{TicketID},
);
}
}
return 1;
}
# Rationale:
# Shortcut if CI is alredy in lowest (best) incident state = can't have changed after link delete.
# Start at highest (worst) incident state and loop down to current incident state,
# checking for links on all types configured for the corresponding incident states.
# If a link for the current incident state is found, exit immediately (no change).
# Otherwise set incident state for first occurance of a link, then exit.
# If no link is found, set the lowest state.
# Due to top-to-bottom method, first remembered incident state is the highest possible.
sub _CheckLowerIncidentState {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Needed (qw(Version IncidentStates LinkTypesByIncidentState TicketID)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
# shortcut for lowest level
return 1 if $Param{Version}->{InciState} eq $Param{IncidentStates}->[-1];
# loop through target states and check if it needs to be changed
my $LowerIncidentState;
INCIDENTSTATE:
for my $IncidentState ( @{ $Param{IncidentStates} } ) {
# skip level if current CI incident state is not on this or any higher level
next INCIDENTSTATE if $Param{Version}->{InciState} ne $IncidentState && !$LowerIncidentState;
# check if we have linked tickets that keep incident state
LINKTYPE:
for my $LinkType ( @{ $Param{LinkTypesByIncidentState}->{$IncidentState} } ) {
next LINKTYPE if !$Self->_CheckConfigItemLinks(
ConfigItemID => $Param{Version}->{ConfigItemID},
Type => $LinkType,
);
# we have at least one linked ticket
# keep incident state
return 1 if $Param{Version}->{InciState} eq $IncidentState;
# otherwise set new (lower) incident state and exit
return $Self->_SetCIStatus(
Version => $Param{Version},
IncidentState => $IncidentState,
TicketID => $Param{TicketID},
);
}
# remember that state needs to be changed (= current level isn'T justified by links anymore)
$LowerIncidentState = 1;
}
# no relevant links have been found, but CI wasn't in lowest incident state before - set it now and exit
return $Self->_SetCIStatus(
Version => $Param{Version},
IncidentState => $Param{IncidentStates}->[-1],
TicketID => $Param{TicketID},
);
}
# set CI to a specified incident state and log change in corresponding ticket
sub _SetCIStatus {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Needed (qw(Version IncidentState TicketID)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
# get incident state list
my $IncidentStateList = $Kernel::OM->Get('Kernel::System::GeneralCatalog')->ItemList(
Class => 'ITSM::Core::IncidentState',
);
return if !IsHashRefWithData($IncidentStateList);
# check if incident state is valid
my %ReverseIncidentStateList = reverse %{$IncidentStateList};
if ( !$ReverseIncidentStateList{ $Param{IncidentState} } ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Have invalid incident state '$Param{IncidentState}'!",
);
return;
}
# add a new version with the new incident state
my $VersionID = $Kernel::OM->Get('Kernel::System::ITSMConfigItem')->VersionAdd(
%{ $Param{Version} },
InciStateID => $ReverseIncidentStateList{ $Param{IncidentState} },
UserID => 1,
);
return if !$VersionID;
# log change in ticket
$Kernel::OM->Get('Kernel::System::Ticket')->HistoryAdd(
TicketID => $Param{TicketID},
HistoryType => 'Misc',
Name => "Updated incident state of config item '$Param{Version}->{Number}' to '$Param{IncidentState}'.",
CreateUserID => 1,
);
return 1;
}
# check if relevant CIs are linked to a ticket
sub _CheckTicketLinks {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Needed (qw(TicketID Type)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
# get link type lookup
my $LinkTypeLookup = $Self->_LinkTypeLookupGet();
my %LinkKeyList = $Kernel::OM->Get('Kernel::System::LinkObject')->LinkKeyList(
Object1 => 'Ticket',
Key1 => $Param{TicketID},
Object2 => 'ITSMConfigItem',
State => 'Valid',
Type => $LinkTypeLookup->{ $Param{Type} }->{Name},
Direction => $LinkTypeLookup->{ $Param{Type} }->{Direction}->{Ticket},
UserID => 1,
);
return if !IsHashRefWithData( \%LinkKeyList );
return ( sort keys %LinkKeyList );
}
# check if relevant open tickets are linked to a CI
sub _CheckConfigItemLinks {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Needed (qw(ConfigItemID Type)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
# get link type lookup
my $LinkTypeLookup = $Self->_LinkTypeLookupGet();
my %LinkKeyList = $Kernel::OM->Get('Kernel::System::LinkObject')->LinkKeyList(
Object1 => 'ITSMConfigItem',
Key1 => $Param{ConfigItemID},
Object2 => 'Ticket',
State => 'Valid',
Type => $LinkTypeLookup->{ $Param{Type} }->{Name},
Direction => $LinkTypeLookup->{ $Param{Type} }->{Direction}->{ITSMConfigItem},
UserID => 1,
);
return if !IsHashRefWithData( \%LinkKeyList );
# list of relevant state types
my @OpenStateTypes = ( 'new', 'open', 'pending reminder', 'pending auto' );
# use ticket type check?
my $TicketTypes = $Kernel::OM->Get('Kernel::Config')->Get('ITSMConfigItem::LinkStatus::TicketTypes');
my $CheckTicketTypes;
$CheckTicketTypes = 1 if IsArrayRefWithData($TicketTypes);
my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
TICKETID:
for my $TicketID ( sort keys %LinkKeyList ) {
my %Ticket = $TicketObject->TicketGet(
TicketID => $TicketID,
UserID => 1,
);
next TICKETID if !grep { $_ eq $Ticket{StateType} } @OpenStateTypes;
if ($CheckTicketTypes) {
next TICKETID if !$Ticket{Type};
next TICKETID if !grep { $_ eq $Ticket{Type} } @{$TicketTypes};
}
return 1;
}
return;
}
# compile list of internal link type names and directions to use for LinkKeyList
sub _LinkTypeLookupGet {
my ( $Self, %Param ) = @_;
# if we have generated lookup before
return $Self->{LinkTypeLookup} if $Self->{LinkTypeLookup};
my %LinkTypeLookup;
my %TypeList = $Kernel::OM->Get('Kernel::System::LinkObject')->TypeList();
for my $TypeNameInternal ( sort keys %TypeList ) {
my $SourceName = $TypeList{$TypeNameInternal}->{SourceName};
my $TargetName = $TypeList{$TypeNameInternal}->{TargetName};
$LinkTypeLookup{$SourceName} = {
Name => $TypeNameInternal,
Direction => {
ITSMConfigItem => $SourceName eq $TargetName ? 'Both' : 'Target',
Ticket => $SourceName eq $TargetName ? 'Both' : 'Source',
},
};
$LinkTypeLookup{$TargetName} = {
Name => $TypeNameInternal,
Direction => {
ITSMConfigItem => $SourceName eq $TargetName ? 'Both' : 'Source',
Ticket => $SourceName eq $TargetName ? 'Both' : 'Target',
},
};
}
# remember result
$Self->{LinkTypeLookup} = \%LinkTypeLookup;
return \%LinkTypeLookup;
}
1;