You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
zabbix/ui/app/controllers/CControllerPopupImportCompa...

476 lines
14 KiB

1 year ago
<?php declare(strict_types = 0);
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
class CControllerPopupImportCompare extends CController {
public const CHANGE_NONE = 0;
public const CHANGE_ADDED = 1;
public const CHANGE_REMOVED = 2;
private $toc = [];
private $id_counter = 0;
protected function init() {
$this->disableCsrfValidation();
}
protected function checkInput(): bool {
$fields = [
'import' => 'in 1',
'rules_preset' => 'in template',
'rules' => 'array'
];
$ret = $this->validateInput($fields);
if (!$ret) {
$this->setResponse(
(new CControllerResponseData(['main_block' => json_encode([
'error' => [
'messages' => array_column(get_and_clear_messages(), 'message')
]
])]))->disableView()
);
}
return $ret;
}
protected function checkPermissions(): bool {
$user_type = $this->getUserType();
switch ($this->getInput('rules_preset', '')) {
case 'template':
return ($user_type === USER_TYPE_ZABBIX_ADMIN || $user_type === USER_TYPE_SUPER_ADMIN);
default:
return false;
}
}
protected function doAction(): void {
$rules = [
'host_groups' => ['updateExisting' => false, 'createMissing' => false],
'template_groups' => ['updateExisting' => false, 'createMissing' => false],
'hosts' => ['updateExisting' => false, 'createMissing' => false],
'templates' => ['updateExisting' => false, 'createMissing' => false],
'templateDashboards' => ['updateExisting' => false, 'createMissing' => false, 'deleteMissing' => false],
'templateLinkage' => ['createMissing' => false, 'deleteMissing' => false],
'items' => ['updateExisting' => false, 'createMissing' => false, 'deleteMissing' => false],
'discoveryRules' => ['updateExisting' => false, 'createMissing' => false, 'deleteMissing' => false],
'triggers' => ['updateExisting' => false, 'createMissing' => false, 'deleteMissing' => false],
'graphs' => ['updateExisting' => false, 'createMissing' => false, 'deleteMissing' => false],
'httptests' => ['updateExisting' => false, 'createMissing' => false, 'deleteMissing' => false],
'maps' => ['updateExisting' => false, 'createMissing' => false],
'images' => ['updateExisting' => false, 'createMissing' => false],
'mediaTypes' => ['updateExisting' => false, 'createMissing' => false],
'valueMaps' => ['updateExisting' => false, 'createMissing' => false, 'deleteMissing' => false]
];
// Adjust defaults for given rule preset, if specified.
switch ($this->getInput('rules_preset')) {
case 'template':
$rules['host_groups'] = ['updateExisting' => true, 'createMissing' => true];
$rules['template_groups'] = ['updateExisting' => true, 'createMissing' => true];
$rules['templates'] = ['updateExisting' => true, 'createMissing' => true];
$rules['templateDashboards'] = ['updateExisting' => true, 'createMissing' => true,
'deleteMissing' => false
];
$rules['items'] = ['updateExisting' => true, 'createMissing' => true, 'deleteMissing' => false];
$rules['discoveryRules'] = ['updateExisting' => true, 'createMissing' => true,
'deleteMissing' => false
];
$rules['triggers'] = ['updateExisting' => true, 'createMissing' => true, 'deleteMissing' => false];
$rules['graphs'] = ['updateExisting' => true, 'createMissing' => true, 'deleteMissing' => false];
$rules['httptests'] = ['updateExisting' => true, 'createMissing' => true, 'deleteMissing' => false];
$rules['templateLinkage'] = ['createMissing' => true, 'deleteMissing' => false];
$rules['valueMaps'] = ['updateExisting' => true, 'createMissing' => true, 'deleteMissing' => false];
break;
}
$request_rules = array_intersect_key($this->getInput('rules', []), $rules);
$request_rules += array_fill_keys(array_keys($rules), []);
$options = array_fill_keys(['updateExisting', 'createMissing', 'deleteMissing'], false);
foreach ($request_rules as $rule_name => &$rule) {
$rule = array_map('boolval', array_intersect_key($rule + $options, $rules[$rule_name]));
}
unset($rule);
$result = false;
if (!isset($_FILES['import_file'])) {
error(_('No file was uploaded.'));
} else {
// CUploadFile throws exceptions, so we need to catch them
try {
$file = new CUploadFile($_FILES['import_file']);
$result = API::Configuration()->importcompare([
'format' => CImportReaderFactory::fileExt2ImportFormat($file->getExtension()),
'source' => $file->getContent(),
'rules' => $request_rules
]);
}
catch (Exception $e) {
error($e->getMessage());
}
}
$data = [
'title' => _('Templates'),
'user' => [
'debug_mode' => $this->getDebugMode()
]
];
if ($result === false) {
$data['error'] = [
'title' => _('Import failed'),
'messages' => array_column(get_and_clear_messages(), 'message')
];
}
else {
$data['diff'] = $this->blocksToDiff($result, 1);
$data['diff_toc'] = $this->normalizeToc($this->toc);
}
$this->setResponse(new CControllerResponseData($data));
}
private function normalizeToc(array $toc): array {
// Order TOC.
$toc = array_replace(array_flip(['removed', 'updated', 'added']), $toc);
$new_toc = [];
$toc_keys = [
'removed' => _('Removed'),
'updated' => _('Updated'),
'added' => _('Added')
];
$names = [
'host_groups' => _('Host groups'),
'template_groups' => _('Template groups'),
'templates' => _('Templates'),
'triggers' => _('Triggers'),
'graphs' => _('Graphs'),
'items' => _('Items'),
'discovery_rules' => _('Discovery rules'),
'dashboards' => _('Dashboards'),
'httptests' => _('Web scenarios'),
'valuemaps' => _('Value mappings'),
'item_prototypes' => _('Item prototypes'),
'trigger_prototypes' => _('Trigger prototypes'),
'graph_prototypes' => _('Graph prototypes'),
'host_prototypes' => _('Host prototypes')
];
foreach ($toc as $toc_key => $changes) {
if (!is_array($changes)) {
continue;
}
$new_changes = [];
foreach ($changes as $key => $values) {
$new_key = $names[$key];
$new_changes[$new_key] = $values;
}
$new_toc_key = $toc_keys[$toc_key];
$new_toc[$new_toc_key] = $new_changes;
}
return $new_toc;
}
/**
* Show exactly which array elements were added/removed/updated. Only on first depth level.
*
* @param string $key
* @param array $before
* @param array $after
* @param int $depth
*
* @return array
*/
private function arrayToRows(string $key, array $before, array $after, int $depth): array {
$rows = [[
'value' => $key . ':',
'depth' => $depth,
'change_type' => self::CHANGE_NONE
]];
$is_hash = CArrayHelper::isHash($before) || CArrayHelper::isHash($after);
// Make sure, order changes are also taken into account.
$unchanged_map = [];
$before_keys = array_keys($before);
$after_keys = array_keys($after);
$last_after_index = -1;
foreach ($before_keys as $before_key) {
$after_keys_count = count($after_keys);
for ($j = $last_after_index + 1; $j < $after_keys_count; $j++) {
$after_key = $after_keys[$j];
if ($is_hash) {
if ($before_key === $after_key && $before[$before_key] === $after[$after_key]) {
$unchanged_map[$before_key] = $after_key;
$last_after_index = $j;
}
}
else {
if ($before[$before_key] === $after[$after_key]) {
$unchanged_map[$before_key] = $after_key;
$last_after_index = $j;
}
}
}
}
unset($after_key, $last_after_index);
$unchanged_before_keys = array_keys($unchanged_map);
$next_unchanged_before_index = 0;
$next_after_index = 0;
foreach ($before_keys as $before_key) {
if (array_key_exists($next_unchanged_before_index, $unchanged_before_keys)
&& $before_key === $unchanged_before_keys[$next_unchanged_before_index]) {
// Show all added after entries.
while (array_key_exists($next_after_index, $after_keys)
&& $after_keys[$next_after_index] !== $unchanged_map[$before_key]) {
$after_key = $after_keys[$next_after_index];
$yaml_key = $this->prepareYamlKey($after_key, $is_hash);
$rows[] = [
'value' => $this->convertToYaml([$yaml_key => $after[$after_key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_ADDED
];
$next_after_index++;
}
// Show unchanged entry.
$yaml_key = $this->prepareYamlKey($before_key, $is_hash);
$rows[] = [
'value' => $this->convertToYaml([$yaml_key => $before[$before_key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_NONE
];
$next_unchanged_before_index++;
$next_after_index++;
}
else {
// Show all removed before entries.
$yaml_key = $this->prepareYamlKey($before_key, $is_hash);
$rows[] = [
'value' => $this->convertToYaml([$yaml_key => $before[$before_key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_REMOVED
];
}
}
// Show remaining after entries.
$after_keys_count = count($after_keys);
for ($after_index = $next_after_index; $after_index < $after_keys_count; $after_index++) {
$after_key = $after_keys[$after_index];
$yaml_key = $this->prepareYamlKey($after_key, $is_hash);
$rows[] = [
'value' => $this->convertToYaml([$yaml_key => $after[$after_key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_ADDED
];
}
return $rows;
}
/**
* Prepares key, with which each array element will be passed to YAML converter. Makes sure the key will be
* properly converted.
*
* @param mixed $key
* @param boolean $is_hash
*
* @return mixed
*/
private function prepareYamlKey($key, bool $is_hash) {
if ($is_hash) {
// Make sure, array with "zero" key element is not considered as indexed array in YAML converter.
// \xE2\x80\x8B is UTF-8 code for zero width space.
$yaml_key = ($key === 0) ? "\xE2\x80\x8B".'0' : $key;
}
else {
// For indexed array any index should be replaced by "-" in YAML converter.
// Passing single-entry array with '0' key, does exactly that.
$yaml_key = 0;
}
return $yaml_key;
}
private function objectToRows(array $before, array $after, int $depth, int $id): array {
$all_keys = [];
foreach (array_keys($before) as $key) {
if (array_key_exists($key, $after)) {
if ($before[$key] == $after[$key]) {
$all_keys[$key] = 'no_change';
}
else if (is_array($before[$key])) {
$all_keys[$key] = 'updated_array';
}
else {
$all_keys[$key] = 'updated';
}
}
else {
$all_keys[$key] = 'removed';
}
}
foreach (array_keys($after) as $key) {
if (!array_key_exists($key, $before)) {
$all_keys[$key] = 'added';
}
}
unset($all_keys['uuid']);
$rows = [];
foreach ($all_keys as $key => $change_type) {
switch ($change_type) {
case 'no_change':
$rows[] = [
'value' => $this->convertToYaml([$key => $before[$key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_NONE
];
break;
case 'updated':
$rows[] = [
'value' => $this->convertToYaml([$key => $before[$key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_REMOVED
];
$rows[] = [
'value' => $this->convertToYaml([$key => $after[$key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_ADDED
];
break;
case 'removed':
$rows[] = [
'value' => $this->convertToYaml([$key => $before[$key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_REMOVED
];
break;
case 'added':
$rows[] = [
'value' => $this->convertToYaml([$key => $after[$key]]),
'depth' => $depth + 1,
'change_type' => self::CHANGE_ADDED
];
break;
case 'updated_array':
$rows = array_merge($rows, $this->arrayToRows($key, $before[$key], $after[$key], $depth + 1));
break;
}
}
if ($rows) {
$rows[0] += ['id' => $id];
}
return $rows;
}
private function blocksToDiff(array $blocks, int $depth, string $outer_change_type = 'updated'): array {
$change_types = [
'added' => self::CHANGE_ADDED,
'removed' => self::CHANGE_REMOVED,
'updated' => self::CHANGE_NONE
];
$rows = [];
foreach ($blocks as $entity_type => $changes) {
$rows[] = [
'value' => $entity_type . ':',
'depth' => $depth,
'change_type' => $change_types[$outer_change_type]
];
foreach ($changes as $change_type => $entities) {
foreach ($entities as $entity) {
$before = array_key_exists('before', $entity) ? $entity['before'] : [];
$after = array_key_exists('after', $entity) ? $entity['after'] : [];
$object = $before ?: $after;
unset($entity['before'], $entity['after']);
$id = $this->id_counter++;
$this->toc[$change_type][$entity_type][] = [
'name' => $this->nameForToc($entity_type, $object),
'id' => $id
];
$rows = array_merge($rows, $this->objectToRows($before, $after, $depth + 1, $id));
// Process any sub-entities.
if ($entity) {
$rows = array_merge($rows, $this->blocksToDiff($entity, $depth + 2, $change_type));
}
}
}
}
return $rows;
}
private function nameForToc(string $entity_type, array $object): string {
switch ($entity_type) {
case 'templates':
return array_key_exists('name', $object) ? $object['name'] : $object['template'];
case 'host_prototypes':
return array_key_exists('name', $object) ? $object['name'] : $object['host'];
default:
return $object['name'];
}
}
private function convertToYaml($object): string {
$writer = new CYamlExportWriter();
return $writer->write($object);
}
}