# -- # 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::ITSMConfigItem::Version; use strict; use warnings; ## nofilter(TidyAll::Plugin::OTRS::Migrations::OTRS6::SysConfig) use Kernel::System::VariableCheck qw(:all); use Storable; our $ObjectManagerDisabled = 1; =head1 NAME Kernel::System::ITSMConfigItem::Version - sub module of Kernel::System::ITSMConfigItem =head1 DESCRIPTION All version functions. =head1 PUBLIC INTERFACE =head2 VersionZoomList() return a config item version list as array-hash reference my $VersionListRef = $ConfigItemObject->VersionZoomList( ConfigItemID => 123, ); =cut sub VersionZoomList { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{ConfigItemID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need ConfigItemID!', ); return; } # get config item my $ConfigItem = $Self->ConfigItemGet( ConfigItemID => $Param{ConfigItemID}, ); # get version zoom list $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => 'SELECT id, name, depl_state_id, inci_state_id, create_time, create_by ' . 'FROM configitem_version WHERE configitem_id = ? ORDER BY id', Bind => [ \$Param{ConfigItemID} ], ); # fetch the result my @VersionList; while ( my @Row = $Kernel::OM->Get('Kernel::System::DB')->FetchrowArray() ) { my %Version; $Version{VersionID} = $Row[0]; $Version{Name} = $Row[1]; $Version{DeplStateID} = $Row[2]; $Version{InciStateID} = $Row[3]; $Version{CreateTime} = $Row[4]; $Version{CreateBy} = $Row[5]; push @VersionList, \%Version; } for my $Version (@VersionList) { # get deployment state functionality my $DeplState = $Kernel::OM->Get('Kernel::System::GeneralCatalog')->ItemGet( ItemID => $Version->{DeplStateID}, ); $Version->{DeplState} = $DeplState->{Name}; $Version->{DeplStateType} = $DeplState->{Functionality}; # get incident state functionality my $InciState = $Kernel::OM->Get('Kernel::System::GeneralCatalog')->ItemGet( ItemID => $Version->{InciStateID}, ); $Version->{InciState} = $InciState->{Name}; $Version->{InciStateType} = $InciState->{Functionality}; # add config item data $Version->{ClassID} = $ConfigItem->{ClassID}; $Version->{Class} = $ConfigItem->{Class}; $Version->{Number} = $ConfigItem->{Number}; $Version->{CurDeplStateID} = $ConfigItem->{CurDeplStateID}; $Version->{CurDeplState} = $ConfigItem->{CurDeplState}; $Version->{CurDeplStateType} = $ConfigItem->{CurDeplStateType}; $Version->{CurInciStateID} = $ConfigItem->{CurInciStateID}; $Version->{CurInciState} = $ConfigItem->{CurInciState}; $Version->{CurInciStateType} = $ConfigItem->{CurInciStateType}; } return \@VersionList; } =head2 VersionListAll() Returns a two-dimensional hash reference with the config item ids as keys of the first level and the corresponding version ids as keys of the second level, then followed by the version data as hash reference. my $VersionListRef = $ConfigItemObject->VersionListAll( ConfigItemIDs => [ 1, 2, 3, 4, ...], # optional OlderDate => '2014-12-22 23:59:59', # optional # finds versions older than the given date # Format MUST be # YYYY-MM-DD HH:MM:SS # fill missing values with 0 first # Example: 2014-04-01 07:03:04 # otherwise it won't be taken as # valid search parameter Limit => 1000000, # optional ); Returns: $VersionListRef = { # ConfigItemID 1 => { # VersionID 100 => { VersionID => 100, ConfigItemID => 1, Name => 'ConfigItem1', DefinitionID => 5, DeplStateID => 3, InciStateID => 2, CreateTime => '2016-03-22 17:58:00', CreateBy => 1, }, # VersionID 101 => { VersionID => 101, ConfigItemID => 1, Name => 'ConfigItem2', DefinitionID => 5, DeplStateID => 3, InciStateID => 2, CreateTime => '2016-03-22 17:58:00', CreateBy => 1, }, }, # ConfigItemID 2 => { # VersionID 150 => { VersionID => 150, ConfigItemID => 2, Name => 'ConfigItem1', DefinitionID => 5, DeplStateID => 3, InciStateID => 2, CreateTime => '2016-03-22 17:58:00', CreateBy => 1, }, # VersionID 151 => { VersionID => 151, ConfigItemID => 2, Name => 'ConfigItem1', DefinitionID => 5, DeplStateID => 3, InciStateID => 2, CreateTime => '2016-03-22 17:58:00', CreateBy => 1, }, }, }; =cut sub VersionListAll { my ( $Self, %Param ) = @_; # build sql my $SQL = 'SELECT id, configitem_id, name, definition_id, depl_state_id, inci_state_id, create_time, create_by FROM configitem_version WHERE 1=1'; # if we got ConfigItemIDs make sure we just have numeric ids, # extract those and use it for the query if ( IsArrayRefWithData( $Param{ConfigItemIDs} ) ) { my @ConfigItemIDs = grep { $_ =~ /^\d+$/ } @{ $Param{ConfigItemIDs} }; $SQL .= ' AND configitem_id IN (' . join ', ', @ConfigItemIDs . ')'; } my @BindParameter; if ( $Param{OlderDate} && $Param{OlderDate} =~ /^\d{4}\-\d{2}\-\d{2}\ \d{2}\:\d{2}:\d{2}$/ ) { $SQL .= ' AND create_time < ?'; push @BindParameter, \$Param{OlderDate}; } # set limit if ( $Param{Limit} ) { $Param{Limit} = $Kernel::OM->Get('Kernel::System::DB')->Quote( $Param{Limit}, 'Integer' ); } if (@BindParameter) { $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => $SQL, Bind => \@BindParameter, Limit => $Param{Limit}, ); } else { $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => $SQL, Limit => $Param{Limit}, ); } my %Results; while ( my @Row = $Kernel::OM->Get('Kernel::System::DB')->FetchrowArray() ) { $Results{ $Row[1] }->{ $Row[0] } = { VersionID => $Row[0], ConfigItemID => $Row[1], Name => $Row[2] || '', DefinitionID => $Row[3] || '', DeplStateID => $Row[4] || '', InciStateID => $Row[5] || '', CreateTime => $Row[6] || '', CreateBy => $Row[7] || '', }; } return \%Results; } =head2 VersionList() return a config item version list as array reference my $VersionListRef = $ConfigItemObject->VersionList( ConfigItemID => 123, ); =cut sub VersionList { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{ConfigItemID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need ConfigItemID!', ); return; } # get version list $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => 'SELECT id FROM configitem_version WHERE configitem_id = ? ORDER BY id', Bind => [ \$Param{ConfigItemID} ], ); # fetch the result my @VersionList; while ( my @Row = $Kernel::OM->Get('Kernel::System::DB')->FetchrowArray() ) { push @VersionList, $Row[0]; } return \@VersionList; } =head2 VersionGet() returns a version of a config item as hash reference. The returned hash contains following attributes. $Version{VersionID} $Version{ConfigItemID} $Version{Number} $Version{ClassID} $Version{Class} $Version{LastVersionID} $Version{Name} $Version{DefinitionID} $Version{DeplStateID} $Version{DeplState} $Version{DeplStateType} $Version{CurDeplStateID} $Version{CurDeplState} $Version{CurDeplStateType} $Version{InciStateID} $Version{InciState} $Version{InciStateType} $Version{CurInciStateID} $Version{CurInciState} $Version{CurInciStateType} $Version{XMLDefinition} $Version{XMLData} $Version{CreateTime} $Version{CreateBy} my $VersionRef = $ConfigItemObject->VersionGet( VersionID => 123, XMLDataGet => 1, # (optional) default 1 (0|1) ); or my $VersionRef = $ConfigItemObject->VersionGet( ConfigItemID => 123, ); When the data from the XML storage is not needed then fetching the XML data can be explicitly turned off by passing XMLDataGet => 0. my $VersionRef = $ConfigItemObject->VersionGet( ConfigItemID => 123, XMLDataGet => 0, ); =cut sub VersionGet { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{VersionID} && !$Param{ConfigItemID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need VersionID or ConfigItemID!', ); return; } if ( !defined $Param{XMLDataGet} ) { $Param{XMLDataGet} = 1; } my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache'); if ( $Param{VersionID} ) { # check if result is already cached my $CacheKey = 'VersionGet::VersionID::' . $Param{VersionID} . '::XMLData::' . $Param{XMLDataGet}; my $Cache = $CacheObject->Get( Type => $Self->{CacheType}, Key => $CacheKey, ); return Storable::dclone($Cache) if $Cache; # get version $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => 'SELECT id, configitem_id, name, definition_id, ' . 'depl_state_id, inci_state_id, create_time, create_by ' . 'FROM configitem_version WHERE id = ?', Bind => [ \$Param{VersionID} ], Limit => 1, ); } else { # check if result is already cached my $CacheKey = 'VersionGet::ConfigItemID::' . $Param{ConfigItemID} . '::XMLData::' . $Param{XMLDataGet}; my $Cache = $CacheObject->Get( Type => $Self->{CacheType}, Key => $CacheKey, ); return Storable::dclone($Cache) if $Cache; # get version $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => 'SELECT id, configitem_id, name, definition_id, ' . 'depl_state_id, inci_state_id, create_time, create_by ' . 'FROM configitem_version ' . 'WHERE configitem_id = ? ORDER BY id DESC', Bind => [ \$Param{ConfigItemID} ], Limit => 1, ); } # fetch the result my %Version; while ( my @Row = $Kernel::OM->Get('Kernel::System::DB')->FetchrowArray() ) { $Version{VersionID} = $Row[0]; $Version{ConfigItemID} = $Row[1]; $Version{Name} = $Row[2]; $Version{DefinitionID} = $Row[3]; $Version{DeplStateID} = $Row[4]; $Version{InciStateID} = $Row[5]; $Version{CreateTime} = $Row[6]; $Version{CreateBy} = $Row[7]; } # check version if ( !$Version{VersionID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'No such config item version!', ); return; } # get deployment state functionality my $DeplState = $Kernel::OM->Get('Kernel::System::GeneralCatalog')->ItemGet( ItemID => $Version{DeplStateID}, ); $Version{DeplState} = $DeplState->{Name}; $Version{DeplStateType} = $DeplState->{Functionality}; # get incident state functionality my $InciState = $Kernel::OM->Get('Kernel::System::GeneralCatalog')->ItemGet( ItemID => $Version{InciStateID}, ); $Version{InciState} = $InciState->{Name}; $Version{InciStateType} = $InciState->{Functionality}; # get config item my $ConfigItem = $Self->ConfigItemGet( ConfigItemID => $Version{ConfigItemID}, Cache => 0, ); # check config item data if ( !$ConfigItem || ref $ConfigItem ne 'HASH' ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Can't get config item $Version{ConfigItemID}!", ); return; } $Version{ClassID} = $ConfigItem->{ClassID}; $Version{Class} = $ConfigItem->{Class}; $Version{LastVersionID} = $ConfigItem->{LastVersionID}; $Version{Number} = $ConfigItem->{Number}; $Version{CurDeplStateID} = $ConfigItem->{CurDeplStateID}; $Version{CurDeplState} = $ConfigItem->{CurDeplState}; $Version{CurDeplStateType} = $ConfigItem->{CurDeplStateType}; $Version{CurInciStateID} = $ConfigItem->{CurInciStateID}; $Version{CurInciState} = $ConfigItem->{CurInciState}; $Version{CurInciStateType} = $ConfigItem->{CurInciStateType}; # set cache for VersionID without xml data (always) my $CacheKey = 'VersionGet::VersionID::' . $Version{VersionID} . '::XMLData::0'; $CacheObject->Set( Type => $Self->{CacheType}, TTL => $Self->{CacheTTL}, Key => $CacheKey, Value => Storable::dclone( \%Version ), ); # set cache for ConfigItemID without xml data (only if called with ConfigItemID) if ( $Param{ConfigItemID} ) { $CacheKey = 'VersionGet::ConfigItemID::' . $Version{ConfigItemID} . '::XMLData::0'; $CacheObject->Set( Type => $Self->{CacheType}, TTL => $Self->{CacheTTL}, Key => $CacheKey, Value => Storable::dclone( \%Version ), ); } # done if we don't need xml data return \%Version if !$Param{XMLDataGet}; # get xml definition my $Definition = $Self->DefinitionGet( DefinitionID => $Version{DefinitionID}, ); $Version{XMLDefinition} = $Definition->{DefinitionRef}; # get xml data $Version{XMLData} = $Self->_XMLVersionGet( ClassID => $ConfigItem->{ClassID}, VersionID => $Version{VersionID}, ); # set cache for VersionID (always) $CacheKey = 'VersionGet::VersionID::' . $Version{VersionID} . '::XMLData::1'; $CacheObject->Set( Type => $Self->{CacheType}, TTL => $Self->{CacheTTL}, Key => $CacheKey, Value => Storable::dclone( \%Version ), ); # set cache for ConfigItemID (only if called with ConfigItemID) if ( $Param{ConfigItemID} ) { $CacheKey = 'VersionGet::ConfigItemID::' . $Version{ConfigItemID} . '::XMLData::1'; $CacheObject->Set( Type => $Self->{CacheType}, TTL => $Self->{CacheTTL}, Key => $CacheKey, Value => Storable::dclone( \%Version ), ); } return \%Version; } =head2 VersionNameGet() returns the name of a version of a config item. my $VersionName = $ConfigItemObject->VersionNameGet( VersionID => 123, ); or my $VersionName = $ConfigItemObject->VersionNameGet( ConfigItemID => 123, ); =cut sub VersionNameGet { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{VersionID} && !$Param{ConfigItemID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need VersionID or ConfigItemID!', ); return; } my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache'); if ( $Param{VersionID} ) { # check if result is already cached my $CacheKey = 'VersionNameGet::VersionID::' . $Param{VersionID}; my $Cache = $CacheObject->Get( Type => $Self->{CacheType}, Key => $CacheKey, ); return ${$Cache} if $Cache; # get version $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => 'SELECT id, name ' . 'FROM configitem_version WHERE id = ?', Bind => [ \$Param{VersionID} ], Limit => 1, ); } else { # check if result is already cached my $CacheKey = 'VersionNameGet::ConfigItemID::' . $Param{ConfigItemID}; my $Cache = $CacheObject->Get( Type => $Self->{CacheType}, Key => $CacheKey, ); return ${$Cache} if $Cache; # get version $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => 'SELECT id, name ' . 'FROM configitem_version ' . 'WHERE configitem_id = ? ORDER BY id DESC', Bind => [ \$Param{ConfigItemID} ], Limit => 1, ); } # fetch the result my %Version; while ( my @Row = $Kernel::OM->Get('Kernel::System::DB')->FetchrowArray() ) { $Version{VersionID} = $Row[0]; $Version{Name} = $Row[1]; } # check version if ( !$Version{VersionID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'No such config item version!', ); return; } # set cache for VersionID (always) my $CacheKey = 'VersionNameGet::VersionID::' . $Version{VersionID}; $CacheObject->Set( Type => $Self->{CacheType}, TTL => $Self->{CacheTTL}, Key => $CacheKey, Value => \$Version{Name}, ); # set cache for ConfigItemID (only if called with ConfigItemID) if ( $Param{ConfigItemID} ) { $CacheKey = 'VersionNameGet::ConfigItemID::' . $Param{ConfigItemID}; $CacheObject->Set( Type => $Self->{CacheType}, TTL => $Self->{CacheTTL}, Key => $CacheKey, Value => \$Version{Name}, ); } return $Version{Name}; } =head2 VersionConfigItemIDGet() return the config item id of a version my $ConfigItemID = $ConfigItemObject->VersionConfigItemIDGet( VersionID => 123, ); =cut sub VersionConfigItemIDGet { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{VersionID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need VersionID!', ); return; } # check if result is already cached return $Self->{Cache}->{VersionConfigItemIDGet}->{ $Param{VersionID} } if $Self->{Cache}->{VersionConfigItemIDGet}->{ $Param{VersionID} }; # get config item id $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => 'SELECT configitem_id FROM configitem_version WHERE id = ?', Bind => [ \$Param{VersionID} ], Limit => 1, ); # fetch the result my $ConfigItemID; while ( my @Row = $Kernel::OM->Get('Kernel::System::DB')->FetchrowArray() ) { $ConfigItemID = $Row[0]; } # cache the result $Self->{Cache}->{VersionConfigItemIDGet}->{ $Param{VersionID} } = $ConfigItemID; return $ConfigItemID; } =head2 VersionAdd() add a new version my $VersionID = $ConfigItemObject->VersionAdd( ConfigItemID => 123, Name => 'The Name', DefinitionID => 1212, DeplStateID => 8, InciStateID => 4, XMLData => $ArrayHashRef, # (optional) UserID => 1, ); =cut sub VersionAdd { my ( $Self, %Param ) = @_; # check needed stuff for my $Attribute (qw(ConfigItemID Name DefinitionID DeplStateID InciStateID UserID)) { if ( !$Param{$Attribute} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Attribute!", ); return; } } my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache'); # get deployment state list my $DeplStateList = $Kernel::OM->Get('Kernel::System::GeneralCatalog')->ItemList( Class => 'ITSM::ConfigItem::DeploymentState', ); return if !$DeplStateList; return if ref $DeplStateList ne 'HASH'; # check the deployment state id if ( !$DeplStateList->{ $Param{DeplStateID} } ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'No valid deployment state id given!', ); return; } # get incident state list my $InciStateList = $Kernel::OM->Get('Kernel::System::GeneralCatalog')->ItemList( Class => 'ITSM::Core::IncidentState', ); return if !$InciStateList; return if ref $InciStateList ne 'HASH'; # check the incident state id if ( !$InciStateList->{ $Param{InciStateID} } ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'No valid incident state id given!', ); return; } # get VersionList my $VersionList = $Self->VersionList( ConfigItemID => $Param{ConfigItemID}, ); my $ConfigItemInfo = {}; if ( @{$VersionList} ) { # get old version info for comparisons with current version # this is needed to trigger some events $ConfigItemInfo = $Self->VersionGet( ConfigItemID => $Param{ConfigItemID}, XMLDataGet => 0, ); } else { # get config item $ConfigItemInfo = $Self->ConfigItemGet( ConfigItemID => $Param{ConfigItemID}, ); } return if !$ConfigItemInfo; return if ref $ConfigItemInfo ne 'HASH'; # check, whether the feature to check for a unique name is enabled if ( $Kernel::OM->Get('Kernel::Config')->Get('UniqueCIName::EnableUniquenessCheck') ) { my $NameDuplicates = $Self->UniqueNameCheck( ConfigItemID => $Param{ConfigItemID}, ClassID => $ConfigItemInfo->{ClassID}, Name => $Param{Name}, ); # stop processing if the name is not unique if ( IsArrayRefWithData($NameDuplicates) ) { # build a string of all duplicate IDs my $Duplicates = join ', ', @{$NameDuplicates}; # write an error log message containing all the duplicate IDs $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "The name $Param{Name} is already in use (ConfigItemIDs: $Duplicates)!", ); return; } } my $Events = $Self->_GetEvents( Param => \%Param, ConfigItemInfo => $ConfigItemInfo, ); my $ReturnVersionID = scalar @{$VersionList} ? $VersionList->[-1] : 0; return $ReturnVersionID if !( $Events && keys %{$Events} ); # Special case that only XML attributes have been changed that should not create a new version if ( $Events->{UpdateLastVersion} && $Events->{UpdateXMLData} && ref $Events->{UpdateXMLData} eq 'ARRAY' ) { my %ValueHash2D = $Kernel::OM->Get('Kernel::System::XML')->XMLHash2D( XMLHash => $Events->{UpdateXMLData}, ); UPDATEVALUE: for my $UpdateValue ( sort keys %{ $Events->{ValueUpdate} } ) { my $ContentString = $UpdateValue . '{\'Content\'}'; # Check if the key exists. next UPDATEVALUE if !exists $ValueHash2D{$ContentString}; my $Type = 'ITSM::ConfigItem::' . $ConfigItemInfo->{ClassID}; # Delete old entries from the database. return if !$DBObject->Do( SQL => ' DELETE FROM xml_storage WHERE xml_type = ? AND xml_key = ? AND xml_content_key = ? ', Bind => [ \$Type, \$ReturnVersionID, \$ContentString, ], ); # Add updated entry to the database. return if !$DBObject->Do( SQL => ' INSERT INTO xml_storage (xml_type, xml_key, xml_content_key, xml_content_value) VALUES (?, ?, ?, ?) ', Bind => [ \$Type, \$ReturnVersionID, \$ContentString, \$ValueHash2D{$ContentString}, ], ); # Delete cache. $CacheObject->Delete( Type => 'XML', Key => "$Type-$ReturnVersionID", ); } # delete affected caches my $CacheKey = 'VersionGet::ConfigItemID::' . $Param{ConfigItemID} . '::XMLData::'; for my $XMLData (qw(0 1)) { $CacheObject->Delete( Type => $Self->{CacheType}, Key => $CacheKey . $XMLData, ); } # delete affected caches $CacheKey = 'VersionGet::VersionID::' . $ReturnVersionID . '::XMLData::'; for my $XMLData (qw(0 1)) { $CacheObject->Delete( Type => $Self->{CacheType}, Key => $CacheKey . $XMLData, ); } # update change_time and change_by of the config item return if !$DBObject->Do( SQL => ' UPDATE configitem SET change_time = current_timestamp, change_by = ? WHERE id = ?', Bind => [ \$Param{UserID}, \$Param{ConfigItemID}, ], ); # delete config item cache $CacheKey = 'ConfigItemGet::ConfigItemID::' . $Param{ConfigItemID}; $CacheObject->Delete( Type => $Self->{CacheType}, Key => $CacheKey, ); # Trigger ValueUpdate event. $Self->_EventHandlerForChangedXMLValues( ConfigItemID => $Param{ConfigItemID}, UpdateValues => $Events->{ValueUpdate}, UserID => $Param{UserID}, ); return $ReturnVersionID; } # insert new version my $Success = $DBObject->Do( SQL => 'INSERT INTO configitem_version ' . '(configitem_id, name, definition_id, ' . 'depl_state_id, inci_state_id, create_time, create_by) VALUES ' . '(?, ?, ?, ?, ?, current_timestamp, ?)', Bind => [ \$Param{ConfigItemID}, \$Param{Name}, \$Param{DefinitionID}, \$Param{DeplStateID}, \$Param{InciStateID}, \$Param{UserID}, ], ); return if !$Success; # delete affected caches my $CacheKey = 'VersionGet::ConfigItemID::' . $Param{ConfigItemID} . '::XMLData::'; for my $XMLData (qw(0 1)) { $CacheObject->Delete( Type => $Self->{CacheType}, Key => $CacheKey . $XMLData, ); } $CacheObject->Delete( Type => $Self->{CacheType}, Key => 'VersionNameGet::ConfigItemID::' . $Param{ConfigItemID}, ); # get id of new version $DBObject->Prepare( SQL => 'SELECT id, create_time FROM configitem_version WHERE ' . 'configitem_id = ? ORDER BY id DESC', Bind => [ \$Param{ConfigItemID} ], Limit => 1, ); # fetch the result my $VersionID; my $CreateTime; while ( my @Row = $DBObject->FetchrowArray() ) { $VersionID = $Row[0]; $CreateTime = $Row[1]; } # check version id if ( !$VersionID ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Can't get the new version id!", ); return; } # add xml data if ( $Param{XMLData} && ref $Param{XMLData} eq 'ARRAY' ) { $Self->_XMLVersionAdd( ClassID => $ConfigItemInfo->{ClassID}, ConfigItemID => $Param{ConfigItemID}, VersionID => $VersionID, XMLData => $Param{XMLData}, ); } # update last_version_id, cur_depl_state_id, cur_inci_state_id, change_time and change_by $DBObject->Do( SQL => 'UPDATE configitem SET last_version_id = ?, ' . 'cur_depl_state_id = ?, cur_inci_state_id = ?, ' . 'change_time = ?, change_by = ? ' . 'WHERE id = ?', Bind => [ \$VersionID, \$Param{DeplStateID}, \$Param{InciStateID}, \$CreateTime, \$Param{UserID}, \$Param{ConfigItemID}, ], ); # delete config item cache $CacheKey = 'ConfigItemGet::ConfigItemID::' . $Param{ConfigItemID}; $CacheObject->Delete( Type => $Self->{CacheType}, Key => $CacheKey, ); # trigger VersionCreate event $Self->EventHandler( Event => 'VersionCreate', Data => { ConfigItemID => $Param{ConfigItemID}, Comment => $VersionID, }, UserID => $Param{UserID}, ); # compare current and old values if ( $Events->{ValueUpdate} ) { $Self->_EventHandlerForChangedXMLValues( ConfigItemID => $Param{ConfigItemID}, UpdateValues => $Events->{ValueUpdate}, UserID => $Param{UserID}, ); } # trigger definition update event if ( $Events->{DefinitionUpdate} ) { $Self->EventHandler( Event => 'DefinitionUpdate', Data => { ConfigItemID => $Param{ConfigItemID}, Comment => $Events->{DefinitionUpdate}, }, UserID => $Param{UserID}, ); } # check old and new name if ( $Events->{NameUpdate} ) { $Self->EventHandler( Event => 'NameUpdate', Data => { ConfigItemID => $Param{ConfigItemID}, Comment => $Events->{NameUpdate}, }, UserID => $Param{UserID}, ); } # trigger incident state update event if ( $Events->{IncidentStateUpdate} ) { $Self->EventHandler( Event => 'IncidentStateUpdate', Data => { ConfigItemID => $Param{ConfigItemID}, Comment => $Events->{IncidentStateUpdate}, }, UserID => $Param{UserID}, ); } # trigger deployment state update event if ( $Events->{DeploymentStateUpdate} ) { $Self->EventHandler( Event => 'DeploymentStateUpdate', Data => { ConfigItemID => $Param{ConfigItemID}, Comment => $Events->{DeploymentStateUpdate}, }, UserID => $Param{UserID}, ); } # recalculate the current incident state of all linked config items $Self->CurInciStateRecalc( ConfigItemID => $Param{ConfigItemID}, ); return $VersionID; } =head2 VersionDelete() delete an existing version or versions my $True = $ConfigItemObject->VersionDelete( VersionID => 123, UserID => 1, ); or my $True = $ConfigItemObject->VersionDelete( ConfigItemID => 321, UserID => 1, ); my $True = $ConfigItemObject->VersionDelete( VersionIDs => [ 1, 2, 3, 4 ], UserID => 1, ); =cut sub VersionDelete { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{UserID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserID!', ); return; } if ( !$Param{VersionID} && !$Param{ConfigItemID} && !IsArrayRefWithData( $Param{VersionIDs} ) ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need VersionID, ConfigItemID or VersionIDs!', ); return; } my $VersionList = []; if ( $Param{VersionID} ) { push @{$VersionList}, $Param{VersionID}; } elsif ( $Param{VersionIDs} ) { push @{$VersionList}, @{ $Param{VersionIDs} }; } else { # get version list $VersionList = $Self->VersionList( ConfigItemID => $Param{ConfigItemID}, ); } return 1 if !scalar @{$VersionList}; # get config item id for version (needed for event handling) my $ConfigItemID = $Param{ConfigItemID}; my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache'); # to store unique config item ids if there are versions from different config items my %ConfigItemIDs; my $Success; for my $VersionID ( @{$VersionList} ) { # get configitem id if neccessary if ( $Param{VersionID} || $Param{VersionIDs} ) { $ConfigItemID = $Self->VersionConfigItemIDGet( VersionID => $VersionID, ); } # remember the unique config item ids $ConfigItemIDs{$ConfigItemID} = 1; # delete the xml version data $Self->_XMLVersionDelete( VersionID => $VersionID, UserID => $Param{UserID}, ); # delete version $Success = $Kernel::OM->Get('Kernel::System::DB')->Do( SQL => "DELETE FROM configitem_version WHERE id = ?", Bind => [ \$VersionID ], ); # trigger VersionDelete event when deletion was successful if ($Success) { $Self->EventHandler( Event => 'VersionDelete', Data => { ConfigItemID => $ConfigItemID, Comment => $VersionID, }, UserID => $Param{UserID}, ); # delete affected caches my $CacheKey = 'VersionGet::VersionID::' . $VersionID . '::XMLData::'; for my $XMLData (qw(0 1)) { $CacheObject->Delete( Type => $Self->{CacheType}, Key => $CacheKey . $XMLData, ); } $CacheObject->Delete( Type => $Self->{CacheType}, Key => 'VersionNameGet::VersionID::' . $VersionID, ); delete $Self->{Cache}->{VersionConfigItemIDGet}->{$VersionID}; } } for my $ConfigItemID ( sort keys %ConfigItemIDs ) { # delete affected caches for ConfigItemID (most recent version might have been removed) my $CacheKey = 'VersionGet::ConfigItemID::' . $ConfigItemID . '::XMLData::'; for my $XMLData (qw(0 1)) { $CacheObject->Delete( Type => $Self->{CacheType}, Key => $CacheKey . $XMLData, ); } $CacheObject->Delete( Type => $Self->{CacheType}, Key => 'VersionNameGet::ConfigItemID::' . $ConfigItemID, ); } return $Success; } =head2 VersionSearch() return a config item list as an array reference my $ConfigItemIDs = $ConfigItemObject->VersionSearch( Name => 'The Name', # (optional) ClassIDs => [ 9, 8, 7, 6 ], # (optional) DeplStateIDs => [ 321, 123 ], # (optional) InciStateIDs => [ 321, 123 ], # (optional) PreviousVersionSearch => 1, # (optional) default 0 (0|1) OrderBy => [ 'ConfigItemID', 'Number' ], # (optional) # default: [ 'ConfigItemID' ] # (ConfigItemID, Name, Number, ClassID, DeplStateID, InciStateID # CreateTime, CreateBy, ChangeTime, ChangeBy) # Additional information for OrderBy: # The OrderByDirection can be specified for each OrderBy attribute. # The pairing is made by the array indices. OrderByDirection => [ 'Up', 'Down' ], # (optional) # default: [ 'Up' ] # (Down | Up) Limit => 122, # (optional) UsingWildcards => 0, # (optional) default 1 ); =cut sub VersionSearch { my ( $Self, %Param ) = @_; # set default values if ( !defined $Param{UsingWildcards} ) { $Param{UsingWildcards} = 1; } # verify that all passed array parameters contain an arrayref ARGUMENT: for my $Argument ( qw( OrderBy OrderByDirection ) ) { if ( !defined $Param{$Argument} ) { $Param{$Argument} ||= []; next ARGUMENT; } if ( ref $Param{$Argument} ne 'ARRAY' ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "$Argument must be an array reference!", ); return; } } # set default order and order direction if ( !@{ $Param{OrderBy} } ) { $Param{OrderBy} = ['ConfigItemID']; } if ( !@{ $Param{OrderByDirection} } ) { $Param{OrderByDirection} = ['Up']; } # define order table my %OrderByTable = ( ConfigItemID => 'vr.configitem_id', Name => 'vr.name', Number => 'ci.configitem_number', ClassID => 'ci.class_id', DeplStateID => 'vr.depl_state_id', InciStateID => 'vr.inci_state_id', CreateTime => 'ci.create_time', CreateBy => 'ci.create_by', # the change time of the CI is the same as the create time of the version! ChangeTime => 'vr.create_time', ChangeBy => 'ci.change_by', ); # check if OrderBy contains only unique valid values my %OrderBySeen; for my $OrderBy ( @{ $Param{OrderBy} } ) { if ( !$OrderBy || !$OrderByTable{$OrderBy} || $OrderBySeen{$OrderBy} ) { # found an error $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "OrderBy contains invalid value '$OrderBy' " . 'or the value is used more than once!', ); return; } # remember the value to check if it appears more than once $OrderBySeen{$OrderBy} = 1; } # check if OrderByDirection array contains only 'Up' or 'Down' DIRECTION: for my $Direction ( @{ $Param{OrderByDirection} } ) { # only 'Up' or 'Down' allowed next DIRECTION if $Direction eq 'Up'; next DIRECTION if $Direction eq 'Down'; # found an error $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "OrderByDirection can only contain 'Up' or 'Down'!", ); return; } # assemble the ORDER BY clause my @SQLOrderBy; my $Count = 0; my @OrderBySelectColumns; for my $OrderBy ( @{ $Param{OrderBy} } ) { # set the default order direction my $Direction = 'DESC'; # add the given order direction if ( $Param{OrderByDirection}->[$Count] ) { if ( $Param{OrderByDirection}->[$Count] eq 'Up' ) { $Direction = 'ASC'; } elsif ( $Param{OrderByDirection}->[$Count] eq 'Down' ) { $Direction = 'DESC'; } } # add SQL push @SQLOrderBy, "$OrderByTable{$OrderBy} $Direction"; push @OrderBySelectColumns, $OrderByTable{$OrderBy}; } continue { $Count++; } # get like escape string needed for some databases (e.g. oracle) my $LikeEscapeString = $Kernel::OM->Get('Kernel::System::DB')->GetDatabaseFunction('LikeEscapeString'); # add name to sql where array my @SQLWhere; if ( defined $Param{Name} && $Param{Name} ne '' ) { # duplicate the name my $Name = $Param{Name}; # quote $Name = $Kernel::OM->Get('Kernel::System::DB')->Quote($Name); if ( $Param{UsingWildcards} ) { # prepare like string $Self->_PrepareLikeString( \$Name ); push @SQLWhere, "LOWER(vr.name) LIKE LOWER('$Name') $LikeEscapeString"; } else { push @SQLWhere, "LOWER(vr.name) = LOWER('$Name')"; } } # set array params my %ArrayParams = ( ClassIDs => 'ci.id = vr.configitem_id AND ci.class_id', DeplStateIDs => 'vr.depl_state_id', InciStateIDs => 'vr.inci_state_id', ); ARRAYPARAM: for my $ArrayParam ( sort keys %ArrayParams ) { next ARRAYPARAM if !$Param{$ArrayParam}; if ( ref $Param{$ArrayParam} ne 'ARRAY' ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "$ArrayParam must be an array reference!", ); return; } next ARRAYPARAM if !@{ $Param{$ArrayParam} }; # quote as integer for my $OneParam ( @{ $Param{$ArrayParam} } ) { $OneParam = $Kernel::OM->Get('Kernel::System::DB')->Quote( $OneParam, 'Integer' ); } # create string my $InString = join q{, }, @{ $Param{$ArrayParam} }; push @SQLWhere, "$ArrayParams{ $ArrayParam } IN ($InString)"; } # add previous version param if ( !$Param{PreviousVersionSearch} ) { push @SQLWhere, 'ci.last_version_id = vr.id'; } # create where string my $WhereString = @SQLWhere ? ' WHERE ' . join q{ AND }, @SQLWhere : ''; # set limit, quote as integer if ( $Param{Limit} ) { $Param{Limit} = $Kernel::OM->Get('Kernel::System::DB')->Quote( $Param{Limit}, 'Integer' ); } # add the order by columns also to the selected columns my $OrderBySelectString = ''; if (@OrderBySelectColumns) { $OrderBySelectString = join ', ', @OrderBySelectColumns; $OrderBySelectString = ', ' . $OrderBySelectString; } # build SQL my $SQL = "SELECT DISTINCT vr.configitem_id $OrderBySelectString " . 'FROM configitem ci, configitem_version vr ' . $WhereString; # add the ORDER BY clause if (@SQLOrderBy) { $SQL .= ' ORDER BY '; $SQL .= join ', ', @SQLOrderBy; $SQL .= ' '; } # ask the database $Kernel::OM->Get('Kernel::System::DB')->Prepare( SQL => $SQL, Limit => $Param{Limit}, ); # fetch the result my @ConfigItemList; while ( my @Row = $Kernel::OM->Get('Kernel::System::DB')->FetchrowArray() ) { push @ConfigItemList, $Row[0]; } return \@ConfigItemList; } =head1 INTERNAL INTERFACE =head2 _GetEvents() This method checks what values were changed and what events have to be triggered. It returns a hash reference with all event names as keys that should be triggered. my $Events = $CIObject->_GetEvents( Param => { DeplStateID => 123, }, ConfigItemInfo => { DeplStateID => 234, }, ); print keys %{$Events}; # prints "DeploymentStateUpdate" =cut sub _GetEvents { my ( $Self, %Param ) = @_; # check needed stuff for my $Needed (qw(ConfigItemInfo Param)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } my $Events = {}; # check old and new name my $OldName = $Param{ConfigItemInfo}->{Name} || ''; my $NewName = $Param{Param}->{Name} || ''; if ( $OldName ne $NewName ) { $Events->{NameUpdate} = $NewName . '%%' . $OldName; } # if depl_state is updated my $LastDeplStateID = $Param{ConfigItemInfo}->{DeplStateID} || ''; my $CurDeplStateID = $Param{Param}->{DeplStateID} || ''; if ( $LastDeplStateID ne $CurDeplStateID ) { $Events->{DeploymentStateUpdate} = $CurDeplStateID . '%%' . $LastDeplStateID; } # if incistate is updated my $LastInciStateID = $Param{ConfigItemInfo}->{InciStateID} || ''; my $CurInciStateID = $Param{Param}->{InciStateID} || ''; if ( $LastInciStateID ne $CurInciStateID ) { $Events->{IncidentStateUpdate} = $CurInciStateID . '%%' . $LastInciStateID; } # check old and new definition_id my $OldDefinitionID = $Param{ConfigItemInfo}->{DefinitionID} || ''; my $NewDefinitionID = $Param{Param}->{DefinitionID} || ''; if ( $OldDefinitionID ne $NewDefinitionID ) { $Events->{DefinitionUpdate} = $NewDefinitionID; } # store if anything like Name, DeploymentState, IncidentState, Definition has been changed. my $NothingElseChanged; if ( !%{$Events} ) { $NothingElseChanged = 1; } # check for changes in XML data if ( $Param{Param}->{XMLData} && ref $Param{Param}->{XMLData} eq 'ARRAY' ) { my %UpdateValues = $Self->_FindChangedXMLValues( ConfigItemID => $Param{Param}->{ConfigItemID}, NewXMLData => $Param{Param}->{XMLData}, DefinitionID => $NewDefinitionID, NothingElseChanged => $NothingElseChanged, ); if (%UpdateValues) { # if only attributes have changed that should not create a new version if ( $UpdateValues{UpdateLastVersion} && $NothingElseChanged ) { $Events->{UpdateLastVersion} = 1; # Clone the data structure, so we can delete the origial. $Events->{UpdateXMLData} = $Kernel::OM->Get('Kernel::System::Storable')->Clone( Data => $UpdateValues{UpdateXMLData}, ); # We need to delete the flag and data again, to not cause any problems later # when processing the values. delete $UpdateValues{UpdateLastVersion}; delete $UpdateValues{UpdateXMLData}; } $Events->{ValueUpdate} = \%UpdateValues; } } return $Events; } =head2 _EventHandlerForChangedXMLValues() This method calls the event handler for each changed value of the config item. The changed values are passed in C as an hashref with tag-keys as keys. Please note that this only handles values inside the XML structure, not general attributes like C. my $Success = $ConfigItemObject->_EventHandlerForChangedXMLValues( ConfigItemID => 123, UpdateValues => { "[1]{'Version'}[1]{'Vendor'}[1]" => 'OldVendor%%NewVendor', "[1]{'Version'}[1]{'Type'}[1]" => '127%%128', } UserID => 1, ); =cut sub _EventHandlerForChangedXMLValues { my ( $Self, %Param ) = @_; # check needed stuff for my $Needed (qw(UpdateValues ConfigItemID UserID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } # trigger ValueUpdate event for each changed value for my $Key ( sort keys %{ $Param{UpdateValues} } ) { $Self->EventHandler( Event => 'ValueUpdate', Data => { ConfigItemID => $Param{ConfigItemID}, Comment => $Key . '%%' . $Param{UpdateValues}->{$Key}, }, UserID => $Param{UserID}, ); } return 1; } =head2 _FindChangedXMLValues() compares the new xml data C with the xml data of the latest version of the config item C. Note that the new XML data does not contain tag keys. All values of the two data sets are compared. When a changed value is encountered, the tag key and the old and the new value are stored in a hash. The hash with the updated values is returned. my %UpdateValues = $ConfigItemObject->_FindChangedXMLValues( ConfigItemID => 123, NewXMLData => [ undef, { 'Version' => [ undef, { 'Owner' => [ undef, { 'Content' => '' }, ], }, ], }, ], ); The returned hash looks like: %UpdateValues = ( "[1]{'Version'}[1]{'Vendor'}[1]" => 'OldVendor%%NewVendor', "[1]{'Version'}[1]{'Type'}[1]" => '127%%128', ); The key is a tag key. The values contains the old and the new XML value. =cut sub _FindChangedXMLValues { my ( $Self, %Param ) = @_; # check for needed stuff for my $Needed (qw(ConfigItemID NewXMLData DefinitionID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } # get a list with all versionnumbers that exist for the # given config item my $VersionList = $Self->VersionList( ConfigItemID => $Param{ConfigItemID}, ); # skip the check if this is the first version of the item return if !@{$VersionList}; # get old version my $OldVersion = $Self->VersionGet( VersionID => $VersionList->[-1], ); # The short names for new and old xml data are used in the 'eval' below, # and we do not want to modify the original parameter data NewXMLData # due to side effects of Perls autovivification feature used later here # in another eval statement. my $NewXMLData = Storable::dclone( $Param{NewXMLData} ); my $OldXMLData = $OldVersion->{XMLData}; # get xml definition my $Definition = $Self->DefinitionGet( DefinitionID => $Param{DefinitionID}, ); # get all tagkeys in new and old XML data # use a side effect of XMLHash2D(), which adds the tag keys to the passed in data structure $Kernel::OM->Get('Kernel::System::XML')->XMLHash2D( XMLHash => $NewXMLData ); my @TagKeys = $Self->_GrabTagKeys( Data => [ $OldXMLData, $NewXMLData ] ); # get an unique list of all tag keys my %UniqueTagKeys = map { $_ => 1 } @TagKeys; my %UpdateValues; my %SuppressVersionAdd; for my $TagKey ( sort keys %UniqueTagKeys ) { my $NewContent = eval '$NewXMLData->' . $TagKey . '->{Content}' || ''; ## no critic my $OldContent = eval '$OldXMLData->' . $TagKey . '->{Content}' || ''; ## no critic if ( $NewContent ne $OldContent ) { # extract attribute path name my $AttributePath = $TagKey; $AttributePath =~ s{ \A \[.*?\] \{'Version'\} \[.*?\] \{' }{}xms; $AttributePath =~ s{ '\} \[.*?\] \{' }{::}xmsg; $AttributePath =~ s{ '\} \[.*?\] \z }{}xms; # get definition info about attribute my $AttributeInfo = $Self->DefinitionAttributeInfo( Definition => $Definition->{DefinitionRef}, AttributePath => $AttributePath, ); # check if this attribute should be suppressed and nothing else of # Name, DeploymentState, IncidentState, Definition has been changed. if ( $AttributeInfo->{SuppressVersionAdd} && $Param{NothingElseChanged} ) { if ( $AttributeInfo->{SuppressVersionAdd} eq 'UpdateLastVersion' ) { $SuppressVersionAdd{UpdateLastVersion}->{$TagKey} = join '%%', $OldContent, $NewContent; # Build a new data structire only with the update values. eval '$SuppressVersionAdd{UpdateXMLData}->' . $TagKey . '->{Content} = $NewContent'; ## no critic } elsif ( $AttributeInfo->{SuppressVersionAdd} eq 'Ignore' ) { $SuppressVersionAdd{Ignore}->{$TagKey} = join '%%', $OldContent, $NewContent; } } # change was found $UpdateValues{$TagKey} = join '%%', $OldContent, $NewContent; } } # count how many of each SuppressVersionAdd types we have. my $UpdateValuesCount = scalar keys %UpdateValues; my $IgnoreCount = scalar keys %{ $SuppressVersionAdd{Ignore} }; my $UpdateLastVersionCount = scalar keys %{ $SuppressVersionAdd{UpdateLastVersion} }; if (%UpdateValues) { # Update values contains only values that should be ignored. if ( $UpdateValuesCount == $IgnoreCount ) { %UpdateValues = (); } # Update values contains only values that should update the last version or should be ignored, elsif ( ( $UpdateValuesCount == $UpdateLastVersionCount ) || ( $UpdateValuesCount == ( $UpdateLastVersionCount + $IgnoreCount ) ) ) { %UpdateValues = %{ $SuppressVersionAdd{UpdateLastVersion} }; $UpdateValues{UpdateXMLData} = $SuppressVersionAdd{UpdateXMLData}; $UpdateValues{UpdateLastVersion} = 1; } } return %UpdateValues; } =head2 _GrabTagKeys() recursively scans a perl data structure for the hash key 'TagKey' and returns a list of all the values for that key. my @TagKeys = $ConfigItemObject->_GrabTagKeys( Data => $XMLHashReferenz, ); =cut sub _GrabTagKeys { my ( $Self, %Param ) = @_; return () if !$Param{Data}; my @TagKeys; if ( ref $Param{Data} eq 'ARRAY' ) { ELEM: for my $Elem ( @{ $Param{Data} } ) { next ELEM if !$Elem; next ELEM if !ref $Elem; push @TagKeys, $Self->_GrabTagKeys( Data => $Elem ); } } elsif ( ref $Param{Data} eq 'HASH' ) { for my $Key ( sort keys %{ $Param{Data} } ) { if ( $Key eq 'TagKey' ) { push @TagKeys, $Param{Data}->{$Key}; } elsif ( ref $Param{Data}->{$Key} ) { push @TagKeys, $Self->_GrabTagKeys( Data => $Param{Data}->{$Key} ); } } } return @TagKeys; } 1; =head1 TERMS AND CONDITIONS This software is part of the OTRS project (L). 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 L. =cut