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		// search
170		if (is_array($options['search'])) {
171			zbx_db_search('hostmacro hm', $options, $sqlParts);
172			zbx_db_search('globalmacro gm', $options, $sqlPartsGlobal);
173		}
174
175		// filter
176		if (is_array($options['filter'])) {
177			if (isset($options['filter']['macro'])) {
178				zbx_value2array($options['filter']['macro']);
179
180				$sqlParts['where'][] = dbConditionString('hm.macro', $options['filter']['macro']);
181				$sqlPartsGlobal['where'][] = dbConditionString('gm.macro', $options['filter']['macro']);
182			}
183		}
184
185		// sorting
186		$sqlParts = $this->applyQuerySortOptions('hostmacro', 'hm', $options, $sqlParts);
187		$sqlPartsGlobal = $this->applyQuerySortOptions('globalmacro', 'gm', $options, $sqlPartsGlobal);
188
189		// limit
190		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
191			$sqlParts['limit'] = $options['limit'];
192			$sqlPartsGlobal['limit'] = $options['limit'];
193		}
194
195		// init GLOBALS
196		if (!is_null($options['globalmacro'])) {
197			$sqlPartsGlobal = $this->applyQueryOutputOptions('globalmacro', 'gm', $options, $sqlPartsGlobal);
198			$res = DBselect(self::createSelectQueryFromParts($sqlPartsGlobal), $sqlPartsGlobal['limit']);
199			while ($macro = DBfetch($res)) {
200				if ($options['countOutput']) {
201					$result = $macro['rowscount'];
202				}
203				else {
204					$result[$macro['globalmacroid']] = $macro;
205				}
206			}
207		}
208		// init HOSTS
209		else {
210			$sqlParts = $this->applyQueryOutputOptions('hostmacro', 'hm', $options, $sqlParts);
211			$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
212			while ($macro = DBfetch($res)) {
213				if ($options['countOutput']) {
214					$result = $macro['rowscount'];
215				}
216				else {
217					$result[$macro['hostmacroid']] = $macro;
218				}
219			}
220		}
221
222		if ($options['countOutput']) {
223			return $result;
224		}
225
226		if ($result) {
227			$result = $this->addRelatedObjects($options, $result);
228			$result = $this->unsetExtraFields($result, ['hostid'], $options['output']);
229		}
230
231		// removing keys (hash -> array)
232		if (!$options['preservekeys']) {
233			$result = zbx_cleanHashes($result);
234		}
235
236		return $result;
237	}
238
239	/**
240	 * @param array $globalmacros
241	 *
242	 * @return array
243	 */
244	public function createGlobal(array $globalmacros) {
245		$this->validateCreateGlobal($globalmacros);
246
247		$globalmacroids = DB::insertBatch('globalmacro', $globalmacros);
248
249		foreach ($globalmacros as $index => &$globalmacro) {
250			$globalmacro['globalmacroid'] = $globalmacroids[$index];
251		}
252		unset($globalmacro);
253
254		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_MACRO, $globalmacros);
255
256		return ['globalmacroids' => $globalmacroids];
257	}
258
259	/**
260	 * @param array $globalmacros
261	 *
262	 * @throws APIException if the input is invalid.
263	 */
264	private function validateCreateGlobal(array &$globalmacros) {
265		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
266			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
267		}
268
269		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['macro']], 'fields' => [
270			'macro' =>	['type' => API_USER_MACRO, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('globalmacro', 'macro')],
271			'value' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('globalmacro', 'value')]
272		]];
273		if (!CApiInputValidator::validate($api_input_rules, $globalmacros, '/', $error)) {
274			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
275		}
276
277		$this->checkDuplicates(zbx_objectValues($globalmacros, 'macro'));
278	}
279
280	/**
281	 * @param array $globalmacros
282	 *
283	 * @return array
284	 */
285	public function updateGlobal(array $globalmacros) {
286		$this->validateUpdateGlobal($globalmacros, $db_globalmacros);
287
288		$upd_globalmacros = [];
289
290		foreach ($globalmacros as $globalmacro) {
291			$db_globalmacro = $db_globalmacros[$globalmacro['globalmacroid']];
292
293			$upd_globalmacro = [];
294
295			// strings
296			foreach (['macro', 'value'] as $field_name) {
297				if (array_key_exists($field_name, $globalmacro)
298						&& $globalmacro[$field_name] !== $db_globalmacro[$field_name]) {
299					$upd_globalmacro[$field_name] = $globalmacro[$field_name];
300				}
301			}
302
303			if ($upd_globalmacro) {
304				$upd_globalmacros[] = [
305					'values'=> $upd_globalmacro,
306					'where'=> ['globalmacroid' => $globalmacro['globalmacroid']]
307				];
308			}
309		}
310
311		if ($upd_globalmacros) {
312			DB::update('globalmacro', $upd_globalmacros);
313		}
314
315		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_MACRO, $globalmacros, $db_globalmacros);
316
317		return ['globalmacroids' => zbx_objectValues($globalmacros, 'globalmacroid')];
318	}
319
320	/**
321	 * Returns macro without spaces and curly braces.
322	 *
323	 * @param string $macro
324	 *
325	 * @return string
326	 */
327	private function trimMacro($macro) {
328		$user_macro_parser = new CUserMacroParser();
329
330		$user_macro_parser->parse($macro);
331
332		$macro = $user_macro_parser->getMacro();
333		$context = $user_macro_parser->getContext();
334
335		if ($context !== null) {
336			$macro .= ':'.$context;
337		}
338
339		return $macro;
340	}
341
342	/**
343	 * @param array $globalmacros
344	 * @param array $db_globalmacros
345	 *
346	 * @throws APIException if the input is invalid
347	 */
348	private function validateUpdateGlobal(array &$globalmacros, array &$db_globalmacros = null) {
349		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
350			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
351		}
352
353		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['globalmacroid'], ['macro']], 'fields' => [
354			'globalmacroid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
355			'macro' =>			['type' => API_USER_MACRO, 'length' => DB::getFieldLength('globalmacro', 'macro')],
356			'value' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('globalmacro', 'value')]
357		]];
358		if (!CApiInputValidator::validate($api_input_rules, $globalmacros, '/', $error)) {
359			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
360		}
361
362		$db_globalmacros = DB::select('globalmacro', [
363			'output' => ['globalmacroid', 'macro', 'value'],
364			'globalmacroids' => zbx_objectValues($globalmacros, 'globalmacroid'),
365			'preservekeys' => true
366		]);
367
368		$macros = [];
369
370		foreach ($globalmacros as $globalmacro) {
371			if (!array_key_exists($globalmacro['globalmacroid'], $db_globalmacros)) {
372				self::exception(ZBX_API_ERROR_PERMISSIONS,
373					_('No permissions to referred object or it does not exist!')
374				);
375			}
376
377			$db_globalmacro = $db_globalmacros[$globalmacro['globalmacroid']];
378
379			if (array_key_exists('macro', $globalmacro)
380					&& $this->trimMacro($globalmacro['macro']) !== $this->trimMacro($db_globalmacro['macro'])) {
381				$macros[] = $globalmacro['macro'];
382			}
383		}
384
385		if ($macros) {
386			$this->checkDuplicates($macros);
387		}
388	}
389
390	/**
391	 * Check for duplicated macros.
392	 *
393	 * @param array $macros
394	 *
395	 * @throws APIException if macros already exists.
396	 */
397	private function checkDuplicates(array $macros) {
398		$user_macro_parser = new CUserMacroParser();
399
400		$db_globalmacros = DB::select('globalmacro', [
401			'output' => ['macro']
402		]);
403
404		$uniq_macros = [];
405
406		foreach ($db_globalmacros as $db_globalmacro) {
407			$uniq_macros[$this->trimMacro($db_globalmacro['macro'])] = true;
408		}
409
410		foreach ($macros as $macro) {
411			$macro_orig = $macro;
412			$macro = $this->trimMacro($macro);
413
414			if (array_key_exists($macro, $uniq_macros)) {
415				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Macro "%1$s" already exists.', $macro_orig));
416			}
417			$uniq_macros[$macro] = true;
418		}
419	}
420
421	/**
422	 * @param array $globalmacroids
423	 *
424	 * @return array
425	 */
426	public function deleteGlobal(array $globalmacroids) {
427		$this->validateDeleteGlobal($globalmacroids, $db_globalmacros);
428
429		DB::delete('globalmacro', ['globalmacroid' => $globalmacroids]);
430
431		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_MACRO, $db_globalmacros);
432
433		return ['globalmacroids' => $globalmacroids];
434	}
435
436	/**
437	 * @param array $globalmacroids
438	 *
439	 * @throws APIException if the input is invalid.
440	 */
441	private function validateDeleteGlobal(array &$globalmacroids, array &$db_globalmacros = null) {
442		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
443			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
444		}
445
446		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
447		if (!CApiInputValidator::validate($api_input_rules, $globalmacroids, '/', $error)) {
448			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
449		}
450
451		$db_globalmacros = DB::select('globalmacro', [
452			'output' => ['globalmacroid', 'macro'],
453			'globalmacroids' => $globalmacroids,
454			'preservekeys' => true
455		]);
456
457		foreach ($globalmacroids as $globalmacroid) {
458			if (!array_key_exists($globalmacroid, $db_globalmacros)) {
459				self::exception(ZBX_API_ERROR_PERMISSIONS,
460					_('No permissions to referred object or it does not exist!')
461				);
462			}
463		}
464	}
465
466	/**
467	 * Validates the input parameters for the create() method.
468	 *
469	 * @param array $hostmacros
470	 *
471	 * @throws APIException if the input is invalid.
472	 */
473	protected function validateCreate(array $hostmacros) {
474		// Check the data required for authorization first.
475		foreach ($hostmacros as $hostmacro) {
476			$this->checkHostId($hostmacro);
477		}
478
479		$this->checkHostPermissions(array_unique(zbx_objectValues($hostmacros, 'hostid')));
480
481		foreach ($hostmacros as $hostmacro) {
482			$this->checkMacro($hostmacro);
483			$this->checkUnsupportedFields('hostmacro', $hostmacro,
484				_s('Wrong fields for macro "%1$s".', $hostmacro['macro']));
485		}
486
487		$this->checkDuplicateMacros($hostmacros);
488		$this->checkIfHostMacrosDontRepeat($hostmacros);
489	}
490
491	/**
492	 * Add new host macros.
493	 *
494	 * @param array $hostmacros an array of host macros
495	 *
496	 * @return array
497	 */
498	public function create(array $hostmacros) {
499		$hostmacros = zbx_toArray($hostmacros);
500
501		$this->validateCreate($hostmacros);
502
503		$hostmacroids = DB::insert('hostmacro', $hostmacros);
504
505		return ['hostmacroids' => $hostmacroids];
506	}
507
508	/**
509	 * Validates the input parameters for the update() method.
510	 *
511	 * @param array $hostmacros
512	 *
513	 * @throws APIException if the input is invalid.
514	 */
515	protected function validateUpdate(array $hostmacros) {
516		$required_fields = ['hostmacroid'];
517
518		foreach ($hostmacros as $hostmacro) {
519			$missing_keys = array_diff($required_fields, array_keys($hostmacro));
520
521			if ($missing_keys) {
522				self::exception(ZBX_API_ERROR_PARAMETERS,
523					_s('User macro missing parameters: %1$s', implode(', ', $missing_keys))
524				);
525			}
526		}
527
528		// Make sure we have all the data we need.
529		$hostmacros = $this->extendObjects($this->tableName(), $hostmacros, ['macro', 'hostid']);
530
531		$db_hostmacros = API::getApiService()->select($this->tableName(), [
532			'output' => ['hostmacroid', 'hostid', 'macro'],
533			'hostmacroids' => zbx_objectValues($hostmacros, 'hostmacroid')
534		]);
535
536		// Check if the macros exist in host.
537		$this->checkIfHostMacrosExistIn(zbx_objectValues($hostmacros, 'hostmacroid'), $db_hostmacros);
538
539		// Check the data required for authorization first.
540		foreach ($hostmacros as $hostmacro) {
541			$this->checkHostId($hostmacro);
542		}
543
544		// Check permissions for all affected hosts.
545		$affected_hostids = array_merge(zbx_objectValues($db_hostmacros, 'hostid'),
546			zbx_objectValues($hostmacros, 'hostid')
547		);
548		$affected_hostids = array_unique($affected_hostids);
549		$this->checkHostPermissions($affected_hostids);
550
551		foreach ($hostmacros as $hostmacro) {
552			$this->checkMacro($hostmacro);
553			$this->checkUnsupportedFields('hostmacro', $hostmacro,
554				_s('Wrong fields for macro "%1$s".', $hostmacro['macro'])
555			);
556		}
557
558		$this->checkDuplicateMacros($hostmacros);
559		$this->checkIfHostMacrosDontRepeat($hostmacros);
560	}
561
562	/**
563	 * Update host macros.
564	 *
565	 * @param array $hostmacros an array of host macros
566	 *
567	 * @return array
568	 */
569	public function update($hostmacros) {
570		$hostmacros = zbx_toArray($hostmacros);
571
572		$this->validateUpdate($hostmacros);
573
574		$data = [];
575
576		foreach ($hostmacros as $macro) {
577			$hostmacroid = $macro['hostmacroid'];
578			unset($macro['hostmacroid']);
579
580			$data[] = [
581				'values' => $macro,
582				'where' => ['hostmacroid' => $hostmacroid]
583			];
584		}
585
586		DB::update('hostmacro', $data);
587
588		return ['hostmacroids' => zbx_objectValues($hostmacros, 'hostmacroid')];
589	}
590
591	/**
592	 * Validates the input parameters for the delete() method.
593	 *
594	 * @param array $hostmacroids
595	 *
596	 * @throws APIException if the input is invalid.
597	 */
598	protected function validateDelete(array $hostmacroids) {
599		if (!$hostmacroids) {
600			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
601		}
602
603		$db_hostmacros = API::getApiService()->select('hostmacro', [
604			'output' => ['hostid', 'hostmacroid'],
605			'hostmacroids' => $hostmacroids
606		]);
607
608		// Check permissions for all affected hosts.
609		$this->checkHostPermissions(array_unique(zbx_objectValues($db_hostmacros, 'hostid')));
610
611		// Check if the macros exist in host.
612		$this->checkIfHostMacrosExistIn($hostmacroids, $db_hostmacros);
613	}
614
615	/**
616	 * Remove macros from hosts.
617	 *
618	 * @param array $hostmacroids
619	 *
620	 * @return array
621	 */
622	public function delete(array $hostmacroids) {
623		$this->validateDelete($hostmacroids);
624
625		DB::delete('hostmacro', ['hostmacroid' => $hostmacroids]);
626
627		return ['hostmacroids' => $hostmacroids];
628	}
629
630	/**
631	 * Replace macros on hosts/templates.
632	 * $macros input array has hostid as key and array of that host macros as value.
633	 *
634	 * @param array $macros
635	 */
636	public function replaceMacros(array $macros) {
637		$hostids = array_keys($macros);
638
639		$this->checkHostPermissions($hostids);
640
641		$db_hosts = API::Host()->get([
642			'output' => ['hostmacroid'],
643			'hostids' => $hostids,
644			'selectMacros' => API_OUTPUT_EXTEND,
645			'templated_hosts' => true,
646			'preservekeys' => true
647		]);
648
649		$hostmacroids_to_delete = [];
650		$hostmacros_to_update = [];
651		$hostmacros_to_add = [];
652
653		foreach ($macros as $hostid => $hostmacros) {
654			$db_hostmacros = zbx_toHash($db_hosts[$hostid]['macros'], 'hostmacroid');
655
656			/*
657			 * Look for db macros which hostmacroids are not in list of new macros. If there are any,
658			 * they should be deleted.
659			 */
660			$hostmacroids = zbx_toHash($hostmacros, 'hostmacroid');
661
662			foreach ($db_hostmacros as $db_hostmacro) {
663				if (!array_key_exists($db_hostmacro['hostmacroid'], $hostmacroids)) {
664					$hostmacroids_to_delete[] = $db_hostmacro['hostmacroid'];
665				}
666			}
667
668			// if macro has hostmacroid it should be updated otherwise created as new
669			foreach ($hostmacros as $hostmacro) {
670				if (array_key_exists('hostmacroid', $hostmacro)
671						&& array_key_exists($hostmacro['hostmacroid'], $db_hostmacros)) {
672					$hostmacros_to_update[] = $hostmacro;
673				}
674				else {
675					$hostmacro['hostid'] = $hostid;
676					$hostmacros_to_add[] = $hostmacro;
677				}
678			}
679		}
680
681		if ($hostmacroids_to_delete) {
682			$this->delete($hostmacroids_to_delete);
683		}
684
685		if ($hostmacros_to_add) {
686			$this->create($hostmacros_to_add);
687		}
688
689		if ($hostmacros_to_update) {
690			$this->update($hostmacros_to_update);
691		}
692	}
693
694	/**
695	 * Validates the "macro" field.
696	 *
697	 * @param array $macro
698	 * @param string $macro['macro']
699	 *
700	 * @throws APIException if the field is not valid.
701	 */
702	protected function checkMacro(array $macro) {
703		$missing_keys = array_diff(['macro'], array_keys($macro));
704
705		if ($missing_keys) {
706			self::exception(ZBX_API_ERROR_PARAMETERS,
707				_s('User macro missing parameters: %1$s', implode(', ', $missing_keys))
708			);
709		}
710
711		$user_macro_parser = new CUserMacroParser();
712
713		if ($user_macro_parser->parse($macro['macro']) != CParser::PARSE_SUCCESS) {
714			self::exception(ZBX_API_ERROR_PARAMETERS,
715				_s('Invalid macro "%1$s": %2$s.', $macro['macro'], $user_macro_parser->getError())
716			);
717		}
718	}
719
720	/**
721	 * Validates the "hostid" field.
722	 *
723	 * @param array $hostmacro
724	 *
725	 * @throws APIException if the field is empty.
726	 */
727	protected function checkHostId(array $hostmacro) {
728		if (!array_key_exists('hostid', $hostmacro) || zbx_empty($hostmacro['hostid'])) {
729			self::exception(ZBX_API_ERROR_PARAMETERS, _s('No host given for macro "%1$s".', $hostmacro['macro']));
730		}
731
732		if (!is_numeric($hostmacro['hostid'])) {
733			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid hostid for macro "%1$s".', $hostmacro['macro']));
734		}
735	}
736
737	/**
738	 * Checks if the current user has access to the given hosts and templates. Assumes the "hostid" field is valid.
739	 *
740	 * @param array $hostids    an array of host or template IDs
741	 *
742	 * @throws APIException if the user doesn't have write permissions for the given hosts.
743	 */
744	protected function checkHostPermissions(array $hostids) {
745		if ($hostids) {
746			$hostids = array_unique($hostids);
747
748			$count = API::Host()->get([
749				'countOutput' => true,
750				'hostids' => $hostids,
751				'filter' => [
752					'flags' => ZBX_FLAG_DISCOVERY_NORMAL
753				],
754				'editable' => true
755			]);
756
757			if ($count == count($hostids)) {
758				return;
759			}
760
761			$count += API::Template()->get([
762				'countOutput' => true,
763				'templateids' => $hostids,
764				'filter' => [
765					'flags' => ZBX_FLAG_DISCOVERY_NORMAL
766				],
767				'editable' => true
768			]);
769
770			if ($count != count($hostids)) {
771				self::exception(ZBX_API_ERROR_PERMISSIONS,
772					_('No permissions to referred object or it does not exist!')
773				);
774			}
775		}
776	}
777
778	/**
779	 * Checks if the given macros contain duplicates. Assumes the "macro" field is valid.
780	 *
781	 * @param array $macros
782	 *
783	 * @throws APIException if the given macros contain duplicates.
784	 */
785	protected function checkDuplicateMacros(array $macros) {
786		if (count($macros) <= 1) {
787			return;
788		}
789
790		$existing_macros = [];
791		$user_macro_parser = new CUserMacroParser();
792
793		foreach ($macros as $macro) {
794			// Global macros don't have a 'hostid'.
795			$hostid = array_key_exists('hostid', $macro) ? $macro['hostid'] : 1;
796
797			$user_macro_parser->parse($macro['macro']);
798
799			$macro_name = $user_macro_parser->getMacro();
800			$context = $user_macro_parser->getContext();
801
802			/*
803			 * Macros with same name can have different contexts. A macro with no context is not the same
804			 * as a macro with an empty context.
805			 */
806			if (array_key_exists($hostid, $existing_macros) && array_key_exists($macro_name, $existing_macros[$hostid])
807					&& in_array($context, $existing_macros[$hostid][$macro_name], true)) {
808				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Macro "%1$s" is not unique.', $macro['macro']));
809			}
810
811			$existing_macros[$hostid][$macro_name][] = $context;
812		}
813	}
814
815	/**
816	 * Checks if any of the given host macros already exist on the corresponding hosts. If the macros are updated and
817	 * the "hostmacroid" field is set, the method will only fail, if a macro with a different hostmacroid exists.
818	 * Assumes the "macro", "hostid" and "hostmacroid" fields are valid.
819	 *
820	 * @param array $hostmacros
821	 * @param int $hostmacros[]['hostmacroid']
822	 * @param int $hostmacros[]['hostid']
823	 * @paramt string $hostmacros['macro']
824	 *
825	 * @throws APIException if any of the given macros already exist.
826	 */
827	protected function checkIfHostMacrosDontRepeat(array $hostmacros) {
828		if (!$hostmacros) {
829			return;
830		}
831
832		$macro_names = [];
833		$user_macro_parser = new CUserMacroParser();
834
835		// Parse each macro, get unique names and, if context exists, narrow down the search.
836		foreach ($hostmacros as $hostmacro) {
837			$user_macro_parser->parse($hostmacro['macro']);
838
839			$macro_name = $user_macro_parser->getMacro();
840			$context = $user_macro_parser->getContext();
841
842			if ($context === null) {
843				$macro_names['{$'.$macro_name] = true;
844			}
845			else {
846				// Narrow down the search for macros with contexts.
847				$macro_names['{$'.$macro_name.':'] = true;
848			}
849		}
850
851		// When updating with empty array, don't select any data from database.
852		$db_hostmacros = API::getApiService()->select($this->tableName(), [
853			'output' => ['hostmacroid', 'hostid', 'macro'],
854			'filter' => ['hostid' => array_unique(zbx_objectValues($hostmacros, 'hostid'))],
855			'search' => ['macro' => array_keys($macro_names)],
856			'searchByAny' => true
857		]);
858
859		$existing_macros = [];
860
861		// Collect existing unique macro names and their contexts for each host.
862		foreach ($db_hostmacros as $db_hostmacro) {
863			$user_macro_parser->parse($db_hostmacro['macro']);
864
865			$macro_name = $user_macro_parser->getMacro();
866			$context = $user_macro_parser->getContext();
867
868			$existing_macros[$db_hostmacro['hostid']][$macro_name][$db_hostmacro['hostmacroid']] = $context;
869		}
870
871		// Compare each macro name and context to existing one.
872		foreach ($hostmacros as $hostmacro) {
873			$hostid = $hostmacro['hostid'];
874
875			$user_macro_parser->parse($hostmacro['macro']);
876
877			$macro_name = $user_macro_parser->getMacro();
878			$context = $user_macro_parser->getContext();
879
880			if (array_key_exists($hostid, $existing_macros) && array_key_exists($macro_name, $existing_macros[$hostid])
881					&& in_array($context, $existing_macros[$hostid][$macro_name], true)) {
882				foreach ($existing_macros[$hostid][$macro_name] as $hostmacroid => $existing_macro_context) {
883					if ((!array_key_exists('hostmacroid', $hostmacro)
884							|| bccomp($hostmacro['hostmacroid'], $hostmacroid) != 0)
885								&& $context === $existing_macro_context) {
886						$hosts = API::getApiService()->select('hosts', [
887							'output' => ['name'],
888							'hostids' => $hostmacro['hostid']
889						]);
890
891						self::exception(ZBX_API_ERROR_PARAMETERS,
892							_s('Macro "%1$s" already exists on "%2$s".', $hostmacro['macro'], $hosts[0]['name'])
893						);
894					}
895				}
896			}
897		}
898	}
899
900	/**
901	 * Checks if all of the host macros with hostmacrosids given in $hostmacrosids are present in $db_hostmacros.
902	 * Assumes the "hostmacroid" field is valid.
903	 *
904	 * @param array $hostmacroids
905	 * @param array $db_hostmacros
906	 *
907	 * @throws APIException if any of the host macros is not present in $db_hostmacros.
908	 */
909	protected function checkIfHostMacrosExistIn(array $hostmacroids, array $db_hostmacros) {
910		$db_hostmacros = zbx_toHash($db_hostmacros, 'hostmacroid');
911
912		foreach ($hostmacroids as $hostmacroid) {
913			if (!array_key_exists($hostmacroid, $db_hostmacros)) {
914				self::exception(ZBX_API_ERROR_PARAMETERS,
915					_s('Macro with hostmacroid "%1$s" does not exist.', $hostmacroid)
916				);
917			}
918		}
919	}
920
921	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
922		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
923
924		if ($options['output'] != API_OUTPUT_COUNT && $options['globalmacro'] === null) {
925			if ($options['selectGroups'] !== null || $options['selectHosts'] !== null || $options['selectTemplates'] !== null) {
926				$sqlParts = $this->addQuerySelect($this->fieldId('hostid'), $sqlParts);
927			}
928		}
929
930		return $sqlParts;
931	}
932
933	protected function addRelatedObjects(array $options, array $result) {
934		$result = parent::addRelatedObjects($options, $result);
935
936		if ($options['globalmacro'] === null) {
937			$hostMacroIds = array_keys($result);
938
939			/*
940			 * Adding objects
941			 */
942			// adding groups
943			if ($options['selectGroups'] !== null && $options['selectGroups'] != API_OUTPUT_COUNT) {
944				$res = DBselect(
945					'SELECT hm.hostmacroid,hg.groupid'.
946						' FROM hostmacro hm,hosts_groups hg'.
947						' WHERE '.dbConditionInt('hm.hostmacroid', $hostMacroIds).
948						' AND hm.hostid=hg.hostid'
949				);
950				$relationMap = new CRelationMap();
951				while ($relation = DBfetch($res)) {
952					$relationMap->addRelation($relation['hostmacroid'], $relation['groupid']);
953				}
954
955				$groups = API::HostGroup()->get([
956					'output' => $options['selectGroups'],
957					'groupids' => $relationMap->getRelatedIds(),
958					'preservekeys' => true
959				]);
960				$result = $relationMap->mapMany($result, $groups, 'groups');
961			}
962
963			// adding templates
964			if ($options['selectTemplates'] !== null && $options['selectTemplates'] != API_OUTPUT_COUNT) {
965				$relationMap = $this->createRelationMap($result, 'hostmacroid', 'hostid');
966				$templates = API::Template()->get([
967					'output' => $options['selectTemplates'],
968					'templateids' => $relationMap->getRelatedIds(),
969					'preservekeys' => true
970				]);
971				$result = $relationMap->mapMany($result, $templates, 'templates');
972			}
973
974			// adding templates
975			if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
976				$relationMap = $this->createRelationMap($result, 'hostmacroid', 'hostid');
977				$templates = API::Host()->get([
978					'output' => $options['selectHosts'],
979					'hostids' => $relationMap->getRelatedIds(),
980					'preservekeys' => true
981				]);
982				$result = $relationMap->mapMany($result, $templates, 'hosts');
983			}
984		}
985
986		return $result;
987	}
988}
989