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