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
22class CMacrosResolverGeneral {
23
24	const PATTERN_HOST_INTERNAL = 'HOST\.HOST|HOSTNAME';
25	const PATTERN_MACRO_PARAM = '[1-9]?';
26
27	/**
28	 * Interface priorities.
29	 *
30	 * @var array
31	 */
32	protected $interfacePriorities = [
33		INTERFACE_TYPE_AGENT => 4,
34		INTERFACE_TYPE_SNMP => 3,
35		INTERFACE_TYPE_JMX => 2,
36		INTERFACE_TYPE_IPMI => 1
37	];
38
39	/**
40	 * Work config name.
41	 *
42	 * @var string
43	 */
44	protected $config = '';
45
46	/**
47	 * Get reference macros for trigger.
48	 * If macro reference non existing value it expands to empty string.
49	 *
50	 * @param string $expression
51	 * @param array  $references
52	 *
53	 * @return array
54	 */
55	protected function resolveTriggerReferences($expression, $references) {
56		$matched_macros = $this->getMacroPositions($expression, ['usermacros' => true]);
57
58		// Replace user macros with string 'macro' to make values search easier.
59		foreach (array_reverse($matched_macros, true) as $pos => $macro) {
60			$expression = substr_replace($expression, 'macro', $pos, strlen($macro));
61		}
62
63		// Replace functionids with string 'function' to make values search easier.
64		$expression = preg_replace('/\{[0-9]+\}/', 'function', $expression);
65
66		// Search for numeric values in expression.
67		preg_match_all('/'.ZBX_PREG_NUMBER.'/', $expression, $values);
68
69		foreach ($references as $reference => &$value) {
70			$i = (int) $reference[1] - 1;
71			$value = array_key_exists($i, $values[0]) ? $values[0][$i] : '';
72		}
73		unset($value);
74
75		return $references;
76	}
77
78	/**
79	 * Checking existence of the macros.
80	 *
81	 * @param array  $texts
82	 * @param array  $type
83	 *
84	 * @return bool
85	 */
86	protected function hasMacros(array $texts, array $types) {
87		foreach ($texts as $text) {
88			if ($this->getMacroPositions($text, $types)) {
89				return true;
90			}
91		}
92
93		return false;
94	}
95
96	/**
97	 * Transform types, used in extractMacros() function to types which can be used in getMacroPositions().
98	 *
99	 * @param array  $types
100	 *
101	 * @return array
102	 */
103	protected function transformToPositionTypes(array $types) {
104		foreach (['macros', 'macros_n'] as $type) {
105			if (array_key_exists($type, $types)) {
106				$patterns = [];
107				foreach ($types[$type] as $key => $_patterns) {
108					$patterns = array_merge($patterns, $_patterns);
109				}
110				$types[$type] = $patterns;
111			}
112		}
113
114		return $types;
115	}
116
117	/**
118	 * Extract positions of the macros from a string.
119	 *
120	 * @param string $text
121	 * @param array  $types
122	 * @param bool   $types['usermacros']
123	 * @param array  $types['macros'][<macro_patterns>]
124	 * @param array  $types['macros_n'][<macro_patterns>]
125	 * @param bool   $types['references']
126	 * @param bool   $types['lldmacros']
127	 * @param bool   $types['functionids']
128	 *
129	 * @return array
130	 */
131	protected function getMacroPositions($text, array $types) {
132		$macros = [];
133		$extract_usermacros = array_key_exists('usermacros', $types);
134		$extract_macros = array_key_exists('macros', $types);
135		$extract_macros_n = array_key_exists('macros_n', $types);
136		$extract_references = array_key_exists('references', $types);
137		$extract_lldmacros = array_key_exists('lldmacros', $types);
138		$extract_functionids = array_key_exists('functionids', $types);
139
140		if ($extract_usermacros) {
141			$user_macro_parser = new CUserMacroParser();
142		}
143
144		if ($extract_macros) {
145			$macro_parser = new CMacroParser($types['macros']);
146		}
147
148		if ($extract_macros_n) {
149			$macro_n_parser = new CMacroParser($types['macros_n'], ['allow_reference' => true]);
150		}
151
152		if ($extract_references) {
153			$reference_parser = new CReferenceParser();
154		}
155
156		if ($extract_lldmacros) {
157			$lld_macro_parser = new CLLDMacroParser();
158		}
159
160		if ($extract_functionids) {
161			$functionid_parser = new CFunctionIdParser();
162		}
163
164		for ($pos = 0; isset($text[$pos]); $pos++) {
165			if ($extract_usermacros && $user_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
166				$macros[$pos] = $user_macro_parser->getMatch();
167				$pos += $user_macro_parser->getLength() - 1;
168			}
169			elseif ($extract_macros && $macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
170				$macros[$pos] = $macro_parser->getMatch();
171				$pos += $macro_parser->getLength() - 1;
172			}
173			elseif ($extract_macros_n && $macro_n_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
174				$macros[$pos] = $macro_n_parser->getMatch();
175				$pos += $macro_n_parser->getLength() - 1;
176			}
177			elseif ($extract_references && $reference_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
178				$macros[$pos] = $reference_parser->getMatch();
179				$pos += $reference_parser->getLength() - 1;
180			}
181			elseif ($extract_lldmacros && $lld_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
182				$macros[$pos] = $lld_macro_parser->getMatch();
183				$pos += $lld_macro_parser->getLength() - 1;
184			}
185			elseif ($extract_functionids && $functionid_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
186				$macros[$pos] = $functionid_parser->getMatch();
187				$pos += $functionid_parser->getLength() - 1;
188			}
189		}
190
191		return $macros;
192	}
193
194	/**
195	 * Extract macros from a string.
196	 *
197	 * @param array  $texts
198	 * @param array  $types
199	 * @param bool   $types['usermacros']
200	 * @param array  $types['macros'][][<macro_patterns>]
201	 * @param array  $types['macros_n'][][<macro_patterns>]
202	 * @param bool   $types['references']
203	 * @param bool   $types['lldmacros']
204	 * @param bool   $types['functionids']
205	 *
206	 * @return array
207	 */
208	protected function extractMacros(array $texts, array $types) {
209		$macros = [];
210		$extract_usermacros = array_key_exists('usermacros', $types);
211		$extract_macros = array_key_exists('macros', $types);
212		$extract_macros_n = array_key_exists('macros_n', $types);
213		$extract_references = array_key_exists('references', $types);
214		$extract_lldmacros = array_key_exists('lldmacros', $types);
215		$extract_functionids = array_key_exists('functionids', $types);
216
217		if ($extract_usermacros) {
218			$macros['usermacros'] = [];
219
220			$user_macro_parser = new CUserMacroParser();
221		}
222
223		if ($extract_macros) {
224			$macros['macros'] = [];
225
226			foreach ($types['macros'] as $key => $macro_patterns) {
227				$types['macros'][$key] = new CMacroParser($macro_patterns);
228				$macros['macros'][$key] = [];
229			}
230		}
231
232		if ($extract_macros_n) {
233			$macros['macros_n'] = [];
234
235			foreach ($types['macros_n'] as $key => $macro_patterns) {
236				$types['macros_n'][$key] = new CMacroParser($macro_patterns, ['allow_reference' => true]);
237				$macros['macros_n'][$key] = [];
238			}
239		}
240
241		if ($extract_references) {
242			$macros['references'] = [];
243
244			$reference_parser = new CReferenceParser();
245		}
246
247		if ($extract_lldmacros) {
248			$macros['lldmacros'] = [];
249
250			$lld_macro_parser = new CLLDMacroParser();
251		}
252
253		if ($extract_functionids) {
254			$macros['functionids'] = [];
255
256			$functionid_parser = new CFunctionIdParser();
257		}
258
259		foreach ($texts as $text) {
260			for ($pos = 0; isset($text[$pos]); $pos++) {
261				if ($extract_usermacros && $user_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
262					$macros['usermacros'][$user_macro_parser->getMatch()] = null;
263					$pos += $user_macro_parser->getLength() - 1;
264					continue;
265				}
266
267				if ($extract_macros) {
268					foreach ($types['macros'] as $key => $macro_parser) {
269						if ($macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
270							$macros['macros'][$key][$macro_parser->getMatch()] = true;
271							$pos += $macro_parser->getLength() - 1;
272							continue 2;
273						}
274					}
275				}
276
277				if ($extract_macros_n) {
278					foreach ($types['macros_n'] as $key => $macro_n_parser) {
279						if ($macro_n_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
280							$macros['macros_n'][$key][$macro_n_parser->getMacro()][] = $macro_n_parser->getN();
281							$pos += $macro_n_parser->getLength() - 1;
282							continue 2;
283						}
284					}
285				}
286
287				if ($extract_references && $reference_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
288					$macros['references'][$reference_parser->getMatch()] = null;
289					$pos += $reference_parser->getLength() - 1;
290					continue;
291				}
292
293				if ($extract_lldmacros && $lld_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
294					$macros['lldmacros'][$lld_macro_parser->getMatch()] = null;
295					$pos += $lld_macro_parser->getLength() - 1;
296					continue;
297				}
298
299				if ($extract_functionids && $functionid_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
300					$macros['functionids'][$functionid_parser->getMatch()] = null;
301					$pos += $functionid_parser->getLength() - 1;
302					continue;
303				}
304			}
305		}
306
307		if ($extract_macros) {
308			foreach ($types['macros'] as $key => $macro_parser) {
309				$macros['macros'][$key] = array_keys($macros['macros'][$key]);
310			}
311		}
312
313		return $macros;
314	}
315
316	/**
317	 * Returns the list of the item key parameters.
318	 *
319	 * @param string $params_raw
320	 *
321	 * @return array
322	 */
323	private function getItemKeyParameters($params_raw) {
324		$item_key_parameters = [];
325
326		foreach ($params_raw as $param_raw) {
327			switch ($param_raw['type']) {
328				case CItemKey::PARAM_ARRAY:
329					$item_key_parameters = array_merge($item_key_parameters,
330						$this->getItemKeyParameters($param_raw['parameters'])
331					);
332					break;
333
334				case CItemKey::PARAM_UNQUOTED:
335					$item_key_parameters[] = $param_raw['raw'];
336					break;
337
338				case CItemKey::PARAM_QUOTED:
339					$item_key_parameters[] = CItemKey::unquoteParam($param_raw['raw']);
340					break;
341			}
342		}
343
344		return $item_key_parameters;
345	}
346
347	/**
348	 * Extract macros from an item key.
349	 *
350	 * @param string $key		an item key
351	 * @param array  $types		the types of macros (see extractMacros() for more details)
352	 *
353	 * @return array			see extractMacros() for more details
354	 */
355	protected function extractItemKeyMacros($key, array $types) {
356		$item_key_parser = new CItemKey();
357
358		$item_key_parameters = [];
359		if ($item_key_parser->parse($key) == CParser::PARSE_SUCCESS) {
360			$item_key_parameters = $this->getItemKeyParameters($item_key_parser->getParamsRaw());
361		}
362
363		return $this->extractMacros($item_key_parameters, $types);
364	}
365
366	/**
367	 * Extract macros from a trigger function.
368	 *
369	 * @param string $function	a trigger function, for example 'last({$LAST})'
370	 * @param array  $types		the types of macros (see extractMacros() for more details)
371	 *
372	 * @return array			see extractMacros() for more details
373	 */
374	protected function extractFunctionMacros($function, array $types) {
375		$function_parser = new CFunctionParser();
376
377		$function_parameters = [];
378		if ($function_parser->parse($function) == CParser::PARSE_SUCCESS) {
379			foreach ($function_parser->getParamsRaw()['parameters'] as $param_raw) {
380				switch ($param_raw['type']) {
381					case CFunctionParser::PARAM_UNQUOTED:
382						$function_parameters[] = $param_raw['raw'];
383						break;
384
385					case CFunctionParser::PARAM_QUOTED:
386						$function_parameters[] = CFunctionParser::unquoteParam($param_raw['raw']);
387						break;
388				}
389			}
390		}
391
392		return $this->extractMacros($function_parameters, $types);
393	}
394
395	/**
396	 * Resolves macros in the item key parameters.
397	 *
398	 * @param string $key_chain		an item key chain
399	 * @param string $params_raw
400	 * @param array  $macros		the list of macros (['{<MACRO>}' => '<value>', ...])
401	 * @param array  $types			the types of macros (see getMacroPositions() for more details)
402	 *
403	 * @return string
404	 */
405	private function resolveItemKeyParamsMacros($key_chain, array $params_raw, array $macros, array $types) {
406		foreach (array_reverse($params_raw) as $param_raw) {
407			$param = $param_raw['raw'];
408			$forced = false;
409
410			switch ($param_raw['type']) {
411				case CItemKey::PARAM_ARRAY:
412					$param = $this->resolveItemKeyParamsMacros($param, $param_raw['parameters'], $macros, $types);
413					break;
414
415				case CItemKey::PARAM_QUOTED:
416					$param = CItemKey::unquoteParam($param);
417					$forced = true;
418					// break; is not missing here
419
420				case CItemKey::PARAM_UNQUOTED:
421					$matched_macros = $this->getMacroPositions($param, $types);
422
423					foreach (array_reverse($matched_macros, true) as $pos => $macro) {
424						$param = substr_replace($param, $macros[$macro], $pos, strlen($macro));
425					}
426
427					$param = quoteItemKeyParam($param, $forced);
428					break;
429			}
430
431			$key_chain = substr_replace($key_chain, $param, $param_raw['pos'], strlen($param_raw['raw']));
432		}
433
434		return $key_chain;
435	}
436
437	/**
438	 * Resolves macros in the item key.
439	 *
440	 * @param string $key		an item key
441	 * @param array  $macros	the list of macros (['{<MACRO>}' => '<value>', ...])
442	 * @param array  $types		the types of macros (see getMacroPositions() for more details)
443	 *
444	 * @return string
445	 */
446	protected function resolveItemKeyMacros($key, array $macros, array $types) {
447		$item_key_parser = new CItemKey();
448
449		if ($item_key_parser->parse($key) == CParser::PARSE_SUCCESS) {
450			$key = $this->resolveItemKeyParamsMacros($key, $item_key_parser->getParamsRaw(), $macros, $types);
451		}
452
453		return $key;
454	}
455
456	/**
457	 * Resolves macros in the trigger function parameters.
458	 *
459	 * @param string $function	a trigger function
460	 * @param array  $macros	the list of macros (['{<MACRO>}' => '<value>', ...])
461	 * @param array  $types		the types of macros (see getMacroPositions() for more details)
462	 *
463	 * @return string
464	 */
465	protected function resolveFunctionMacros($function, array $macros, array $types) {
466		$function_parser = new CFunctionParser();
467
468		if ($function_parser->parse($function) == CParser::PARSE_SUCCESS) {
469			$params_raw = $function_parser->getParamsRaw();
470			$function_chain = $params_raw['raw'];
471
472			foreach (array_reverse($params_raw['parameters']) as $param_raw) {
473				$param = $param_raw['raw'];
474				$forced = false;
475
476				switch ($param_raw['type']) {
477					case CFunctionParser::PARAM_QUOTED:
478						$param = CFunctionParser::unquoteParam($param);
479						$forced = true;
480						// break; is not missing here
481
482					case CFunctionParser::PARAM_UNQUOTED:
483						$matched_macros = $this->getMacroPositions($param, $types);
484
485						foreach (array_reverse($matched_macros, true) as $pos => $macro) {
486							$param = substr_replace($param, $macros[$macro], $pos, strlen($macro));
487						}
488
489						$param = quoteFunctionParam($param, $forced);
490						break;
491				}
492
493				$function_chain = substr_replace($function_chain, $param, $param_raw['pos'], strlen($param_raw['raw']));
494			}
495
496			$function = substr_replace($function, $function_chain, $params_raw['pos'], strlen($params_raw['raw']));
497		}
498
499		return $function;
500	}
501
502	/**
503	 * Find function ids in trigger expression.
504	 *
505	 * @param string $expression
506	 *
507	 * @return array	where key is function id position in expression and value is function id
508	 */
509	protected function findFunctions($expression) {
510		$functionids = [];
511
512		$functionid_parser = new CFunctionIdParser();
513		$macro_parser = new CMacroParser(['{TRIGGER.VALUE}']);
514		$user_macro_parser = new CUserMacroParser();
515
516		for ($pos = 0, $i = 1; isset($expression[$pos]); $pos++) {
517			if ($functionid_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
518				$pos += $functionid_parser->getLength() - 1;
519				$functionids[$i++] = substr($functionid_parser->getMatch(), 1, -1);
520			}
521			elseif ($user_macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
522				$pos += $user_macro_parser->getLength() - 1;
523			}
524			elseif ($macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
525				$pos += $macro_parser->getLength() - 1;
526			}
527		}
528
529		if (array_key_exists(1, $functionids)) {
530			$functionids[0] = $functionids[1];
531		}
532
533		return $functionids;
534	}
535
536	/**
537	 * Add function macro name with corresponding value to replace to $macroValues array.
538	 *
539	 * @param array  $macroValues
540	 * @param array  $fNums
541	 * @param int    $triggerId
542	 * @param string $macro
543	 * @param string $replace
544	 *
545	 * @return array
546	 */
547	protected function getFunctionMacroValues(array $macroValues, array $fNums, $triggerId, $macro, $replace) {
548		foreach ($fNums as $fNum) {
549			$macroValues[$triggerId][$this->getFunctionMacroName($macro, $fNum)] = $replace;
550		}
551
552		return $macroValues;
553	}
554
555	/**
556	 * Get {ITEM.LASTVALUE} macro.
557	 *
558	 * @param mixed $lastValue
559	 * @param array $item
560	 *
561	 * @return string
562	 */
563	protected function getItemLastValueMacro($lastValue, array $item) {
564		return ($lastValue === null) ? UNRESOLVED_MACRO_STRING : formatHistoryValue($lastValue, $item);
565	}
566
567	/**
568	 * Get function macro name.
569	 *
570	 * @param string $macro
571	 * @param int    $fNum
572	 *
573	 * @return string
574	 */
575	protected function getFunctionMacroName($macro, $fNum) {
576		return '{'.(($fNum == 0) ? $macro : $macro.$fNum).'}';
577	}
578
579	/**
580	 * Get interface macros.
581	 *
582	 * @param array $macros
583	 * @param array $macroValues
584	 * @param bool  $port
585	 *
586	 * @return array
587	 */
588	protected function getIpMacros(array $macros, array $macroValues, $port) {
589		if ($macros) {
590			$selectPort = $port ? ',n.port' : '';
591
592			$dbInterfaces = DBselect(
593				'SELECT f.triggerid,f.functionid,n.ip,n.dns,n.type,n.useip'.$selectPort.
594				' FROM functions f'.
595					' JOIN items i ON f.itemid=i.itemid'.
596					' JOIN interface n ON i.hostid=n.hostid'.
597				' WHERE '.dbConditionInt('f.functionid', array_keys($macros)).
598					' AND n.main=1'
599			);
600
601			// Macro should be resolved to interface with highest priority ($priorities).
602			$interfaces = [];
603
604			while ($dbInterface = DBfetch($dbInterfaces)) {
605				if (isset($interfaces[$dbInterface['functionid']])
606						&& $this->interfacePriorities[$interfaces[$dbInterface['functionid']]['type']] > $this->interfacePriorities[$dbInterface['type']]) {
607					continue;
608				}
609
610				$interfaces[$dbInterface['functionid']] = $dbInterface;
611			}
612
613			foreach ($interfaces as $interface) {
614				foreach ($macros[$interface['functionid']] as $macro => $fNums) {
615					switch ($macro) {
616						case 'IPADDRESS':
617						case 'HOST.IP':
618							$replace = $interface['ip'];
619							break;
620						case 'HOST.DNS':
621							$replace = $interface['dns'];
622							break;
623						case 'HOST.CONN':
624							$replace = $interface['useip'] ? $interface['ip'] : $interface['dns'];
625							break;
626						case 'HOST.PORT':
627							$replace = $interface['port'];
628							break;
629					}
630
631					$macroValues = $this->getFunctionMacroValues($macroValues, $fNums, $interface['triggerid'], $macro, $replace);
632				}
633			}
634		}
635
636		return $macroValues;
637	}
638
639	/**
640	 * Get item macros.
641	 *
642	 * @param array $macros
643	 * @param array $triggers
644	 * @param array $macroValues
645	 * @param bool  $events			resolve {ITEM.VALUE} macro using 'clock' and 'ns' fields
646	 *
647	 * @return array
648	 */
649	protected function getItemMacros(array $macros, array $triggers, array $macroValues, $events) {
650		if ($macros) {
651			$functions = DbFetchArray(DBselect(
652				'SELECT f.triggerid,f.functionid,i.itemid,i.value_type,i.units,i.valuemapid'.
653				' FROM functions f'.
654					' JOIN items i ON f.itemid=i.itemid'.
655					' JOIN hosts h ON i.hostid=h.hostid'.
656				' WHERE '.dbConditionInt('f.functionid', array_keys($macros))
657			));
658
659			$history = Manager::History()->getLast($functions, 1, ZBX_HISTORY_PERIOD);
660
661			// False passed to DBfetch to get data without null converted to 0, which is done by default.
662			foreach ($functions as $func) {
663				foreach ($macros[$func['functionid']] as $macro => $fNums) {
664					$lastValue = isset($history[$func['itemid']]) ? $history[$func['itemid']][0]['value'] : null;
665
666					switch ($macro) {
667						case 'ITEM.LASTVALUE':
668							$replace = $this->getItemLastValueMacro($lastValue, $func);
669							break;
670						case 'ITEM.VALUE':
671							if ($events) {
672								$trigger = $triggers[$func['triggerid']];
673								$value = item_get_history($func, $trigger['clock'], $trigger['ns']);
674
675								$replace = ($value === null)
676									? UNRESOLVED_MACRO_STRING
677									: formatHistoryValue($value, $func);
678							}
679							else {
680								$replace = $this->getItemLastValueMacro($lastValue, $func);
681							}
682							break;
683					}
684
685					$macroValues = $this->getFunctionMacroValues($macroValues, $fNums, $func['triggerid'], $macro, $replace);
686				}
687			}
688		}
689
690		return $macroValues;
691	}
692
693	/**
694	 * Get host macros.
695	 *
696	 * @param array $macros
697	 * @param array $macroValues
698	 *
699	 * @return array
700	 */
701	protected function getHostMacros(array $macros, array $macroValues) {
702		if ($macros) {
703			$dbFuncs = DBselect(
704				'SELECT f.triggerid,f.functionid,h.hostid,h.host,h.name'.
705				' FROM functions f'.
706					' JOIN items i ON f.itemid=i.itemid'.
707					' JOIN hosts h ON i.hostid=h.hostid'.
708				' WHERE '.dbConditionInt('f.functionid', array_keys($macros))
709			);
710			while ($func = DBfetch($dbFuncs)) {
711				foreach ($macros[$func['functionid']] as $macro => $fNums) {
712					switch ($macro) {
713						case 'HOST.ID':
714							$replace = $func['hostid'];
715							break;
716
717						case 'HOSTNAME':
718						case 'HOST.HOST':
719							$replace = $func['host'];
720							break;
721
722						case 'HOST.NAME':
723							$replace = $func['name'];
724							break;
725					}
726
727					$macroValues = $this->getFunctionMacroValues($macroValues, $fNums, $func['triggerid'], $macro, $replace);
728				}
729			}
730		}
731
732		return $macroValues;
733	}
734
735	/**
736	 * Is type available.
737	 *
738	 * @param string $type
739	 *
740	 * @return bool
741	 */
742	protected function isTypeAvailable($type) {
743		return in_array($type, $this->configs[$this->config]['types']);
744	}
745
746	/**
747	 * Get source field.
748	 *
749	 * @return string
750	 */
751	protected function getSource() {
752		return $this->configs[$this->config]['source'];
753	}
754
755	/**
756	 * Get macros with values.
757	 *
758	 * @param array $data
759	 * @param array $data[<id>]			any identificator
760	 * @param array $data['hostids']	the list of host ids; [<hostid1>, ...]
761	 * @param array $data['macros']		the list of user macros to resolve, ['<usermacro1>' => null, ...]
762	 *
763	 * @return array
764	 */
765	protected function getUserMacros(array $data) {
766		// User macros.
767		$hostids = [];
768		foreach ($data as $element) {
769			foreach ($element['hostids'] as $hostid) {
770				$hostids[$hostid] = true;
771			}
772		}
773
774		if (!$hostids) {
775			return $data;
776		}
777
778		/*
779		 * @var array $host_templates
780		 * @var array $host_templates[<hostid>]		array of templates
781		 */
782		$host_templates = [];
783
784		/*
785		 * @var array  $host_macros
786		 * @var array  $host_macros[<hostid>]
787		 * @var array  $host_macros[<hostid>][<macro>]				macro base without curly braces
788		 * @var string $host_macros[<hostid>][<macro>]['value']		base macro value (without context); can be null
789		 * @var array  $host_macros[<hostid>][<macro>]['contexts']	context values; ['<context1>' => '<value1>', ...]
790		 */
791		$host_macros = [];
792
793		$user_macro_parser = new CUserMacroParser();
794
795		do {
796			$hostids = array_keys($hostids);
797
798			$db_host_macros = DBselect(
799				'SELECT hm.hostid,hm.macro,hm.value'.
800				' FROM hostmacro hm'.
801				' WHERE '.dbConditionInt('hm.hostid', $hostids)
802			);
803			while ($db_host_macro = DBfetch($db_host_macros)) {
804				if ($user_macro_parser->parse($db_host_macro['macro']) != CParser::PARSE_SUCCESS) {
805					continue;
806				}
807
808				$macro = $user_macro_parser->getMacro();
809				$context = $user_macro_parser->getContext();
810
811				if (!array_key_exists($db_host_macro['hostid'], $host_macros)) {
812					$host_macros[$db_host_macro['hostid']] = [];
813				}
814
815				if (!array_key_exists($macro, $host_macros[$db_host_macro['hostid']])) {
816					$host_macros[$db_host_macro['hostid']][$macro] = ['value' => null, 'contexts' => []];
817				}
818
819				if ($context === null) {
820					$host_macros[$db_host_macro['hostid']][$macro]['value'] = $db_host_macro['value'];
821				}
822				else {
823					$host_macros[$db_host_macro['hostid']][$macro]['contexts'][$context] = $db_host_macro['value'];
824				}
825			}
826
827			foreach ($hostids as $hostid) {
828				$host_templates[$hostid] = [];
829			}
830
831			$templateids = [];
832			$db_host_templates = DBselect(
833				'SELECT ht.hostid,ht.templateid'.
834				' FROM hosts_templates ht'.
835				' WHERE '.dbConditionInt('ht.hostid', $hostids)
836			);
837			while ($db_host_template = DBfetch($db_host_templates)) {
838				$host_templates[$db_host_template['hostid']][] = $db_host_template['templateid'];
839				$templateids[$db_host_template['templateid']] = true;
840			}
841
842			// only unprocessed templates will be populated
843			$hostids = [];
844			foreach (array_keys($templateids) as $templateid) {
845				if (!array_key_exists($templateid, $host_templates)) {
846					$hostids[$templateid] = true;
847				}
848			}
849		} while ($hostids);
850
851		$all_macros_resolved = true;
852
853		foreach ($data as &$element) {
854			$hostids = [];
855			foreach ($element['hostids'] as $hostid) {
856				$hostids[$hostid] = true;
857			}
858
859			$hostids = array_keys($hostids);
860			natsort($hostids);
861
862			foreach ($element['macros'] as $usermacro => &$value) {
863				if ($user_macro_parser->parse($usermacro) == CParser::PARSE_SUCCESS) {
864					$value = $this->getHostUserMacros($hostids, $user_macro_parser->getMacro(),
865						$user_macro_parser->getContext(), $host_templates, $host_macros
866					);
867
868					if ($value['value'] === null) {
869						$all_macros_resolved = false;
870					}
871				}
872				else {
873					// This macro cannot be resolved.
874					$value = ['value' => $usermacro, 'value_default' => null];
875				}
876			}
877			unset($value);
878		}
879		unset($element);
880
881		if (!$all_macros_resolved) {
882			// Global macros.
883			$db_global_macros = API::UserMacro()->get([
884				'output' => ['macro', 'value'],
885				'globalmacro' => true
886			]);
887
888			/*
889			 * @var array  $global_macros
890			 * @var array  $global_macros[<macro>]				macro base without curly braces
891			 * @var string $global_macros[<macro>]['value']		base macro value (without context); can be null
892			 * @var array  $global_macros[<macro>]['contexts']	context values; ['<context1>' => '<value1>', ...]
893			 */
894			$global_macros = [];
895
896			foreach ($db_global_macros as $db_global_macro) {
897				if ($user_macro_parser->parse($db_global_macro['macro']) == CParser::PARSE_SUCCESS) {
898					$macro = $user_macro_parser->getMacro();
899					$context = $user_macro_parser->getContext();
900
901					if (!array_key_exists($macro, $global_macros)) {
902						$global_macros[$macro] = ['value' => null, 'contexts' => []];
903					}
904
905					if ($context === null) {
906						$global_macros[$macro]['value'] = $db_global_macro['value'];
907					}
908					else {
909						$global_macros[$macro]['contexts'][$context] = $db_global_macro['value'];
910					}
911				}
912			}
913
914			foreach ($data as &$element) {
915				foreach ($element['macros'] as $usermacro => &$value) {
916					if ($value['value'] === null && $user_macro_parser->parse($usermacro) == CParser::PARSE_SUCCESS) {
917						$macro = $user_macro_parser->getMacro();
918						$context = $user_macro_parser->getContext();
919
920						if (array_key_exists($macro, $global_macros)) {
921							if ($context !== null && array_key_exists($context, $global_macros[$macro]['contexts'])) {
922								$value['value'] = $global_macros[$macro]['contexts'][$context];
923							}
924							elseif ($global_macros[$macro]['value'] !== null) {
925								if ($context === null) {
926									$value['value'] = $global_macros[$macro]['value'];
927								}
928								elseif ($value['value_default'] === null) {
929									$value['value_default'] = $global_macros[$macro]['value'];
930								}
931							}
932						}
933					}
934				}
935				unset($value);
936			}
937			unset($element);
938		}
939
940		foreach ($data as &$element) {
941			foreach ($element['macros'] as $usermacro => &$value) {
942				if ($value['value'] !== null) {
943					$value = $value['value'];
944				}
945				elseif ($value['value_default'] !== null) {
946					$value = $value['value_default'];
947				}
948				else {
949					// Unresolved macro.
950					$value = $usermacro;
951				}
952			}
953			unset($value);
954		}
955		unset($element);
956
957		return $data;
958	}
959
960	/**
961	 * Get user macro from the requested hosts.
962	 *
963	 * Use the base value returned by host macro as default value when expanding expand global macro. This will ensure
964	 * the following user macro resolving priority:
965	 *  1) host/template context macro
966	 *  2) global context macro
967	 *  3) host/template base (default) macro
968	 *  4) global base (default) macro
969	 *
970	 * @param array  $hostids			The sorted list of hosts where macros will be looked for (hostid => hostid)
971	 * @param string $macro				Macro base without curly braces, for example: SNMP_COMMUNITY
972	 * @param string $context			Macro context to resolve
973	 * @param array  $host_templates	The list of linked templates (see getUserMacros() for more details)
974	 * @param array  $host_macros		The list of macros on hosts (see getUserMacros() for more details)
975	 * @param string $value_default		Value
976	 *
977	 * @return array
978	 */
979	private function getHostUserMacros(array $hostids, $macro, $context, array $host_templates, array $host_macros,
980			$value_default = null) {
981		foreach ($hostids as $hostid) {
982			if (array_key_exists($hostid, $host_macros) && array_key_exists($macro, $host_macros[$hostid])) {
983				if ($context !== null && array_key_exists($context, $host_macros[$hostid][$macro]['contexts'])) {
984					return [
985						'value' => $host_macros[$hostid][$macro]['contexts'][$context],
986						'value_default' => $value_default
987					];
988				}
989
990				if ($host_macros[$hostid][$macro]['value'] !== null) {
991					if ($context === null) {
992						return ['value' => $host_macros[$hostid][$macro]['value'], 'value_default' => $value_default];
993					}
994					elseif ($value_default === null) {
995						$value_default = $host_macros[$hostid][$macro]['value'];
996					}
997				}
998			}
999		}
1000
1001		if (!$host_templates) {
1002			return ['value' => null, 'value_default' => $value_default];
1003		}
1004
1005		$templateids = [];
1006
1007		foreach ($hostids as $hostid) {
1008			if (array_key_exists($hostid, $host_templates)) {
1009				foreach ($host_templates[$hostid] as $templateid) {
1010					$templateids[$templateid] = true;
1011				}
1012			}
1013		}
1014
1015		if ($templateids) {
1016			$templateids = array_keys($templateids);
1017			natsort($templateids);
1018
1019			return $this->getHostUserMacros($templateids, $macro, $context, $host_templates, $host_macros,
1020				$value_default
1021			);
1022		}
1023
1024		return ['value' => null, 'value_default' => $value_default];
1025	}
1026}
1027