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
22abstract class CHostBase extends CApiService {
23
24	public const ACCESS_RULES = [
25		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
26		'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
27		'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
28		'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
29	];
30
31	protected $tableName = 'hosts';
32	protected $tableAlias = 'h';
33
34	/**
35	 * Links the templates to the given hosts.
36	 *
37	 * @param array $templateIds
38	 * @param array $targetIds		an array of host IDs to link the templates to
39	 *
40	 * @return array 	an array of added hosts_templates rows, with 'hostid' and 'templateid' set for each row
41	 */
42	protected function link(array $templateIds, array $targetIds) {
43		if (empty($templateIds)) {
44			return;
45		}
46
47		// permission check
48		$templateIds = array_unique($templateIds);
49
50		$count = API::Template()->get([
51			'countOutput' => true,
52			'templateids' => $templateIds
53		]);
54
55		if ($count != count($templateIds)) {
56			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
57		}
58
59		// check if someone passed duplicate templates in the same query
60		$templateIdDuplicates = zbx_arrayFindDuplicates($templateIds);
61		if (!zbx_empty($templateIdDuplicates)) {
62			$duplicatesFound = [];
63			foreach ($templateIdDuplicates as $value => $count) {
64				$duplicatesFound[] = _s('template ID "%1$s" is passed %2$s times', $value, $count);
65			}
66			self::exception(
67				ZBX_API_ERROR_PARAMETERS,
68				_s('Cannot pass duplicate template IDs for the linkage: %1$s.', implode(', ', $duplicatesFound))
69			);
70		}
71
72		// get DB templates which exists in all targets
73		$res = DBselect('SELECT * FROM hosts_templates WHERE '.dbConditionInt('hostid', $targetIds));
74		$mas = [];
75		while ($row = DBfetch($res)) {
76			if (!isset($mas[$row['templateid']])) {
77				$mas[$row['templateid']] = [];
78			}
79			$mas[$row['templateid']][$row['hostid']] = 1;
80		}
81		$targetIdCount = count($targetIds);
82		$commonDBTemplateIds = [];
83		foreach ($mas as $templateId => $targetList) {
84			if (count($targetList) == $targetIdCount) {
85				$commonDBTemplateIds[] = $templateId;
86			}
87		}
88
89		// check if there are any template with triggers which depends on triggers in templates which will be not linked
90		$commonTemplateIds = array_unique(array_merge($commonDBTemplateIds, $templateIds));
91		foreach ($templateIds as $templateid) {
92			$triggerids = [];
93			$dbTriggers = get_triggers_by_hostid($templateid);
94			while ($trigger = DBfetch($dbTriggers)) {
95				$triggerids[$trigger['triggerid']] = $trigger['triggerid'];
96			}
97
98			$sql = 'SELECT DISTINCT h.host'.
99				' FROM trigger_depends td,functions f,items i,hosts h'.
100				' WHERE ('.
101				dbConditionInt('td.triggerid_down', $triggerids).
102				' AND f.triggerid=td.triggerid_up'.
103				' )'.
104				' AND i.itemid=f.itemid'.
105				' AND h.hostid=i.hostid'.
106				' AND '.dbConditionInt('h.hostid', $commonTemplateIds, true).
107				' AND h.status='.HOST_STATUS_TEMPLATE;
108			if ($dbDepHost = DBfetch(DBselect($sql))) {
109				$tmpTpls = API::Template()->get([
110					'templateids' => $templateid,
111					'output'=> API_OUTPUT_EXTEND
112				]);
113				$tmpTpl = reset($tmpTpls);
114
115				self::exception(ZBX_API_ERROR_PARAMETERS,
116					_s('Trigger in template "%1$s" has dependency with trigger in template "%2$s".', $tmpTpl['host'], $dbDepHost['host']));
117			}
118		}
119
120		$res = DBselect(
121			'SELECT ht.hostid,ht.templateid'.
122				' FROM hosts_templates ht'.
123				' WHERE '.dbConditionInt('ht.hostid', $targetIds).
124				' AND '.dbConditionInt('ht.templateid', $templateIds)
125		);
126		$linked = [];
127		while ($row = DBfetch($res)) {
128			$linked[$row['templateid']][$row['hostid']] = true;
129		}
130
131		// add template linkages, if problems rollback later
132		$hostsLinkageInserts = [];
133
134		foreach ($templateIds as $templateid) {
135			$linked_targets = array_key_exists($templateid, $linked) ? $linked[$templateid] : [];
136
137			foreach ($targetIds as $targetid) {
138				if (array_key_exists($targetid, $linked_targets)) {
139					continue;
140				}
141
142				$hostsLinkageInserts[] = ['hostid' => $targetid, 'templateid' => $templateid];
143			}
144		}
145
146		if ($hostsLinkageInserts) {
147			self::checkCircularLinkage($hostsLinkageInserts);
148			self::checkDoubleLinkage($hostsLinkageInserts);
149
150			DB::insertBatch('hosts_templates', $hostsLinkageInserts);
151		}
152
153		// check if all trigger templates are linked to host.
154		// we try to find template that is not linked to hosts ($targetids)
155		// and exists trigger which reference that template and template from ($templateids)
156		$sql = 'SELECT DISTINCT h.host'.
157			' FROM functions f,items i,triggers t,hosts h'.
158			' WHERE f.itemid=i.itemid'.
159			' AND f.triggerid=t.triggerid'.
160			' AND i.hostid=h.hostid'.
161			' AND h.status='.HOST_STATUS_TEMPLATE.
162			' AND NOT EXISTS (SELECT 1 FROM hosts_templates ht WHERE ht.templateid=i.hostid AND '.dbConditionInt('ht.hostid', $targetIds).')'.
163			' AND EXISTS (SELECT 1 FROM functions ff,items ii WHERE ff.itemid=ii.itemid AND ff.triggerid=t.triggerid AND '.dbConditionInt('ii.hostid', $templateIds). ')';
164		if ($dbNotLinkedTpl = DBfetch(DBSelect($sql, 1))) {
165			self::exception(ZBX_API_ERROR_PARAMETERS,
166				_s('Trigger has items from template "%1$s" that is not linked to host.', $dbNotLinkedTpl['host'])
167			);
168		}
169
170		return $hostsLinkageInserts;
171	}
172
173	protected function unlink($templateids, $targetids = null) {
174		$cond = ['templateid' => $templateids];
175		if (!is_null($targetids)) {
176			$cond['hostid'] =  $targetids;
177		}
178		DB::delete('hosts_templates', $cond);
179
180		if (!is_null($targetids)) {
181			$hosts = API::Host()->get([
182				'hostids' => $targetids,
183				'output' => ['hostid', 'host'],
184				'nopermissions' => true
185			]);
186		}
187		else{
188			$hosts = API::Host()->get([
189				'templateids' => $templateids,
190				'output' => ['hostid', 'host'],
191				'nopermissions' => true
192			]);
193		}
194
195		if (!empty($hosts)) {
196			$templates = API::Template()->get([
197				'templateids' => $templateids,
198				'output' => ['hostid', 'host'],
199				'nopermissions' => true
200			]);
201
202			$hosts = implode(', ', zbx_objectValues($hosts, 'host'));
203			$templates = implode(', ', zbx_objectValues($templates, 'host'));
204
205			info(_s('Templates "%1$s" unlinked from hosts "%2$s".', $templates, $hosts));
206		}
207	}
208
209	/**
210	 * Searches for circular linkages for specific template.
211	 *
212	 * @param array  $links[<templateid>][<hostid>]  The list of linkages.
213	 * @param string $templateid                     ID of the template to check circular linkages.
214	 * @param array  $hostids[<hostid>]
215	 *
216	 * @throws APIException if circular linkage is found.
217	 */
218	private static function checkTemplateCircularLinkage(array $links, $templateid, array $hostids) {
219		if (array_key_exists($templateid, $hostids)) {
220			self::exception(ZBX_API_ERROR_PARAMETERS, _('Circular template linkage is not allowed.'));
221		}
222
223		foreach ($hostids as $hostid => $foo) {
224			if (array_key_exists($hostid, $links)) {
225				self::checkTemplateCircularLinkage($links, $templateid, $links[$hostid]);
226			}
227		}
228	}
229
230	/**
231	 * Searches for circular linkages.
232	 *
233	 * @param array  $host_templates
234	 * @param string $host_templates[]['templateid']
235	 * @param string $host_templates[]['hostid']
236	 */
237	private static function checkCircularLinkage(array $host_templates) {
238		$links = [];
239
240		foreach ($host_templates as $host_template) {
241			$links[$host_template['templateid']][$host_template['hostid']] = true;
242		}
243
244		$templateids = array_keys($links);
245		$_templateids = $templateids;
246
247		do {
248			$result = DBselect(
249				'SELECT ht.templateid,ht.hostid'.
250				' FROM hosts_templates ht'.
251				' WHERE '.dbConditionId('ht.hostid', $_templateids)
252			);
253
254			$_templateids = [];
255
256			while ($row = DBfetch($result)) {
257				if (!array_key_exists($row['templateid'], $links)) {
258					$_templateids[$row['templateid']] = true;
259				}
260
261				$links[$row['templateid']][$row['hostid']] = true;
262			}
263
264			$_templateids = array_keys($_templateids);
265		}
266		while ($_templateids);
267
268		foreach ($templateids as $templateid) {
269			self::checkTemplateCircularLinkage($links, $templateid, $links[$templateid]);
270		}
271	}
272
273	/**
274	 * Searches for double linkages.
275	 *
276	 * @param array  $links[<hostid>][<templateid>]  The list of linked template IDs by host ID.
277	 * @param string $hostid
278	 *
279	 * @throws APIException if double linkage is found.
280	 *
281	 * @return array  An array of the linked templates for the selected host.
282	 */
283	private static function checkTemplateDoubleLinkage(array $links, $hostid) {
284		$templateids = $links[$hostid];
285
286		foreach ($links[$hostid] as $templateid => $foo) {
287			if (array_key_exists($templateid, $links)) {
288				$_templateids = self::checkTemplateDoubleLinkage($links, $templateid);
289
290				if (array_intersect_key($templateids, $_templateids)) {
291					self::exception(ZBX_API_ERROR_PARAMETERS,
292						_('Template cannot be linked to another template more than once even through other templates.')
293					);
294				}
295
296				$templateids += $_templateids;
297			}
298		}
299
300		return $templateids;
301	}
302
303	/**
304	 * Searches for double linkages.
305	 *
306	 * @param array  $host_templates
307	 * @param string $host_templates[]['templateid']
308	 * @param string $host_templates[]['hostid']
309	 */
310	private static function checkDoubleLinkage(array $host_templates) {
311		$links = [];
312		$templateids = [];
313		$hostids = [];
314
315		foreach ($host_templates as $host_template) {
316			$links[$host_template['hostid']][$host_template['templateid']] = true;
317			$templateids[$host_template['templateid']] = true;
318			$hostids[$host_template['hostid']] = true;
319		}
320
321		$_hostids = array_keys($hostids);
322
323		do {
324			$result = DBselect(
325				'SELECT ht.hostid'.
326				' FROM hosts_templates ht'.
327				' WHERE '.dbConditionId('ht.templateid', $_hostids)
328			);
329
330			$_hostids = [];
331
332			while ($row = DBfetch($result)) {
333				if (!array_key_exists($row['hostid'], $hostids)) {
334					$_hostids[$row['hostid']] = true;
335				}
336
337				$hostids[$row['hostid']] = true;
338			}
339
340			$_hostids = array_keys($_hostids);
341		}
342		while ($_hostids);
343
344		$_templateids = array_keys($templateids + $hostids);
345		$templateids = [];
346
347		do {
348			$result = DBselect(
349				'SELECT ht.templateid,ht.hostid'.
350				' FROM hosts_templates ht'.
351				' WHERE '.dbConditionId('hostid', $_templateids)
352			);
353
354			$_templateids = [];
355
356			while ($row = DBfetch($result)) {
357				if (!array_key_exists($row['templateid'], $templateids)) {
358					$_templateids[$row['templateid']] = true;
359				}
360
361				$templateids[$row['templateid']] = true;
362				$links[$row['hostid']][$row['templateid']] = true;
363			}
364
365			$_templateids = array_keys($_templateids);
366		}
367		while ($_templateids);
368
369		foreach ($hostids as $hostid => $foo) {
370			self::checkTemplateDoubleLinkage($links, $hostid);
371		}
372	}
373
374	/**
375	 * Updates tags by deleting existing tags if they are not among the input tags, and adding missing ones.
376	 *
377	 * @param array  $host_tags
378	 * @param int    $host_tags[<hostid>]
379	 * @param string $host_tags[<hostid>][]['tag']
380	 * @param string $host_tags[<hostid>][]['value']
381	 */
382	protected function updateTags(array $host_tags): void {
383		if (!$host_tags) {
384			return;
385		}
386
387		$insert = [];
388		$db_tags = DB::select('host_tag', [
389			'output' => ['hosttagid', 'hostid', 'tag', 'value'],
390			'filter' => ['hostid' => array_keys($host_tags)],
391			'preservekeys' => true
392		]);
393
394		$db_host_tags = [];
395		foreach ($db_tags as $db_tag) {
396			$db_host_tags[$db_tag['hostid']][] = $db_tag;
397		}
398
399		foreach ($host_tags as $hostid => $tags) {
400			foreach (zbx_toArray($tags) as $tag) {
401				if (array_key_exists($hostid, $db_host_tags)) {
402					$tag += ['value' => ''];
403
404					foreach ($db_host_tags[$hostid] as $db_tag) {
405						if ($tag['tag'] === $db_tag['tag'] && $tag['value'] === $db_tag['value']) {
406							unset($db_tags[$db_tag['hosttagid']]);
407							$tag = null;
408							break;
409						}
410					}
411				}
412
413				if ($tag !== null) {
414					$insert[] = ['hostid' => $hostid] + $tag;
415				}
416			}
417		}
418
419		if ($db_tags) {
420			DB::delete('host_tag', ['hosttagid' => array_keys($db_tags)]);
421		}
422
423		if ($insert) {
424			DB::insert('host_tag', $insert);
425		}
426	}
427
428	/**
429	 * Creates user macros for hosts, templates and host prototypes.
430	 *
431	 * @param array  $hosts
432	 * @param array  $hosts[]['templateid|hostid']
433	 * @param array  $hosts[]['macros']             (optional)
434	 */
435	protected function createHostMacros(array $hosts): void {
436		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';
437
438		$ins_hostmacros = [];
439
440		foreach ($hosts as $host) {
441			if (array_key_exists('macros', $host)) {
442				foreach ($host['macros'] as $macro) {
443					$ins_hostmacros[] = ['hostid' => $host[$id_field_name]] + $macro;
444				}
445			}
446		}
447
448		if ($ins_hostmacros) {
449			DB::insert('hostmacro', $ins_hostmacros);
450		}
451	}
452
453	/**
454	 * Adding "macros" to the each host object.
455	 *
456	 * @param array  $db_hosts
457	 *
458	 * @return array
459	 */
460	protected function getHostMacros(array $db_hosts): array {
461		foreach ($db_hosts as &$db_host) {
462			$db_host['macros'] = [];
463		}
464		unset($db_host);
465
466		$options = [
467			'output' => ['hostmacroid', 'hostid', 'macro', 'type', 'value', 'description'],
468			'filter' => ['hostid' => array_keys($db_hosts)]
469		];
470		$db_macros = DBselect(DB::makeSql('hostmacro', $options));
471
472		while ($db_macro = DBfetch($db_macros)) {
473			$hostid = $db_macro['hostid'];
474			unset($db_macro['hostid']);
475
476			$db_hosts[$hostid]['macros'][$db_macro['hostmacroid']] = $db_macro;
477		}
478
479		return $db_hosts;
480	}
481
482	/**
483	 * Checks user macros for host.update, template.update and hostprototype.update methods.
484	 *
485	 * @param array  $hosts
486	 * @param array  $hosts[]['templateid|hostid']
487	 * @param array  $hosts[]['macros']             (optional)
488	 * @param array  $db_hosts
489	 * @param array  $db_hosts[<hostid>]['macros']
490	 *
491	 * @return array Array of passed hosts/templates with padded macros data, when it's necessary.
492	 *
493	 * @throws APIException if input of host macros data is invalid.
494	 */
495	protected function validateHostMacros(array $hosts, array $db_hosts): array {
496		$hostmacro_defaults = [
497			'type' => DB::getDefault('hostmacro', 'type')
498		];
499
500		// Populating new host macro objects for correct inheritance.
501		if ($this instanceof CHostPrototype) {
502			$hostmacro_defaults['description'] = DB::getDefault('hostmacro', 'description');
503		}
504
505		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';
506
507		foreach ($hosts as $i1 => &$host) {
508			if (!array_key_exists('macros', $host)) {
509				continue;
510			}
511
512			$db_host = $db_hosts[$host[$id_field_name]];
513			$path = '/'.($i1 + 1).'/macros';
514
515			$db_macros = array_column($db_host['macros'], 'hostmacroid', 'macro');
516			$macros = [];
517
518			foreach ($host['macros'] as $i2 => &$hostmacro) {
519				if (!array_key_exists('hostmacroid', $hostmacro)) {
520					foreach (['macro', 'value'] as $field_name) {
521						if (!array_key_exists($field_name, $hostmacro)) {
522							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path,
523								_s('the parameter "%1$s" is missing', $field_name)
524							));
525						}
526					}
527
528					$hostmacro += $hostmacro_defaults;
529				}
530				else {
531					if (!array_key_exists($hostmacro['hostmacroid'], $db_host['macros'])) {
532						self::exception(ZBX_API_ERROR_PERMISSIONS,
533							_('No permissions to referred object or it does not exist!')
534						);
535					}
536
537					$db_hostmacro = $db_host['macros'][$hostmacro['hostmacroid']];
538					$hostmacro += array_intersect_key($db_hostmacro, array_flip(['macro', 'type']));
539
540					if ($hostmacro['type'] != $db_hostmacro['type']) {
541						if ($db_hostmacro['type'] == ZBX_MACRO_TYPE_SECRET) {
542							$hostmacro += ['value' => ''];
543						}
544
545						if ($hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
546							$hostmacro += ['value' => $db_hostmacro['value']];
547						}
548					}
549
550					// Populating new host macro objects for correct inheritance.
551					if ($this instanceof CHostPrototype) {
552						$hostmacro += array_intersect_key($db_hostmacro, array_flip(['value', 'description']));
553					}
554
555					$macros[$hostmacro['hostmacroid']] = $hostmacro['macro'];
556				}
557
558				if (array_key_exists('value', $hostmacro) && $hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
559					if (!CApiInputValidator::validate(['type' => API_VAULT_SECRET], $hostmacro['value'],
560							$path.'/'.($i2 + 1).'/value', $error)) {
561						self::exception(ZBX_API_ERROR_PARAMETERS, $error);
562					}
563				}
564			}
565			unset($hostmacro);
566
567			// Checking for cross renaming of existing macros.
568			foreach ($macros as $hostmacroid => $macro) {
569				if (array_key_exists($macro, $db_macros) && bccomp($hostmacroid, $db_macros[$macro]) != 0
570						&& array_key_exists($db_macros[$macro], $macros)) {
571					$hosts = DB::select('hosts', [
572						'output' => ['name'],
573						'hostids' => $host[$id_field_name]
574					]);
575
576					self::exception(ZBX_API_ERROR_PARAMETERS,
577						_s('Macro "%1$s" already exists on "%2$s".', $macro, $hosts[0]['name'])
578					);
579				}
580			}
581
582			$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['macro']], 'fields' => [
583				'macro' =>	['type' => API_USER_MACRO]
584			]];
585
586			if (!CApiInputValidator::validateUniqueness($api_input_rules, $host['macros'], $path, $error)) {
587				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
588			}
589		}
590		unset($host);
591
592		return $hosts;
593	}
594
595	/**
596	 * Updates user macros for hosts, templates and host prototypes.
597	 *
598	 * @param array  $hosts
599	 * @param array  $hosts[]['templateid|hostid']
600	 * @param array  $hosts[]['macros']             (optional)
601	 * @param array  $db_hosts
602	 * @param array  $db_hosts[<hostid>]['macros']  An array of host macros indexed by hostmacroid.
603	 */
604	protected function updateHostMacros(array $hosts, array $db_hosts): void {
605		$id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid';
606
607		$ins_hostmacros = [];
608		$upd_hostmacros = [];
609		$del_hostmacroids = [];
610
611		foreach ($hosts as $host) {
612			if (!array_key_exists('macros', $host)) {
613				continue;
614			}
615
616			$db_host = $db_hosts[$host[$id_field_name]];
617
618			foreach ($host['macros'] as $hostmacro) {
619				if (array_key_exists('hostmacroid', $hostmacro)) {
620					$db_hostmacro = $db_host['macros'][$hostmacro['hostmacroid']];
621					unset($db_host['macros'][$hostmacro['hostmacroid']]);
622
623					$upd_hostmacro = DB::getUpdatedValues('hostmacro', $hostmacro, $db_hostmacro);
624
625					if ($upd_hostmacro) {
626						$upd_hostmacros[] = [
627							'values' => $upd_hostmacro,
628							'where' => ['hostmacroid' => $hostmacro['hostmacroid']]
629						];
630					}
631				}
632				else {
633					$ins_hostmacros[] = $hostmacro + ['hostid' => $host[$id_field_name]];
634				}
635			}
636
637			$del_hostmacroids = array_merge($del_hostmacroids, array_keys($db_host['macros']));
638		}
639
640		if ($del_hostmacroids) {
641			DB::delete('hostmacro', ['hostmacroid' => $del_hostmacroids]);
642		}
643
644		if ($upd_hostmacros) {
645			DB::update('hostmacro', $upd_hostmacros);
646		}
647
648		if ($ins_hostmacros) {
649			DB::insert('hostmacro', $ins_hostmacros);
650		}
651	}
652
653	/**
654	 * Retrieves and adds additional requested data to the result set.
655	 *
656	 * @param array  $options
657	 * @param array  $result
658	 *
659	 * @return array
660	 */
661	protected function addRelatedObjects(array $options, array $result) {
662		$result = parent::addRelatedObjects($options, $result);
663
664		$hostids = array_keys($result);
665
666		// adding macros
667		if ($options['selectMacros'] !== null && $options['selectMacros'] !== API_OUTPUT_COUNT) {
668			$macros = API::UserMacro()->get([
669				'output' => $this->outputExtend($options['selectMacros'], ['hostid', 'hostmacroid']),
670				'hostids' => $hostids,
671				'preservekeys' => true,
672				'nopermissions' => true
673			]);
674
675			$relationMap = $this->createRelationMap($macros, 'hostid', 'hostmacroid');
676			$macros = $this->unsetExtraFields($macros, ['hostid', 'hostmacroid'], $options['selectMacros']);
677			$result = $relationMap->mapMany($result, $macros, 'macros',
678				array_key_exists('limitSelects', $options) ? $options['limitSelects'] : null
679			);
680		}
681
682		return $result;
683	}
684}
685