1<?php
2/*
3** Zabbix
4** Copyright (C) 2001-2021 Zabbix SIA
5**
6** This program is free software; you can redistribute it and/or modify
7** it under the terms of the GNU General Public License as published by
8** the Free Software Foundation; either version 2 of the License, or
9** (at your option) any later version.
10**
11** This program is distributed in the hope that it will be useful,
12** but WITHOUT ANY WARRANTY; without even the implied warranty of
13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14** GNU General Public License for more details.
15**
16** You should have received a copy of the GNU General Public License
17** along with this program; if not, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19**/
20
21
22require_once dirname(__FILE__).'/../../include/blocks.inc.php';
23
24class CControllerWidgetNavTreeView extends CControllerWidget {
25
26	private $problems_per_severity_tpl;
27
28	public function __construct() {
29		parent::__construct();
30
31		$this->setType(WIDGET_NAV_TREE);
32		$this->setValidationRules([
33			'name' => 'string',
34			'uniqueid' => 'required|string',
35			'widgetid' => 'db widget.widgetid',
36			'initial_load' => 'in 0,1',
37			'fields' => 'json'
38		]);
39	}
40
41	protected function getNumberOfProblemsBySysmap(array $navtree_items = []) {
42		$response = [];
43		$sysmapids = [];
44
45		foreach ($navtree_items as $navtree_item) {
46			$sysmapids[$navtree_item['sysmapid']] = true;
47		}
48		unset($sysmapids[0]);
49
50		$sysmaps = $sysmapids
51			? API::Map()->get([
52				'output' => ['sysmapid', 'severity_min'],
53				'selectLinks' => ['linktriggers', 'permission'],
54				'selectSelements' => ['elements', 'elementtype', 'permission'],
55				'sysmapids' => array_keys($sysmapids),
56				'preservekeys' => true
57			])
58			: [];
59
60		if ($sysmaps) {
61			$triggers_per_hosts = [];
62			$triggers_per_host_groups = [];
63			$problems_per_trigger = [];
64			$submaps_relations = [];
65			$submaps_found = [];
66			$host_groups = [];
67			$hosts = [];
68
69			// Gather submaps from all selected maps.
70			foreach ($sysmaps as $map) {
71				foreach ($map['selements'] as $selement) {
72					if ($selement['elementtype'] == SYSMAP_ELEMENT_TYPE_MAP) {
73						if (($element = reset($selement['elements'])) !== false) {
74							$submaps_relations[$map['sysmapid']][] = $element['sysmapid'];
75							$submaps_found[] = $element['sysmapid'];
76						}
77					}
78				}
79			}
80
81			// Gather maps added as submaps for each of map in any depth.
82			$sysmaps_resolved = array_keys($sysmaps);
83			while ($diff = array_diff($submaps_found, $sysmaps_resolved)) {
84				$submaps = API::Map()->get([
85					'output' => ['sysmapid', 'severity_min'],
86					'selectLinks' => ['linktriggers', 'permission'],
87					'selectSelements' => ['elements', 'elementtype', 'permission'],
88					'sysmapids' => $diff,
89					'preservekeys' => true
90				]);
91
92				$sysmaps_resolved = array_merge($sysmaps_resolved, $diff);
93
94				foreach ($submaps as $submap) {
95					$sysmaps[$submap['sysmapid']] = $submap;
96
97					foreach ($submap['selements'] as $selement) {
98						if ($selement['elementtype'] == SYSMAP_ELEMENT_TYPE_MAP) {
99							$element = reset($selement['elements']);
100							if ($element) {
101								$submaps_relations[$submap['sysmapid']][] = $element['sysmapid'];
102								$submaps_found[] = $element['sysmapid'];
103							}
104						}
105					}
106				}
107			}
108
109			// Gather elements from all maps selected.
110			foreach ($sysmaps as $map) {
111				// Collect triggers from map links.
112				foreach ($map['links'] as $link) {
113					foreach ($link['linktriggers'] as $linktrigger) {
114						$problems_per_trigger[$linktrigger['triggerid']] = $this->problems_per_severity_tpl;
115					}
116				}
117
118				// Collect map elements.
119				foreach ($map['selements'] as $selement) {
120					switch ($selement['elementtype']) {
121						case SYSMAP_ELEMENT_TYPE_HOST_GROUP:
122							if (($element = reset($selement['elements'])) !== false) {
123								$host_groups[$element['groupid']] = true;
124							}
125							break;
126
127						case SYSMAP_ELEMENT_TYPE_TRIGGER:
128							foreach (zbx_objectValues($selement['elements'], 'triggerid') as $triggerid) {
129								$problems_per_trigger[$triggerid] = $this->problems_per_severity_tpl;
130							}
131							break;
132
133						case SYSMAP_ELEMENT_TYPE_HOST:
134							if (($element = reset($selement['elements'])) !== false) {
135								$hosts[$element['hostid']] = true;
136							}
137							break;
138					}
139				}
140			}
141
142			// Select lowest severity to reduce amount of data returned by API.
143			$severity_min = min(zbx_objectValues($sysmaps, 'severity_min'));
144
145			// Get triggers related to host groups.
146			if ($host_groups) {
147				$triggers = API::Trigger()->get([
148					'output' => ['triggerid'],
149					'groupids' => array_keys($host_groups),
150					'skipDependent' => true,
151					'selectGroups' => ['groupid'],
152					'preservekeys' => true
153				]);
154
155				foreach ($triggers as $trigger) {
156					foreach ($trigger['groups'] as $host_group) {
157						$triggers_per_host_groups[$host_group['groupid']][$trigger['triggerid']] = true;
158					}
159					$problems_per_trigger[$trigger['triggerid']] = $this->problems_per_severity_tpl;
160				}
161
162				unset($host_groups);
163			}
164
165			// Get triggers related to hosts.
166			if ($hosts) {
167				$triggers = API::Trigger()->get([
168					'output' => ['triggerid'],
169					'selectHosts' => ['hostid'],
170					'hostids' => array_keys($hosts),
171					'skipDependent' => true,
172					'preservekeys' => true,
173					'monitored' => true
174				]);
175
176				foreach ($triggers as $trigger) {
177					if (($host = reset($trigger['hosts'])) !== false) {
178						$triggers_per_hosts[$host['hostid']][$trigger['triggerid']] = true;
179						$problems_per_trigger[$trigger['triggerid']] = $this->problems_per_severity_tpl;
180					}
181				}
182
183				unset($hosts);
184			}
185
186			// Count problems per trigger.
187			if ($problems_per_trigger) {
188				$triggers = API::Trigger()->get([
189					'output' => [],
190					'selectGroups' => ['groupid'],
191					'triggerids' => array_keys($problems_per_trigger),
192					'skipDependent' => true,
193					'preservekeys' => true,
194					'monitored' => true
195				]);
196
197				$problems = API::Problem()->get([
198					'output' => ['objectid', 'severity'],
199					'source' => EVENT_SOURCE_TRIGGERS,
200					'object' => EVENT_OBJECT_TRIGGER,
201					'objectids' => array_keys($triggers),
202					'severities' => range($severity_min, TRIGGER_SEVERITY_COUNT - 1),
203					'preservekeys' => true
204				]);
205
206				if ($problems) {
207					foreach ($problems as $problem) {
208						$problems_per_trigger[$problem['objectid']][$problem['severity']]++;
209					}
210				}
211			}
212
213			// Count problems in each submap included in navigation tree:
214			foreach ($navtree_items as $id => $navtree_item) {
215				$maps_need_to_count_in = $navtree_item['child_sysmapids'];
216				if ($navtree_item['sysmapid'] != 0) {
217					$maps_need_to_count_in[$navtree_item['sysmapid']] = true;
218				}
219
220				$response[$id] = $this->problems_per_severity_tpl;
221				$problems_counted = [];
222
223				foreach (array_keys($maps_need_to_count_in) as $sysmapid) {
224					if (array_key_exists($sysmapid, $sysmaps)) {
225						$map = $sysmaps[$sysmapid];
226
227						// Count problems occurred in linked elements.
228						foreach ($map['selements'] as $selement) {
229							if ($selement['permission'] >= PERM_READ) {
230								$problems = $this->getElementProblems($selement, $problems_per_trigger, $sysmaps,
231									$submaps_relations, $map['severity_min'], $problems_counted, $triggers_per_hosts,
232									$triggers_per_host_groups
233								);
234
235								if ($problems !== null) {
236									$response[$id] = self::sumArrayValues($response[$id], $problems);
237								}
238							}
239						}
240
241						// Count problems occurred in triggers which are related to links.
242						foreach ($map['links'] as $link) {
243							$uncounted_problem_triggers = array_diff_key(
244								array_flip(zbx_objectValues($link['linktriggers'], 'triggerid')),
245								$problems_counted
246							);
247
248							foreach ($uncounted_problem_triggers as $triggerid => $var) {
249								$problems_to_add = $problems_per_trigger[$triggerid];
250								$problems_counted[$triggerid] = true;
251
252								// Remove problems which are less important than map's min-severity.
253								if ($map['severity_min'] > 0) {
254									foreach ($problems_to_add as $sev => $probl) {
255										if ($map['severity_min'] > $sev) {
256											$problems_to_add[$sev] = 0;
257										}
258									}
259								}
260
261								$response[$id] = self::sumArrayValues($response[$id], $problems_to_add);
262							}
263							unset($uncounted_problem_triggers);
264						}
265					}
266				}
267			}
268		}
269
270		foreach ($response as &$row) {
271			// Reduce the amount of data transferred over Ajax.
272			if ($row === $this->problems_per_severity_tpl) {
273				$row = 0;
274			}
275		}
276		unset($row);
277
278		return $response;
279	}
280
281	protected function getElementProblems(array $selement, array $problems_per_trigger, array $sysmaps,
282			array $submaps_relations, $severity_min = 0, array &$problems_counted = [], array $triggers_per_hosts = [],
283			array $triggers_per_host_groups = []) {
284		$problems = null;
285
286		switch ($selement['elementtype']) {
287			case SYSMAP_ELEMENT_TYPE_HOST_GROUP:
288				$problems = $this->problems_per_severity_tpl;
289
290				if (($element = reset($selement['elements'])) !== false) {
291					if (array_key_exists($element['groupid'], $triggers_per_host_groups)) {
292						$uncounted_problem_triggers = array_diff_key($triggers_per_host_groups[$element['groupid']],
293							$problems_counted
294						);
295						foreach ($uncounted_problem_triggers as $triggerid => $var) {
296							$problems_counted[$triggerid] = true;
297							$problems = self::sumArrayValues($problems, $problems_per_trigger[$triggerid]);
298						}
299						unset($uncounted_problem_triggers);
300					}
301				}
302				break;
303
304			case SYSMAP_ELEMENT_TYPE_TRIGGER:
305				$problems = $this->problems_per_severity_tpl;
306				$uncounted_problem_triggers = array_diff_key(
307					array_flip(zbx_objectValues($selement['elements'], 'triggerid')),
308					$problems_counted
309				);
310				foreach ($uncounted_problem_triggers as $triggerid => $var) {
311					$problems_counted[$triggerid] = true;
312					$problems = self::sumArrayValues($problems, $problems_per_trigger[$triggerid]);
313				}
314				unset($uncounted_problem_triggers);
315				break;
316
317			case SYSMAP_ELEMENT_TYPE_HOST:
318				$problems = $this->problems_per_severity_tpl;
319
320				if (($element = reset($selement['elements'])) !== false) {
321					if (array_key_exists($element['hostid'], $triggers_per_hosts)) {
322						$uncounted_problem_triggers = array_diff_key($triggers_per_hosts[$element['hostid']],
323							$problems_counted
324						);
325						foreach ($uncounted_problem_triggers as $triggerid => $var) {
326							$problems_counted[$triggerid] = true;
327							$problems = self::sumArrayValues($problems, $problems_per_trigger[$triggerid]);
328						}
329						unset($uncounted_problem_triggers);
330					}
331				}
332				break;
333
334			case SYSMAP_ELEMENT_TYPE_MAP:
335				$problems = $this->problems_per_severity_tpl;
336
337				if (($submap_element = reset($selement['elements'])) !== false) {
338					// Recursively find all submaps in any depth and put them into an array.
339					$maps_to_process[$submap_element['sysmapid']] = false;
340
341					while (array_filter($maps_to_process, function($item) {return !$item;})) {
342						foreach ($maps_to_process as $linked_map => $val) {
343							$maps_to_process[$linked_map] = true;
344
345							if (array_key_exists($linked_map, $submaps_relations)) {
346								foreach ($submaps_relations[$linked_map] as $submap) {
347									if (!array_key_exists($submap, $maps_to_process)) {
348										$maps_to_process[$submap] = false;
349									}
350								}
351							}
352						}
353					}
354
355					// Count problems in each of selected submap.
356					foreach ($maps_to_process as $sysmapid => $val) {
357						// Count problems in elements assigned to selements.
358						if (array_key_exists($sysmapid, $sysmaps)) {
359							foreach ($sysmaps[$sysmapid]['selements'] as $submap_selement) {
360								if ($submap_selement['permission'] >= PERM_READ) {
361									$problems_in_submap = $this->getElementProblems($submap_selement,
362										$problems_per_trigger, $sysmaps, $submaps_relations,
363										$sysmaps[$sysmapid]['severity_min'], $problems_counted, $triggers_per_hosts,
364										$triggers_per_host_groups
365									);
366
367									if ($problems_in_submap !== null) {
368										$problems = self::sumArrayValues($problems, $problems_in_submap);
369									}
370								}
371							}
372						}
373
374						// Count problems in triggers assigned to linked.
375						if (array_key_exists($sysmapid, $sysmaps)) {
376							foreach ($sysmaps[$sysmapid]['links'] as $link) {
377								if ($link['permission'] >= PERM_READ) {
378									$uncounted_problem_triggers = array_diff_key(
379										array_flip(zbx_objectValues($link['linktriggers'], 'triggerid')),
380										$problems_counted
381									);
382									foreach ($uncounted_problem_triggers as $triggerid => $var) {
383										$problems_counted[$triggerid] = true;
384										$problems = self::sumArrayValues($problems, $problems_per_trigger[$triggerid]);
385									}
386									unset($uncounted_problem_triggers);
387								}
388							}
389						}
390					}
391				}
392				break;
393		}
394
395		// Remove problems which are less important than $severity_min.
396		if ($problems !== null && $severity_min > 0) {
397			foreach ($problems as $sev => $probl) {
398				if ($severity_min > $sev) {
399					$problems[$sev] = 0;
400				}
401			}
402		}
403
404		return $problems;
405	}
406
407	protected function doAction() {
408		$fields = $this->getForm()->getFieldsData();
409		$error = null;
410
411		// Get list of sysmapids.
412		$sysmapids = [];
413		$navtree_items = [];
414		foreach ($fields['navtree'] as $id => $navtree_item) {
415			$sysmapid = array_key_exists('sysmapid', $navtree_item) ? $navtree_item['sysmapid'] : 0;
416			if ($sysmapid != 0) {
417				$sysmapids[$sysmapid] = true;
418			}
419
420			$navtree_items[$id] = [
421				'parent' => $navtree_item['parent'],
422				'sysmapid' => $sysmapid,
423				'child_sysmapids' => []
424			];
425		}
426
427		// Propagate item mapids to all its parent items.
428		foreach ($navtree_items as $navtree_item) {
429			$parent = $navtree_item['parent'];
430
431			while (array_key_exists($parent, $navtree_items)) {
432				if ($navtree_item['sysmapid'] != 0) {
433					$navtree_items[$parent]['child_sysmapids'][$navtree_item['sysmapid']] = true;
434				}
435				$parent = $navtree_items[$parent]['parent'];
436			}
437		}
438
439		// Get severity levels and colors and select list of sysmapids to count problems per maps.
440		$this->problems_per_severity_tpl = [];
441		$config = select_config();
442		$severity_config = [];
443
444		$maps_accessible = $sysmapids
445			? API::Map()->get([
446				'output' => [],
447				'sysmapids' => array_keys($sysmapids),
448				'preservekeys' => true
449			])
450			: [];
451
452		for ($severity = TRIGGER_SEVERITY_NOT_CLASSIFIED; $severity < TRIGGER_SEVERITY_COUNT; $severity++) {
453			$this->problems_per_severity_tpl[$severity] = 0;
454			$severity_config[$severity] = [
455				'name' => getSeverityName($severity, $config),
456				'style_class' => getSeverityStatusStyle($severity)
457			];
458		}
459
460		$widgetid = $this->getInput('widgetid', 0);
461		$navtree_item_selected = 0;
462		$navtree_items_opened = [];
463		if ($widgetid) {
464			$navtree_items_opened = CProfile::findByIdxPattern('web.dashbrd.navtree-%.toggle', $widgetid);
465			// Keep only numerical value from idx key name.
466			foreach ($navtree_items_opened as &$item_opened) {
467				$item_opened = substr($item_opened, 20, -7);
468			}
469			unset($item_opened);
470			$navtree_item_selected = CProfile::get('web.dashbrd.navtree.item.selected', 0, $widgetid);
471		}
472
473		$this->setResponse(new CControllerResponseData([
474			'name' => $this->getInput('name', $this->getDefaultHeader()),
475			'uniqueid' => $this->getInput('uniqueid'),
476			'navtree' => $fields['navtree'],
477			'navtree_item_selected' => $navtree_item_selected,
478			'navtree_items_opened' => $navtree_items_opened,
479			'problems' => $this->getNumberOfProblemsBySysmap($navtree_items),
480			'show_unavailable' => $fields['show_unavailable'],
481			'maps_accessible' => array_keys($maps_accessible),
482			'severity_config' => $severity_config,
483			'initial_load' => $this->getInput('initial_load', 0),
484			'error' => $error,
485			'user' => [
486				'debug_mode' => $this->getDebugMode()
487			]
488		]));
489	}
490
491	/**
492	 * Function is used to sum problems in 2 arrays.
493	 *
494	 * Example:
495	 * $a1 = [1 => 0, 2 => 5, 3 => 10];
496	 * $a2 = [1 => 1, 2 => 2, 3 => 3];
497	 * self::sumArrayValues($a1, $a2); // returns [1 => 1, 2 => 7, 3 => 13]
498	 *
499	 * @param array $a1  Array containing severity as key and number of problems as value.
500	 * @param array $a2  Array containing severity as key and number of problems as value.
501	 *
502	 * @return array  Array containing problems in both arrays summed.
503	 */
504	protected static function sumArrayValues(array $a1, array $a2) {
505		foreach ($a1 as $key => &$value) {
506			$value += $a2[$key];
507		}
508		unset($value);
509
510		return $a1;
511	}
512}
513