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
22/**
23 * Class containing methods for operations with user macro.
24 */
25class CUserMacro extends CApiService {
26
27	public const ACCESS_RULES = [
28		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
29		'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
30		'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
31		'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
32		'createglobal' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
33		'updateglobal' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
34		'deleteglobal' => ['min_user_type' => USER_TYPE_SUPER_ADMIN]
35	];
36
37	protected $tableName = 'hostmacro';
38	protected $tableAlias = 'hm';
39	protected $sortColumns = ['macro'];
40
41	/**
42	 * Get UserMacros data.
43	 *
44	 * @param array $options
45	 * @param array $options['groupids'] usermacrosgroup ids
46	 * @param array $options['hostids'] host ids
47	 * @param array $options['hostmacroids'] host macros ids
48	 * @param array $options['globalmacroids'] global macros ids
49	 * @param array $options['templateids'] template ids
50	 * @param boolean $options['globalmacro'] only global macros
51	 * @param boolean $options['selectGroups'] select groups
52	 * @param boolean $options['selectHosts'] select hosts
53	 * @param boolean $options['selectTemplates'] select templates
54	 *
55	 * @return array|boolean UserMacros data as array or false if error
56	 */
57	public function get($options = []) {
58		$result = [];
59		$userid = self::$userData['userid'];
60
61		$sqlParts = [
62			'select'	=> ['macros' => 'hm.hostmacroid'],
63			'from'		=> ['hostmacro hm'],
64			'where'		=> [],
65			'order'		=> [],
66			'limit'		=> null
67		];
68
69		$sqlPartsGlobal = [
70			'select'	=> ['macros' => 'gm.globalmacroid'],
71			'from'		=> ['globalmacro gm'],
72			'where'		=> [],
73			'order'		=> [],
74			'limit'		=> null
75		];
76
77		$defOptions = [
78			'groupids'					=> null,
79			'hostids'					=> null,
80			'hostmacroids'				=> null,
81			'globalmacroids'			=> null,
82			'templateids'				=> null,
83			'globalmacro'				=> null,
84			'inherited'					=> null,
85			'editable'					=> false,
86			'nopermissions'				=> null,
87			// filter
88			'filter'					=> null,
89			'search'					=> null,
90			'searchByAny'				=> null,
91			'startSearch'				=> false,
92			'excludeSearch'				=> false,
93			'searchWildcardsEnabled'	=> null,
94			// output
95			'output'					=> API_OUTPUT_EXTEND,
96			'selectGroups'				=> null,
97			'selectHosts'				=> null,
98			'selectTemplates'			=> null,
99			'countOutput'				=> false,
100			'preservekeys'				=> false,
101			'sortfield'					=> '',
102			'sortorder'					=> '',
103			'limit'						=> null
104		];
105		$options = zbx_array_merge($defOptions, $options);
106
107		// editable + PERMISSION CHECK
108		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
109			if ($options['editable'] && !is_null($options['globalmacro'])) {
110				return [];
111			}
112			else {
113				$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
114
115				$userGroups = getUserGroupsByUserId($userid);
116
117				$sqlParts['where'][] = 'EXISTS ('.
118						'SELECT NULL'.
119						' FROM hosts_groups hgg'.
120							' JOIN rights r'.
121								' ON r.id=hgg.groupid'.
122									' AND '.dbConditionInt('r.groupid', $userGroups).
123						' WHERE hm.hostid=hgg.hostid'.
124						' GROUP BY hgg.hostid'.
125						' HAVING MIN(r.permission)>'.PERM_DENY.
126							' AND MAX(r.permission)>='.zbx_dbstr($permission).
127						')';
128			}
129		}
130
131		// global macro
132		if (!is_null($options['globalmacro'])) {
133			$options['groupids'] = null;
134			$options['hostmacroids'] = null;
135			$options['triggerids'] = null;
136			$options['hostids'] = null;
137			$options['itemids'] = null;
138			$options['selectGroups'] = null;
139			$options['selectTemplates'] = null;
140			$options['selectHosts'] = null;
141			$options['inherited'] = null;
142		}
143
144		// globalmacroids
145		if (!is_null($options['globalmacroids'])) {
146			zbx_value2array($options['globalmacroids']);
147			$sqlPartsGlobal['where'][] = dbConditionInt('gm.globalmacroid', $options['globalmacroids']);
148		}
149
150		// hostmacroids
151		if (!is_null($options['hostmacroids'])) {
152			zbx_value2array($options['hostmacroids']);
153			$sqlParts['where'][] = dbConditionInt('hm.hostmacroid', $options['hostmacroids']);
154		}
155
156		// inherited
157		if (!is_null($options['inherited'])) {
158			$sqlParts['from']['hosts'] = 'hosts h';
159			$sqlParts['where'][] = $options['inherited'] ? 'h.templateid IS NOT NULL' : 'h.templateid IS NULL';
160			$sqlParts['where']['hmh'] = 'hm.hostid=h.hostid';
161		}
162
163		// groupids
164		if (!is_null($options['groupids'])) {
165			zbx_value2array($options['groupids']);
166
167			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
168			$sqlParts['where'][] = dbConditionInt('hg.groupid', $options['groupids']);
169			$sqlParts['where']['hgh'] = 'hg.hostid=hm.hostid';
170		}
171
172		// hostids
173		if (!is_null($options['hostids'])) {
174			zbx_value2array($options['hostids']);
175
176			$sqlParts['where'][] = dbConditionInt('hm.hostid', $options['hostids']);
177		}
178
179		// templateids
180		if (!is_null($options['templateids'])) {
181			zbx_value2array($options['templateids']);
182
183			$sqlParts['from']['macros_templates'] = 'hosts_templates ht';
184			$sqlParts['where'][] = dbConditionInt('ht.templateid', $options['templateids']);
185			$sqlParts['where']['hht'] = 'hm.hostid=ht.hostid';
186		}
187
188		// sorting
189		$sqlParts = $this->applyQuerySortOptions('hostmacro', 'hm', $options, $sqlParts);
190		$sqlPartsGlobal = $this->applyQuerySortOptions('globalmacro', 'gm', $options, $sqlPartsGlobal);
191
192		// limit
193		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
194			$sqlParts['limit'] = $options['limit'];
195			$sqlPartsGlobal['limit'] = $options['limit'];
196		}
197
198		// init GLOBALS
199		if (!is_null($options['globalmacro'])) {
200			$sqlPartsGlobal = $this->applyQueryFilterOptions('globalmacro', 'gm', $options, $sqlPartsGlobal);
201			$sqlPartsGlobal = $this->applyQueryOutputOptions('globalmacro', 'gm', $options, $sqlPartsGlobal);
202			$res = DBselect(self::createSelectQueryFromParts($sqlPartsGlobal), $sqlPartsGlobal['limit']);
203			while ($macro = DBfetch($res)) {
204				if ($options['countOutput']) {
205					$result = $macro['rowscount'];
206				}
207				else {
208					$result[$macro['globalmacroid']] = $macro;
209				}
210			}
211		}
212		// init HOSTS
213		else {
214			$sqlParts = $this->applyQueryFilterOptions('hostmacro', 'hm', $options, $sqlParts);
215			$sqlParts = $this->applyQueryOutputOptions('hostmacro', 'hm', $options, $sqlParts);
216			$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
217			while ($macro = DBfetch($res)) {
218				if ($options['countOutput']) {
219					$result = $macro['rowscount'];
220				}
221				else {
222					$result[$macro['hostmacroid']] = $macro;
223				}
224			}
225		}
226
227		if ($options['countOutput']) {
228			return $result;
229		}
230
231		if ($result) {
232			$result = $this->addRelatedObjects($options, $result);
233			$result = $this->unsetExtraFields($result, ['hostid', 'type'], $options['output']);
234		}
235
236		// removing keys (hash -> array)
237		if (!$options['preservekeys']) {
238			$result = zbx_cleanHashes($result);
239		}
240
241		return $result;
242	}
243
244	/**
245	 * @param array $globalmacros
246	 *
247	 * @return array
248	 */
249	public function createGlobal(array $globalmacros) {
250		$this->validateCreateGlobal($globalmacros);
251
252		$globalmacroids = DB::insertBatch('globalmacro', $globalmacros);
253
254		foreach ($globalmacros as $index => &$globalmacro) {
255			$globalmacro['globalmacroid'] = $globalmacroids[$index];
256		}
257		unset($globalmacro);
258
259		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_MACRO, $globalmacros);
260
261		return ['globalmacroids' => $globalmacroids];
262	}
263
264	/**
265	 * @param array $globalmacros
266	 *
267	 * @throws APIException if the input is invalid.
268	 */
269	private function validateCreateGlobal(array &$globalmacros) {
270		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
271			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
272		}
273
274		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['macro']], 'fields' => [
275			'macro' =>			['type' => API_USER_MACRO, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('globalmacro', 'macro')],
276			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT]), 'default' => ZBX_MACRO_TYPE_TEXT],
277			'value' =>			['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
278									['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('globalmacro', 'value')],
279									['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_VAULT])], 'type' => API_VAULT_SECRET, 'length' => DB::getFieldLength('globalmacro', 'value')]
280			]],
281			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('globalmacro', 'description')]
282		]];
283
284		if (!CApiInputValidator::validate($api_input_rules, $globalmacros, '/', $error)) {
285			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
286		}
287
288		$this->checkDuplicates($globalmacros);
289	}
290
291	/**
292	 * @param array $globalmacros
293	 *
294	 * @return array
295	 */
296	public function updateGlobal(array $globalmacros) {
297		$this->validateUpdateGlobal($globalmacros, $db_globalmacros);
298
299		$upd_globalmacros = [];
300
301		foreach ($globalmacros as $globalmacro) {
302			$db_globalmacro = $db_globalmacros[$globalmacro['globalmacroid']];
303
304			$upd_globalmacro = DB::getUpdatedValues('globalmacro', $globalmacro, $db_globalmacro);
305
306			if ($upd_globalmacro) {
307				$upd_globalmacros[] = [
308					'values'=> $upd_globalmacro,
309					'where'=> ['globalmacroid' => $globalmacro['globalmacroid']]
310				];
311			}
312		}
313
314		if ($upd_globalmacros) {
315			DB::update('globalmacro', $upd_globalmacros);
316		}
317
318		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_MACRO, $globalmacros, $db_globalmacros);
319
320		return ['globalmacroids' => array_column($globalmacros, 'globalmacroid')];
321	}
322
323	/**
324	 * @param array $globalmacros
325	 * @param array $db_globalmacros
326	 *
327	 * @throws APIException if the input is invalid
328	 */
329	private function validateUpdateGlobal(array &$globalmacros, array &$db_globalmacros = null) {
330		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
331			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
332		}
333
334		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['globalmacroid'], ['macro']], 'fields' => [
335			'globalmacroid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
336			'macro' =>			['type' => API_USER_MACRO, 'length' => DB::getFieldLength('globalmacro', 'macro')],
337			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT])],
338			'value' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('globalmacro', 'value')],
339			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('globalmacro', 'description')]
340		]];
341
342		if (!CApiInputValidator::validate($api_input_rules, $globalmacros, '/', $error)) {
343			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
344		}
345
346		$db_globalmacros = DB::select('globalmacro', [
347			'output' => ['globalmacroid', 'macro', 'value', 'description', 'type'],
348			'globalmacroids' => array_column($globalmacros, 'globalmacroid'),
349			'preservekeys' => true
350		]);
351
352		if (count($globalmacros) != count($db_globalmacros)) {
353			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
354		}
355
356		$globalmacros = $this->extendObjectsByKey($globalmacros, $db_globalmacros, 'globalmacroid', ['type']);
357
358		foreach ($globalmacros as $index => &$globalmacro) {
359			$db_globalmacro = $db_globalmacros[$globalmacro['globalmacroid']];
360
361			if ($globalmacro['type'] != $db_globalmacro['type']) {
362				if ($db_globalmacro['type'] == ZBX_MACRO_TYPE_SECRET) {
363					$globalmacro += ['value' => ''];
364				}
365
366				if ($globalmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
367					$globalmacro += ['value' => $db_globalmacro['value']];
368				}
369			}
370
371			if (array_key_exists('value', $globalmacro) && $globalmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
372				if (!CApiInputValidator::validate(['type' => API_VAULT_SECRET], $globalmacro['value'],
373						'/'.($index + 1).'/value', $error)) {
374					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
375				}
376			}
377		}
378		unset($globalmacro);
379
380		$this->checkDuplicates($globalmacros, $db_globalmacros);
381	}
382
383	/**
384	 * Check for duplicated macros.
385	 *
386	 * @param array      $globalmacros
387	 * @param string     $globalmacros[]['globalmacroid']  (optional if $db_globalmacros is null)
388	 * @param string     $globalmacros[]['macro']          (optional if $db_globalmacros is not null)
389	 * @param array|null $db_globalmacros
390	 *
391	 * @throws APIException if macros already exists.
392	 */
393	private function checkDuplicates(array $globalmacros, array $db_globalmacros = null): void {
394		$macros = [];
395
396		foreach ($globalmacros as $globalmacro) {
397			if ($db_globalmacros === null || (array_key_exists('macro', $globalmacro)
398					&& CApiInputValidator::trimMacro($globalmacro['macro'])
399						!== CApiInputValidator::trimMacro($db_globalmacros[$globalmacro['globalmacroid']]['macro']))) {
400				$macros[] = $globalmacro['macro'];
401			}
402		}
403
404		if (!$macros) {
405			return;
406		}
407
408		$db_globalmacros = DB::select('globalmacro', [
409			'output' => ['macro']
410		]);
411
412		$db_macros = [];
413
414		foreach ($db_globalmacros as $db_globalmacro) {
415			$db_macros[CApiInputValidator::trimMacro($db_globalmacro['macro'])] = true;
416		}
417
418		foreach ($macros as $macro) {
419			if (array_key_exists(CApiInputValidator::trimMacro($macro), $db_macros)) {
420				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Macro "%1$s" already exists.', $macro));
421			}
422		}
423	}
424
425	/**
426	 * @param array $globalmacroids
427	 *
428	 * @return array
429	 */
430	public function deleteGlobal(array $globalmacroids) {
431		$this->validateDeleteGlobal($globalmacroids, $db_globalmacros);
432
433		DB::delete('globalmacro', ['globalmacroid' => $globalmacroids]);
434
435		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_MACRO, $db_globalmacros);
436
437		return ['globalmacroids' => $globalmacroids];
438	}
439
440	/**
441	 * @param array $globalmacroids
442	 *
443	 * @throws APIException if the input is invalid.
444	 */
445	private function validateDeleteGlobal(array &$globalmacroids, array &$db_globalmacros = null) {
446		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
447			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
448		}
449
450		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
451
452		if (!CApiInputValidator::validate($api_input_rules, $globalmacroids, '/', $error)) {
453			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
454		}
455
456		$db_globalmacros = DB::select('globalmacro', [
457			'output' => ['globalmacroid', 'macro'],
458			'globalmacroids' => $globalmacroids,
459			'preservekeys' => true
460		]);
461
462		if (count($globalmacroids) != count($db_globalmacros)) {
463			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
464		}
465	}
466
467	/**
468	 * @param array $hostmacros
469	 *
470	 * @throws APIException if the input is invalid.
471	 */
472	protected function validateCreate(array &$hostmacros) {
473		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['hostid', 'macro']], 'fields' => [
474			'hostid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
475			'macro' =>			['type' => API_USER_MACRO, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('hostmacro', 'macro')],
476			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT]), 'default' => ZBX_MACRO_TYPE_TEXT],
477			'value' =>			['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
478									['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')],
479									['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_VAULT])], 'type' => API_VAULT_SECRET, 'length' => DB::getFieldLength('hostmacro', 'value')]
480			]],
481			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')]
482		]];
483
484		if (!CApiInputValidator::validate($api_input_rules, $hostmacros, '/', $error)) {
485			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
486		}
487
488		$this->checkHostPermissions(array_unique(array_column($hostmacros, 'hostid')));
489		$this->checkHostDuplicates($hostmacros);
490	}
491
492	/**
493	 * @param array $hostmacros
494	 *
495	 * @return array
496	 */
497	public function create(array $hostmacros) {
498		$this->validateCreate($hostmacros);
499
500		$this->createReal($hostmacros);
501
502		if ($tpl_hostmacros = $this->getMacrosToInherit($hostmacros)) {
503			$this->inherit($tpl_hostmacros);
504		}
505
506		return ['hostmacroids' => array_column($hostmacros, 'hostmacroid')];
507	}
508
509	/**
510	 * Inserts hostmacros records into the database.
511	 *
512	 * @param array $hostmacros
513	 */
514	private function createReal(array &$hostmacros): void {
515		$hostmacroids = DB::insert('hostmacro', $hostmacros);
516
517		foreach ($hostmacros as $index => &$hostmacro) {
518			$hostmacro['hostmacroid'] = $hostmacroids[$index];
519		}
520		unset($hostmacro);
521	}
522
523	/**
524	 * @param array $hostmacros
525	 *
526	 * @throws APIException if the input is invalid.
527	 */
528	protected function validateUpdate(array &$hostmacros, array &$db_hostmacros = null) {
529		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['hostmacroid']], 'fields' => [
530			'hostmacroid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
531			'macro' =>			['type' => API_USER_MACRO, 'length' => DB::getFieldLength('hostmacro', 'macro')],
532			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT])],
533			'value' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')],
534			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')]
535		]];
536
537		if (!CApiInputValidator::validate($api_input_rules, $hostmacros, '/', $error)) {
538			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
539		}
540
541		$db_hostmacros = $this->get([
542			'output' => ['hostmacroid', 'hostid', 'macro', 'type', 'description'],
543			'hostmacroids' => array_column($hostmacros, 'hostmacroid'),
544			'editable' => true,
545			'inherited' => false,
546			'preservekeys' => true
547		]);
548
549		if (count($hostmacros) != count($db_hostmacros)) {
550			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
551		}
552
553		// CUserMacro::get does not return secret values. Loading directly from the database.
554		$options = [
555			'output' => ['hostmacroid', 'value'],
556			'hostmacroids' => array_keys($db_hostmacros)
557		];
558		$db_hostmacro_values = DBselect(DB::makeSql('hostmacro', $options));
559
560		while ($db_hostmacro_value = DBfetch($db_hostmacro_values)) {
561			$db_hostmacros[$db_hostmacro_value['hostmacroid']] += $db_hostmacro_value;
562		}
563
564		$hostmacros = $this->extendObjectsByKey($hostmacros, $db_hostmacros, 'hostmacroid', ['hostid', 'type']);
565
566		foreach ($hostmacros as $index => &$hostmacro) {
567			$db_hostmacro = $db_hostmacros[$hostmacro['hostmacroid']];
568
569			if ($hostmacro['type'] != $db_hostmacro['type']) {
570				if ($db_hostmacro['type'] == ZBX_MACRO_TYPE_SECRET) {
571					$hostmacro += ['value' => ''];
572				}
573
574				if ($hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
575					$hostmacro += ['value' => $db_hostmacro['value']];
576				}
577			}
578
579			if (array_key_exists('value', $hostmacro) && $hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) {
580				if (!CApiInputValidator::validate(['type' => API_VAULT_SECRET], $hostmacro['value'],
581						'/'.($index + 1).'/value', $error)) {
582					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
583				}
584			}
585		}
586		unset($hostmacro);
587
588		$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['hostid', 'macro']], 'fields' => [
589			'hostid' =>	['type' => API_ID],
590			'macro' =>	['type' => API_USER_MACRO]
591		]];
592
593		if (!CApiInputValidator::validateUniqueness($api_input_rules, $hostmacros, '/', $error)) {
594			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
595		}
596
597		$this->checkHostDuplicates($hostmacros, $db_hostmacros);
598	}
599
600	/**
601	 * Checks if any of the given host macros already exist on the corresponding hosts. If the macros are updated and
602	 * the "hostmacroid" field is set, the method will only fail, if a macro with a different hostmacroid exists.
603	 * Assumes the "macro", "hostid" and "hostmacroid" fields are valid.
604	 *
605	 * @param array      $hostmacros
606	 * @param string     $hostmacros[]['hostmacroid']  (optional if $db_hostmacros is null)
607	 * @param string     $hostmacros[]['hostid']
608	 * @param string     $hostmacros[]['macro']        (optional if $db_hostmacros is not null)
609	 * @param array|null $db_hostmacros
610	 *
611	 * @throws APIException if any of the given macros already exist.
612	 */
613	private function checkHostDuplicates(array $hostmacros, array $db_hostmacros = null) {
614		$macro_names = [];
615		$existing_macros = [];
616
617		// Parse each macro, get unique names and, if context exists, narrow down the search.
618		foreach ($hostmacros as $index => $hostmacro) {
619			if ($db_hostmacros !== null && (!array_key_exists('macro', $hostmacro)
620					|| CApiInputValidator::trimMacro($hostmacro['macro'])
621						=== CApiInputValidator::trimMacro($db_hostmacros[$hostmacro['hostmacroid']]['macro']))) {
622				unset($hostmacros[$index]);
623
624				continue;
625			}
626
627			$trimmed_macro = CApiInputValidator::trimMacro($hostmacro['macro']);
628			[$macro_name] = explode(':', $trimmed_macro, 2);
629			$macro_name = !isset($trimmed_macro[strlen($macro_name)]) ? '{$'.$macro_name : '{$'.$macro_name.':';
630
631			$macro_names[$macro_name] = true;
632			$existing_macros[$hostmacro['hostid']] = [];
633		}
634
635		if (!$existing_macros) {
636			return;
637		}
638
639		$options = [
640			'output' => ['hostmacroid', 'hostid', 'macro'],
641			'filter' => ['hostid' => array_keys($existing_macros)],
642			'search' => ['macro' => array_keys($macro_names)],
643			'searchByAny' => true,
644			'startSearch' => true
645		];
646
647		$db_hostmacros = DBselect(DB::makeSql('hostmacro', $options));
648
649		// Collect existing unique macro names and their contexts for each host.
650		while ($db_hostmacro = DBfetch($db_hostmacros)) {
651			$trimmed_macro = CApiInputValidator::trimMacro($db_hostmacro['macro']);
652
653			$existing_macros[$db_hostmacro['hostid']][$trimmed_macro] = $db_hostmacro['hostmacroid'];
654		}
655
656		// Compare each macro name and context to existing one.
657		foreach ($hostmacros as $hostmacro) {
658			$hostid = $hostmacro['hostid'];
659			$trimmed_macro = CApiInputValidator::trimMacro($hostmacro['macro']);
660
661			if (array_key_exists($trimmed_macro, $existing_macros[$hostid])) {
662				$hosts = DB::select('hosts', [
663					'output' => ['name'],
664					'hostids' => $hostid
665				]);
666
667				self::exception(ZBX_API_ERROR_PARAMETERS,
668					_s('Macro "%1$s" already exists on "%2$s".', $hostmacro['macro'], $hosts[0]['name'])
669				);
670			}
671		}
672	}
673
674	/**
675	 * Update host macros.
676	 *
677	 * @param array $hostmacros an array of host macros
678	 *
679	 * @return array
680	 */
681	public function update($hostmacros) {
682		$this->validateUpdate($hostmacros, $db_hostmacros);
683
684		$this->updateReal($hostmacros, $db_hostmacros);
685
686		if ($tpl_hostmacros = $this->getMacrosToInherit($hostmacros, $db_hostmacros)) {
687			$this->inherit($tpl_hostmacros);
688		}
689
690		return ['hostmacroids' => array_column($hostmacros, 'hostmacroid')];
691	}
692
693	/**
694	 * Updates hostmacros records in the database.
695	 *
696	 * @param array $hostmacros
697	 * @param array $db_hostmacros
698	 */
699	private function updateReal(array $hostmacros, array $db_hostmacros) {
700		$upd_hostmacros = [];
701
702		foreach ($hostmacros as $hostmacro) {
703			$db_hostmacro = $db_hostmacros[$hostmacro['hostmacroid']];
704
705			$upd_hostmacro = DB::getUpdatedValues('hostmacro', $hostmacro, $db_hostmacro);
706
707			if ($upd_hostmacro) {
708				$upd_hostmacros[] = [
709					'values' => $upd_hostmacro,
710					'where' => ['hostmacroid' => $hostmacro['hostmacroid']]
711				];
712			}
713		}
714
715		if ($upd_hostmacros) {
716			DB::update('hostmacro', $upd_hostmacros);
717		}
718	}
719
720	/**
721	 * @param array $hostmacroids
722	 * @param array $db_hostmacros
723	 *
724	 * @throws APIException if the input is invalid.
725	 */
726	protected function validateDelete(array &$hostmacroids, array &$db_hostmacros = null) {
727		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
728
729		if (!CApiInputValidator::validate($api_input_rules, $hostmacroids, '/', $error)) {
730			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
731		}
732
733		$db_hostmacros = $this->get([
734			'output' => ['hostmacroid', 'hostid', 'macro'],
735			'hostmacroids' => $hostmacroids,
736			'editable' => true,
737			'inherited' => false,
738			'preservekeys' => true
739		]);
740
741		if (count($hostmacroids) != count($db_hostmacros)) {
742			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
743		}
744	}
745
746	/**
747	 * Remove macros from hosts.
748	 *
749	 * @param array $hostmacroids
750	 *
751	 * @return array
752	 */
753	public function delete(array $hostmacroids) {
754		$this->validateDelete($hostmacroids, $db_hostmacros);
755
756		DB::delete('hostmacro', ['hostmacroid' => $hostmacroids]);
757
758		if ($tpl_hostmacros = $this->getMacrosToInherit($db_hostmacros)) {
759			$this->inherit($tpl_hostmacros, true);
760		}
761
762		return ['hostmacroids' => $hostmacroids];
763	}
764
765	/**
766	 * Checks if the current user has access to the given hosts and templates. Assumes the "hostid" field is valid.
767	 *
768	 * @param array $hostids  An array of host or template IDs.
769	 *
770	 * @throws APIException if the user doesn't have write permissions for the given hosts.
771	 */
772	protected function checkHostPermissions(array $hostids) {
773		$count = API::Host()->get([
774			'countOutput' => true,
775			'hostids' => $hostids,
776			'filter' => [
777				'flags' => ZBX_FLAG_DISCOVERY_NORMAL
778			],
779			'editable' => true
780		]);
781
782		if ($count == count($hostids)) {
783			return;
784		}
785
786		$count += API::Template()->get([
787			'countOutput' => true,
788			'templateids' => $hostids,
789			'editable' => true
790		]);
791
792		if ($count == count($hostids)) {
793			return;
794		}
795
796		$count += API::HostPrototype()->get([
797			'countOutput' => true,
798			'hostids' => $hostids,
799			'editable' => true,
800			'inherited' => false
801		]);
802
803		if ($count != count($hostids)) {
804			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
805		}
806	}
807
808	/**
809	 * Forms the array of hostmacros, which are support the inheritance, from the passed hostmacros array.
810	 *
811	 * @param array      $hostmacros
812	 * @param string     $hostmacros[]['hostmacroid']
813	 * @param string     $hostmacros[]['hostid']
814	 * @param string     $hostmacros[]['macro']                  (optional)
815	 * @param string     $hostmacros[]['value']                  (optional)
816	 * @param string     $hostmacros[]['description']            (optional)
817	 * @param int        $hostmacros[]['type']                   (optional)
818	 * @param array|null $db_hostmacros                          Used to set the old macro name in case when it was
819	 *                                                           updated.
820	 * @param string     $db_hostmacros[<hostmacroid>]['macro']
821	 *
822	 * @return array
823	 */
824	private function getMacrosToInherit(array $hostmacros, array $db_hostmacros = null): array {
825		$templated_host_prototypeids = DBfetchColumn(DBselect(
826			'SELECT hd.hostid'.
827			' FROM host_discovery hd,items i,hosts h'.
828			' WHERE hd.parent_itemid=i.itemid'.
829				' AND i.hostid=h.hostid'.
830				' AND h.status='.HOST_STATUS_TEMPLATE.
831				' AND '.dbConditionId('hd.hostid', array_unique(array_column($hostmacros, 'hostid')))
832		), 'hostid');
833
834		if (!$templated_host_prototypeids) {
835			return [];
836		}
837
838		foreach ($hostmacros as $index => &$hostmacro) {
839			if (!in_array($hostmacro['hostid'], $templated_host_prototypeids)) {
840				unset($hostmacros[$index]);
841
842				continue;
843			}
844
845			if ($db_hostmacros) {
846				$db_hostmacro = $db_hostmacros[$hostmacro['hostmacroid']];
847				$hostmacro += array_intersect_key($db_hostmacro, array_flip(['macro']));
848
849				if ($hostmacro['macro'] !== $db_hostmacro['macro']) {
850					$hostmacro['macro_old'] = $db_hostmacro['macro'];
851				}
852			}
853		}
854		unset($hostmacro);
855
856		return $hostmacros;
857	}
858
859	/**
860	 * Prepares and returns an array of child hostmacros, inherited from hostmacros $tpl_hostmacros on the all hosts.
861	 *
862	 * @param array  $tpl_hostmacros
863	 * @param string $tpl_hostmacros[]['hostmacroid']
864	 * @param string $tpl_hostmacros[]['hostid']
865	 * @param string $tpl_hostmacros[]['macro']
866	 * @param string $tpl_hostmacros[]['value']        (optional)
867	 * @param string $tpl_hostmacros[]['description']  (optional)
868	 * @param int    $tpl_hostmacros[]['type']         (optional)
869	 * @param string $tpl_hostmacros[]['macro_old']    (optional)
870	 * @param array  $ins_hostmacros
871	 * @param array  $upd_hostmacros
872	 * @param array  $db_hostmacros
873	 */
874	private function prepareInheritedObjects(array $tpl_hostmacros, array &$ins_hostmacros = null,
875			array &$upd_hostmacros = null, array &$db_hostmacros = null): void {
876		$ins_hostmacros = [];
877		$upd_hostmacros = [];
878		$db_hostmacros = [];
879
880		$templateids_hostids = [];
881		$hostids = [];
882
883		$options = [
884			'output' => ['hostid', 'templateid'],
885			'filter' => ['templateid' => array_unique(array_column($tpl_hostmacros, 'hostid'))]
886		];
887		$chd_hosts = DBselect(DB::makeSql('hosts', $options));
888
889		while ($chd_host = DBfetch($chd_hosts)) {
890			$templateids_hostids[$chd_host['templateid']][] = $chd_host['hostid'];
891			$hostids[] = $chd_host['hostid'];
892		}
893
894		if (!$templateids_hostids) {
895			return;
896		}
897
898		$macros = [];
899		foreach ($tpl_hostmacros as $tpl_hostmacro) {
900			if (array_key_exists('macro_old', $tpl_hostmacro)) {
901				$macros[$tpl_hostmacro['macro_old']] = true;
902			}
903			else {
904				$macros[$tpl_hostmacro['macro']] = true;
905			}
906		}
907
908		$chd_hostmacros = DB::select('hostmacro', [
909			'output' => ['hostmacroid', 'hostid', 'macro', 'type', 'value', 'description'],
910			'filter' => ['hostid' => $hostids, 'macro' => array_keys($macros)],
911			'preservekeys' => true
912		]);
913
914		$host_macros = array_fill_keys($hostids, []);
915
916		foreach ($chd_hostmacros as $hostmacroid => $hostmacro) {
917			$host_macros[$hostmacro['hostid']][$hostmacro['macro']] = $hostmacroid;
918		}
919
920		foreach ($tpl_hostmacros as $tpl_hostmacro) {
921			$templateid = $tpl_hostmacro['hostid'];
922
923			if (!array_key_exists($templateid, $templateids_hostids)) {
924				continue;
925			}
926
927			foreach ($templateids_hostids[$templateid] as $hostid) {
928				if (array_key_exists('macro_old', $tpl_hostmacro)) {
929					$hostmacroid = $host_macros[$hostid][$tpl_hostmacro['macro_old']];
930
931					$upd_hostmacros[] = ['hostmacroid' => $hostmacroid, 'hostid' => $hostid] + $tpl_hostmacro;
932					$db_hostmacros[$hostmacroid] = $chd_hostmacros[$hostmacroid];
933
934					unset($chd_hostmacros[$hostmacroid], $host_macros[$hostid][$tpl_hostmacro['macro_old']]);
935				}
936				elseif (array_key_exists($tpl_hostmacro['macro'], $host_macros[$hostid])) {
937					$hostmacroid = $host_macros[$hostid][$tpl_hostmacro['macro']];
938
939					$upd_hostmacros[] = ['hostmacroid' => $hostmacroid, 'hostid' => $hostid] + $tpl_hostmacro;
940					$db_hostmacros[$hostmacroid] = $chd_hostmacros[$hostmacroid];
941
942					unset($chd_hostmacros[$hostmacroid], $host_macros[$hostid][$tpl_hostmacro['macro']]);
943				}
944				else {
945					$ins_hostmacros[] = ['hostid' => $hostid] + $tpl_hostmacro;
946				}
947			}
948		}
949	}
950
951	/**
952	 * Updates the macros for the children of host prototypes and propagates the inheritance to the child host
953	 * prototypes.
954	 *
955	 * @param array  $tpl_hostmacros
956	 * @param string $tpl_hostmacros[]['hostmacroid']
957	 * @param string $tpl_hostmacros[]['hostid']
958	 * @param string $tpl_hostmacros[]['macro']
959	 * @param string $tpl_hostmacros[]['value']        (optional)
960	 * @param string $tpl_hostmacros[]['description']  (optional)
961	 * @param int    $tpl_hostmacros[]['type']         (optional)
962	 * @param string $tpl_hostmacros[]['macro_old']    (optional)
963	 * @param bool   $is_delete                        Whether the passed hostmacros are intended to delete.
964	 */
965	private function inherit(array $tpl_hostmacros, bool $is_delete = false): void {
966		$this->prepareInheritedObjects($tpl_hostmacros, $ins_hostmacros, $upd_hostmacros, $db_hostmacros);
967
968		if ($ins_hostmacros) {
969			$this->createReal($ins_hostmacros);
970		}
971
972		if ($upd_hostmacros) {
973			if ($is_delete) {
974				DB::delete('hostmacro', ['hostmacroid' => array_column($upd_hostmacros, 'hostmacroid')]);
975			}
976			else {
977				$this->updateReal($upd_hostmacros, $db_hostmacros);
978			}
979		}
980
981		if ($ins_hostmacros || $upd_hostmacros) {
982			$this->inherit(array_merge($ins_hostmacros, $upd_hostmacros), $is_delete);
983		}
984	}
985
986	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
987		// Added type to query because it required to check macro is secret or not.
988		if (!$this->outputIsRequested('type', $options['output'])) {
989			$options['output'][] = 'type';
990		}
991
992		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
993
994		if ($options['output'] != API_OUTPUT_COUNT && $options['globalmacro'] === null) {
995			if ($options['selectGroups'] !== null || $options['selectHosts'] !== null || $options['selectTemplates'] !== null) {
996				$sqlParts = $this->addQuerySelect($this->fieldId('hostid'), $sqlParts);
997			}
998		}
999
1000		return $sqlParts;
1001	}
1002
1003	/**
1004	 * @inheritdoc
1005	 */
1006	protected function applyQueryFilterOptions($table, $alias, array $options, $sql_parts) {
1007		if (is_array($options['search'])) {
1008			// Do not allow to search by value for macro of type ZBX_MACRO_TYPE_SECRET.
1009			if (array_key_exists('value', $options['search'])) {
1010				$sql_parts['where']['search'] = $alias.'.type!='.ZBX_MACRO_TYPE_SECRET;
1011				zbx_db_search($table.' '.$alias, [
1012						'searchByAny' => false,
1013						'search' => ['value' => $options['search']['value']]
1014					] + $options, $sql_parts
1015				);
1016				unset($options['search']['value']);
1017			}
1018
1019			if ($options['search']) {
1020				zbx_db_search($table.' '.$alias, $options, $sql_parts);
1021			}
1022		}
1023
1024		if (is_array($options['filter'])) {
1025			// Do not allow to filter by value for macro of type ZBX_MACRO_TYPE_SECRET.
1026			if (array_key_exists('value', $options['filter'])) {
1027				$sql_parts['where']['filter'] = $alias.'.type!='.ZBX_MACRO_TYPE_SECRET;
1028				$this->dbFilter($table.' '.$alias, [
1029						'searchByAny' => false,
1030						'filter' => ['value' => $options['filter']['value']]
1031					] + $options, $sql_parts
1032				);
1033				unset($options['filter']['value']);
1034			}
1035
1036			if ($options['filter']) {
1037				$this->dbFilter($table.' '.$alias, $options, $sql_parts);
1038			}
1039		}
1040
1041		return $sql_parts;
1042	}
1043
1044	protected function addRelatedObjects(array $options, array $result) {
1045		$result = parent::addRelatedObjects($options, $result);
1046
1047		if ($options['globalmacro'] === null) {
1048			$hostMacroIds = array_keys($result);
1049
1050			/*
1051			 * Adding objects
1052			 */
1053			// adding groups
1054			if ($options['selectGroups'] !== null && $options['selectGroups'] != API_OUTPUT_COUNT) {
1055				$res = DBselect(
1056					'SELECT hm.hostmacroid,hg.groupid'.
1057						' FROM hostmacro hm,hosts_groups hg'.
1058						' WHERE '.dbConditionInt('hm.hostmacroid', $hostMacroIds).
1059						' AND hm.hostid=hg.hostid'
1060				);
1061				$relationMap = new CRelationMap();
1062				while ($relation = DBfetch($res)) {
1063					$relationMap->addRelation($relation['hostmacroid'], $relation['groupid']);
1064				}
1065
1066				$groups = API::HostGroup()->get([
1067					'output' => $options['selectGroups'],
1068					'groupids' => $relationMap->getRelatedIds(),
1069					'preservekeys' => true
1070				]);
1071				$result = $relationMap->mapMany($result, $groups, 'groups');
1072			}
1073
1074			// adding templates
1075			if ($options['selectTemplates'] !== null && $options['selectTemplates'] != API_OUTPUT_COUNT) {
1076				$relationMap = $this->createRelationMap($result, 'hostmacroid', 'hostid');
1077				$templates = API::Template()->get([
1078					'output' => $options['selectTemplates'],
1079					'templateids' => $relationMap->getRelatedIds(),
1080					'preservekeys' => true
1081				]);
1082				$result = $relationMap->mapMany($result, $templates, 'templates');
1083			}
1084
1085			// adding templates
1086			if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
1087				$relationMap = $this->createRelationMap($result, 'hostmacroid', 'hostid');
1088				$templates = API::Host()->get([
1089					'output' => $options['selectHosts'],
1090					'hostids' => $relationMap->getRelatedIds(),
1091					'preservekeys' => true
1092				]);
1093				$result = $relationMap->mapMany($result, $templates, 'hosts');
1094			}
1095		}
1096
1097		return $result;
1098	}
1099
1100	protected function unsetExtraFields(array $objects, array $fields, $output) {
1101		foreach ($objects as &$object) {
1102			if ($object['type'] == ZBX_MACRO_TYPE_SECRET) {
1103				unset($object['value']);
1104			}
1105		}
1106		unset($object);
1107
1108		return parent::unsetExtraFields($objects, $fields, $output);
1109	}
1110}
1111