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		$values = [];
57		$expression_data = new CTriggerExpression(['collapsed_expression' => true]);
58
59		if (($result = $expression_data->parse($expression)) !== false) {
60			foreach ($result->getTokens() as $token) {
61				switch ($token['type']) {
62					case CTriggerExprParserResult::TOKEN_TYPE_NUMBER:
63					case CTriggerExprParserResult::TOKEN_TYPE_USER_MACRO:
64						$values[] = $token['value'];
65						break;
66
67					case CTriggerExprParserResult::TOKEN_TYPE_STRING:
68						$values[] = $token['data']['string'];
69						break;
70				}
71			}
72		}
73
74		foreach ($references as $macro => $value) {
75			$i = (int) $macro[1] - 1;
76			$references[$macro] = array_key_exists($i, $values) ? $values[$i] : '';
77		}
78
79		return $references;
80	}
81
82	/**
83	 * Checking existence of the macros.
84	 *
85	 * @param array  $texts
86	 * @param array  $type
87	 *
88	 * @return bool
89	 */
90	protected function hasMacros(array $texts, array $types) {
91		foreach ($texts as $text) {
92			if ($this->getMacroPositions($text, $types)) {
93				return true;
94			}
95		}
96
97		return false;
98	}
99
100	/**
101	 * Transform types, used in extractMacros() function to types which can be used in getMacroPositions().
102	 *
103	 * @param array  $types
104	 *
105	 * @return array
106	 */
107	protected function transformToPositionTypes(array $types) {
108		foreach (['macros', 'macros_n', 'macros_an', 'macro_funcs_n'] as $type) {
109			if (array_key_exists($type, $types)) {
110				$patterns = [];
111				foreach ($types[$type] as $key => $_patterns) {
112					$patterns = array_merge($patterns, $_patterns);
113				}
114				$types[$type] = $patterns;
115			}
116		}
117
118		return $types;
119	}
120
121	/**
122	 * Extract positions of the macros from a string.
123	 *
124	 * @param string $text
125	 * @param array  $types
126	 * @param bool   $types['usermacros']
127	 * @param array  $types['macros'][<macro_patterns>]
128	 * @param array  $types['macros_n'][<macro_patterns>]
129	 * @param array  $types['macros_an'][<macro_patterns>]
130	 * @param array  $types['macro_funcs_n'][<macro_patterns>]
131	 * @param bool   $types['references']
132	 * @param bool   $types['lldmacros']
133	 * @param bool   $types['functionids']
134	 * @param bool   $types['replacements']
135	 *
136	 * @return array
137	 */
138	public function getMacroPositions($text, array $types) {
139		$macros = [];
140		$extract_usermacros = array_key_exists('usermacros', $types);
141		$extract_macros = array_key_exists('macros', $types);
142		$extract_macros_n = array_key_exists('macros_n', $types);
143		$extract_macros_an = array_key_exists('macros_an', $types);
144		$extract_macro_funcs_n = array_key_exists('macro_funcs_n', $types);
145		$extract_references = array_key_exists('references', $types);
146		$extract_lldmacros = array_key_exists('lldmacros', $types);
147		$extract_functionids = array_key_exists('functionids', $types);
148		$extract_replacements = array_key_exists('replacements', $types);
149
150		if ($extract_usermacros) {
151			$user_macro_parser = new CUserMacroParser();
152		}
153
154		if ($extract_macros) {
155			$macro_parser = new CMacroParser($types['macros']);
156		}
157
158		if ($extract_macros_n) {
159			$macro_n_parser = new CMacroParser($types['macros_n'], ['ref_type' => CMacroParser::REFERENCE_NUMERIC]);
160		}
161
162		if ($extract_macros_an) {
163			$macro_an_parser = new CMacroParser($types['macros_an'],
164				['ref_type' => CMacroParser::REFERENCE_ALPHANUMERIC]
165			);
166		}
167
168		if ($extract_macro_funcs_n) {
169			$macro_func_n_parser = new CMacroFunctionParser($types['macro_funcs_n'],
170				['ref_type' => CMacroParser::REFERENCE_NUMERIC]
171			);
172		}
173
174		if ($extract_references) {
175			$reference_parser = new CReferenceParser();
176		}
177
178		if ($extract_lldmacros) {
179			$lld_macro_parser = new CLLDMacroParser();
180			$lld_macro_function_parser = new CLLDMacroFunctionParser();
181		}
182
183		if ($extract_functionids) {
184			$functionid_parser = new CFunctionIdParser();
185		}
186
187		if ($extract_replacements) {
188			$replacement_parser = new CReplacementParser();
189		}
190
191		for ($pos = 0; isset($text[$pos]); $pos++) {
192			if ($extract_usermacros && $user_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
193				$macros[$pos] = $user_macro_parser->getMatch();
194				$pos += $user_macro_parser->getLength() - 1;
195			}
196			elseif ($extract_macros && $macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
197				$macros[$pos] = $macro_parser->getMatch();
198				$pos += $macro_parser->getLength() - 1;
199			}
200			elseif ($extract_macros_n && $macro_n_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
201				$macros[$pos] = $macro_n_parser->getMatch();
202				$pos += $macro_n_parser->getLength() - 1;
203			}
204			elseif ($extract_macros_an && $macro_an_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
205				$macros[$pos] = $macro_an_parser->getMatch();
206				$pos += $macro_an_parser->getLength() - 1;
207			}
208			elseif ($extract_macro_funcs_n && $macro_func_n_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
209				$macros[$pos] = $macro_func_n_parser->getMatch();
210				$pos += $macro_func_n_parser->getLength() - 1;
211			}
212			elseif ($extract_references && $reference_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
213				$macros[$pos] = $reference_parser->getMatch();
214				$pos += $reference_parser->getLength() - 1;
215			}
216			elseif ($extract_lldmacros && $lld_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
217				$macros[$pos] = $lld_macro_parser->getMatch();
218				$pos += $lld_macro_parser->getLength() - 1;
219			}
220			elseif ($extract_lldmacros && $lld_macro_function_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
221				$macros[$pos] = $lld_macro_function_parser->getMatch();
222				$pos += $lld_macro_function_parser->getLength() - 1;
223			}
224			elseif ($extract_functionids && $functionid_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
225				$macros[$pos] = $functionid_parser->getMatch();
226				$pos += $functionid_parser->getLength() - 1;
227			}
228			elseif ($extract_replacements && $replacement_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
229				$macros[$pos] = $replacement_parser->getMatch();
230				$pos += $replacement_parser->getLength() - 1;
231			}
232		}
233
234		return $macros;
235	}
236
237	/**
238	 * Extract macros from a string.
239	 *
240	 * @param array  $texts
241	 * @param array  $types
242	 * @param bool   $types['usermacros']
243	 * @param array  $types['macros'][][<macro_patterns>]
244	 * @param array  $types['macros_n'][][<macro_patterns>]
245	 * @param array  $types['macros_an'][][<macro_patterns>]
246	 * @param array  $types['macro_funcs_n'][][<macro_patterns>]
247	 * @param bool   $types['references']
248	 * @param bool   $types['lldmacros']
249	 * @param bool   $types['functionids']
250	 *
251	 * @return array
252	 */
253	public static function extractMacros(array $texts, array $types) {
254		$macros = [];
255		$extract_usermacros = array_key_exists('usermacros', $types);
256		$extract_macros = array_key_exists('macros', $types);
257		$extract_macros_n = array_key_exists('macros_n', $types);
258		$extract_macros_an = array_key_exists('macros_an', $types);
259		$extract_macro_funcs_n = array_key_exists('macro_funcs_n', $types);
260		$extract_references = array_key_exists('references', $types);
261		$extract_lldmacros = array_key_exists('lldmacros', $types);
262		$extract_functionids = array_key_exists('functionids', $types);
263
264		if ($extract_usermacros) {
265			$macros['usermacros'] = [];
266
267			$user_macro_parser = new CUserMacroParser();
268		}
269
270		if ($extract_macros) {
271			$macros['macros'] = [];
272
273			foreach ($types['macros'] as $key => $macro_patterns) {
274				$types['macros'][$key] = new CMacroParser($macro_patterns);
275				$macros['macros'][$key] = [];
276			}
277		}
278
279		if ($extract_macros_n) {
280			$macros['macros_n'] = [];
281
282			foreach ($types['macros_n'] as $key => $macro_patterns) {
283				$types['macros_n'][$key] = new CMacroParser($macro_patterns,
284					['ref_type' => CMacroParser::REFERENCE_NUMERIC]
285				);
286				$macros['macros_n'][$key] = [];
287			}
288		}
289
290		if ($extract_macros_an) {
291			$macros['macros_an'] = [];
292
293			foreach ($types['macros_an'] as $key => $macro_patterns) {
294				$types['macros_an'][$key] = new CMacroParser($macro_patterns,
295					['ref_type' => CMacroParser::REFERENCE_ALPHANUMERIC]
296				);
297				$macros['macros_an'][$key] = [];
298			}
299		}
300
301		if ($extract_macro_funcs_n) {
302			$macros['macro_funcs_n'] = [];
303
304			foreach ($types['macro_funcs_n'] as $key => $macro_patterns) {
305				$types['macro_funcs_n'][$key] = new CMacroFunctionParser($macro_patterns,
306					['ref_type' => CMacroParser::REFERENCE_NUMERIC]
307				);
308				$macros['macro_funcs_n'][$key] = [];
309			}
310		}
311
312		if ($extract_references) {
313			$macros['references'] = [];
314
315			$reference_parser = new CReferenceParser();
316		}
317
318		if ($extract_lldmacros) {
319			$macros['lldmacros'] = [];
320
321			$lld_macro_parser = new CLLDMacroParser();
322			$lld_macro_function_parser = new CLLDMacroFunctionParser();
323		}
324
325		if ($extract_functionids) {
326			$macros['functionids'] = [];
327
328			$functionid_parser = new CFunctionIdParser();
329		}
330
331		foreach ($texts as $text) {
332			for ($pos = 0; isset($text[$pos]); $pos++) {
333				if ($extract_usermacros && $user_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
334					$macros['usermacros'][$user_macro_parser->getMatch()] = null;
335					$pos += $user_macro_parser->getLength() - 1;
336					continue;
337				}
338
339				if ($extract_macros) {
340					foreach ($types['macros'] as $key => $macro_parser) {
341						if ($macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
342							$macros['macros'][$key][$macro_parser->getMatch()] = true;
343							$pos += $macro_parser->getLength() - 1;
344							continue 2;
345						}
346					}
347				}
348
349				if ($extract_macros_n) {
350					foreach ($types['macros_n'] as $key => $macro_n_parser) {
351						if ($macro_n_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
352							$macros['macros_n'][$key][$macro_n_parser->getMatch()] = [
353								'macro' => $macro_n_parser->getMacro(),
354								'f_num' => $macro_n_parser->getReference()
355							];
356							$pos += $macro_n_parser->getLength() - 1;
357							continue 2;
358						}
359					}
360				}
361
362				if ($extract_macros_an) {
363					foreach ($types['macros_an'] as $key => $macro_an_parser) {
364						if ($macro_an_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
365							$macros['macros_an'][$key][$macro_an_parser->getMatch()] = [
366								'macro' => $macro_an_parser->getMacro(),
367								'f_num' => $macro_an_parser->getReference()
368							];
369							$pos += $macro_an_parser->getLength() - 1;
370							continue 2;
371						}
372					}
373				}
374
375				if ($extract_macro_funcs_n) {
376					foreach ($types['macro_funcs_n'] as $key => $macro_func_n_parser) {
377						if ($macro_func_n_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
378							$macro_n_parser = $macro_func_n_parser->getMacroParser();
379							$function_parser = $macro_func_n_parser->getFunctionParser();
380							$function_parameters = [];
381
382							foreach ($function_parser->getParamsRaw()['parameters'] as $param_raw) {
383								switch ($param_raw['type']) {
384									case CFunctionParser::PARAM_UNQUOTED:
385										$function_parameters[] = $param_raw['raw'];
386										break;
387
388									case CFunctionParser::PARAM_QUOTED:
389										$function_parameters[] = CFunctionParser::unquoteParam($param_raw['raw']);
390										break;
391								}
392							}
393
394							$macros['macro_funcs_n'][$key][$macro_func_n_parser->getMatch()] = [
395								'macro' => $macro_n_parser->getMacro(),
396								'f_num' => $macro_n_parser->getReference(),
397								'function' => $function_parser->getFunction(),
398								'parameters' => $function_parameters
399							];
400							$pos += $macro_func_n_parser->getLength() - 1;
401							continue 2;
402						}
403					}
404				}
405
406				if ($extract_references && $reference_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
407					$macros['references'][$reference_parser->getMatch()] = null;
408					$pos += $reference_parser->getLength() - 1;
409					continue;
410				}
411
412				if ($extract_lldmacros) {
413					if ($lld_macro_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
414						$macros['lldmacros'][$lld_macro_parser->getMatch()] = null;
415						$pos += $lld_macro_parser->getLength() - 1;
416						continue;
417					}
418					elseif ($lld_macro_function_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
419						$macros['lldmacros'][$lld_macro_function_parser->getMatch()] = null;
420						$pos += $lld_macro_function_parser->getLength() - 1;
421						continue;
422					}
423				}
424
425				if ($extract_functionids && $functionid_parser->parse($text, $pos) != CParser::PARSE_FAIL) {
426					$macros['functionids'][$functionid_parser->getMatch()] = null;
427					$pos += $functionid_parser->getLength() - 1;
428					continue;
429				}
430			}
431		}
432
433		if ($extract_macros) {
434			foreach ($types['macros'] as $key => $macro_parser) {
435				$macros['macros'][$key] = array_keys($macros['macros'][$key]);
436			}
437		}
438
439		return $macros;
440	}
441
442	/**
443	 * Returns the list of the item key parameters.
444	 *
445	 * @param string $params_raw
446	 *
447	 * @return array
448	 */
449	private function getItemKeyParameters($params_raw) {
450		$item_key_parameters = [];
451
452		foreach ($params_raw as $param_raw) {
453			switch ($param_raw['type']) {
454				case CItemKey::PARAM_ARRAY:
455					$item_key_parameters = array_merge($item_key_parameters,
456						$this->getItemKeyParameters($param_raw['parameters'])
457					);
458					break;
459
460				case CItemKey::PARAM_UNQUOTED:
461					$item_key_parameters[] = $param_raw['raw'];
462					break;
463
464				case CItemKey::PARAM_QUOTED:
465					$item_key_parameters[] = CItemKey::unquoteParam($param_raw['raw']);
466					break;
467			}
468		}
469
470		return $item_key_parameters;
471	}
472
473	/**
474	 * Extract macros from an item key.
475	 *
476	 * @param string $key		an item key
477	 * @param array  $types		the types of macros (see extractMacros() for more details)
478	 *
479	 * @return array			see extractMacros() for more details
480	 */
481	protected function extractItemKeyMacros($key, array $types) {
482		$item_key_parser = new CItemKey();
483
484		$item_key_parameters = [];
485		if ($item_key_parser->parse($key) == CParser::PARSE_SUCCESS) {
486			$item_key_parameters = $this->getItemKeyParameters($item_key_parser->getParamsRaw());
487		}
488
489		return self::extractMacros($item_key_parameters, $types);
490	}
491
492	/**
493	 * Extract macros from a trigger function.
494	 *
495	 * @param string $function	a trigger function, for example 'last({$LAST})'
496	 * @param array  $types		the types of macros (see extractMacros() for more details)
497	 *
498	 * @return array			see extractMacros() for more details
499	 */
500	protected function extractFunctionMacros($function, array $types) {
501		$function_parser = new CFunctionParser();
502
503		$function_parameters = [];
504		if ($function_parser->parse($function) == CParser::PARSE_SUCCESS) {
505			foreach ($function_parser->getParamsRaw()['parameters'] as $param_raw) {
506				switch ($param_raw['type']) {
507					case CFunctionParser::PARAM_UNQUOTED:
508						$function_parameters[] = $param_raw['raw'];
509						break;
510
511					case CFunctionParser::PARAM_QUOTED:
512						$function_parameters[] = CFunctionParser::unquoteParam($param_raw['raw']);
513						break;
514				}
515			}
516		}
517
518		return self::extractMacros($function_parameters, $types);
519	}
520
521	/**
522	 * Resolves macros in the item key parameters.
523	 *
524	 * @param string $key_chain		an item key chain
525	 * @param string $params_raw
526	 * @param array  $macros		the list of macros (['{<MACRO>}' => '<value>', ...])
527	 * @param array  $types			the types of macros (see getMacroPositions() for more details)
528	 *
529	 * @return string
530	 */
531	private function resolveItemKeyParamsMacros($key_chain, array $params_raw, array $macros, array $types) {
532		foreach (array_reverse($params_raw) as $param_raw) {
533			$param = $param_raw['raw'];
534			$forced = false;
535
536			switch ($param_raw['type']) {
537				case CItemKey::PARAM_ARRAY:
538					$param = $this->resolveItemKeyParamsMacros($param, $param_raw['parameters'], $macros, $types);
539					break;
540
541				case CItemKey::PARAM_QUOTED:
542					$param = CItemKey::unquoteParam($param);
543					$forced = true;
544					// break; is not missing here
545
546				case CItemKey::PARAM_UNQUOTED:
547					$matched_macros = $this->getMacroPositions($param, $types);
548
549					foreach (array_reverse($matched_macros, true) as $pos => $macro) {
550						$param = substr_replace($param, $macros[$macro], $pos, strlen($macro));
551					}
552
553					$param = quoteItemKeyParam($param, $forced);
554					break;
555			}
556
557			$key_chain = substr_replace($key_chain, $param, $param_raw['pos'], strlen($param_raw['raw']));
558		}
559
560		return $key_chain;
561	}
562
563	/**
564	 * Resolves macros in the item key.
565	 *
566	 * @param string $key		an item key
567	 * @param array  $macros	the list of macros (['{<MACRO>}' => '<value>', ...])
568	 * @param array  $types		the types of macros (see getMacroPositions() for more details)
569	 *
570	 * @return string
571	 */
572	protected function resolveItemKeyMacros($key, array $macros, array $types) {
573		$item_key_parser = new CItemKey();
574
575		if ($item_key_parser->parse($key) == CParser::PARSE_SUCCESS) {
576			$key = $this->resolveItemKeyParamsMacros($key, $item_key_parser->getParamsRaw(), $macros, $types);
577		}
578
579		return $key;
580	}
581
582	/**
583	 * Resolves macros in the trigger function parameters.
584	 *
585	 * @param string $function	a trigger function
586	 * @param array  $macros	the list of macros (['{<MACRO>}' => '<value>', ...])
587	 * @param array  $types		the types of macros (see getMacroPositions() for more details)
588	 *
589	 * @return string
590	 */
591	protected function resolveFunctionMacros($function, array $macros, array $types) {
592		$function_parser = new CFunctionParser();
593
594		if ($function_parser->parse($function) == CParser::PARSE_SUCCESS) {
595			$params_raw = $function_parser->getParamsRaw();
596			$function_chain = $params_raw['raw'];
597
598			foreach (array_reverse($params_raw['parameters']) as $param_raw) {
599				$param = $param_raw['raw'];
600				$forced = false;
601
602				switch ($param_raw['type']) {
603					case CFunctionParser::PARAM_QUOTED:
604						$param = CFunctionParser::unquoteParam($param);
605						$forced = true;
606						// break; is not missing here
607
608					case CFunctionParser::PARAM_UNQUOTED:
609						$matched_macros = $this->getMacroPositions($param, $types);
610
611						foreach (array_reverse($matched_macros, true) as $pos => $macro) {
612							$param = substr_replace($param, $macros[$macro], $pos, strlen($macro));
613						}
614
615						$param = quoteFunctionParam($param, $forced);
616						break;
617				}
618
619				$function_chain = substr_replace($function_chain, $param, $param_raw['pos'], strlen($param_raw['raw']));
620			}
621
622			$function = substr_replace($function, $function_chain, $params_raw['pos'], strlen($params_raw['raw']));
623		}
624
625		return $function;
626	}
627
628	/**
629	 * Find function ids in trigger expression.
630	 *
631	 * @param string $expression
632	 *
633	 * @return array	where key is function id position in expression and value is function id
634	 */
635	protected function findFunctions($expression) {
636		$functionids = [];
637
638		$functionid_parser = new CFunctionIdParser();
639		$macro_parser = new CMacroParser(['{TRIGGER.VALUE}']);
640		$user_macro_parser = new CUserMacroParser();
641
642		for ($pos = 0, $i = 1; isset($expression[$pos]); $pos++) {
643			if ($functionid_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
644				$pos += $functionid_parser->getLength() - 1;
645				$functionids[$i++] = substr($functionid_parser->getMatch(), 1, -1);
646			}
647			elseif ($user_macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
648				$pos += $user_macro_parser->getLength() - 1;
649			}
650			elseif ($macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
651				$pos += $macro_parser->getLength() - 1;
652			}
653		}
654
655		if (array_key_exists(1, $functionids)) {
656			$functionids[0] = $functionids[1];
657		}
658
659		return $functionids;
660	}
661
662	/**
663	 * Get interface macros.
664	 *
665	 * @param array $macros
666	 * @param array $macros[<functionid>]
667	 * @param array $macros[<functionid>][<macro>]  an array of the tokens
668	 * @param array $macro_values
669	 *
670	 * @return array
671	 */
672	protected function getIpMacros(array $macros, array $macro_values) {
673		if (!$macros) {
674			return $macro_values;
675		}
676
677		$result = DBselect(
678			'SELECT f.triggerid,f.functionid,n.ip,n.dns,n.type,n.useip,n.port'.
679			' FROM functions f'.
680				' JOIN items i ON f.itemid=i.itemid'.
681				' JOIN interface n ON i.hostid=n.hostid'.
682			' WHERE '.dbConditionInt('f.functionid', array_keys($macros)).
683				' AND n.main=1'
684		);
685
686		// Macro should be resolved to interface with highest priority ($priorities).
687		$interfaces = [];
688
689		while ($row = DBfetch($result)) {
690			if (array_key_exists($row['functionid'], $interfaces)
691					&& $this->interfacePriorities[$interfaces[$row['functionid']]['type']]
692						> $this->interfacePriorities[$row['type']]) {
693				continue;
694			}
695
696			$interfaces[$row['functionid']] = $row;
697		}
698
699		foreach ($interfaces as $interface) {
700			foreach ($macros[$interface['functionid']] as $macro => $tokens) {
701				switch ($macro) {
702					case 'IPADDRESS':
703					case 'HOST.IP':
704						$value = $interface['ip'];
705						break;
706					case 'HOST.DNS':
707						$value = $interface['dns'];
708						break;
709					case 'HOST.CONN':
710						$value = $interface['useip'] ? $interface['ip'] : $interface['dns'];
711						break;
712					case 'HOST.PORT':
713						$value = $interface['port'];
714						break;
715				}
716
717				foreach ($tokens as $token) {
718					$macro_values[$interface['triggerid']][$token['token']] = $value;
719				}
720			}
721		}
722
723		return $macro_values;
724	}
725
726	/**
727	 * Get item macros.
728	 *
729	 * @param array $macros
730	 * @param array $macros[<functionid>]
731	 * @param array $macros[<functionid>][<macro>]  An array of the tokens.
732	 * @param array $macro_values
733	 * @param array $triggers
734	 * @param array $options
735	 * @param bool  $options['events]               Resolve {ITEM.VALUE} macro using 'clock' and 'ns' fields.
736	 * @param bool  $options['html]
737	 *
738	 * @return array
739	 */
740	protected function getItemMacros(array $macros, array $macro_values, array $triggers = [], array $options = []) {
741		if (!$macros) {
742			return $macro_values;
743		}
744
745		$options += [
746			'events' => false,
747			'html' => false
748		];
749
750		$functions = DBfetchArray(DBselect(
751			'SELECT f.triggerid,f.functionid,i.itemid,i.hostid,i.name,i.key_,i.value_type,i.units,i.valuemapid'.
752			' FROM functions f'.
753				' JOIN items i ON f.itemid=i.itemid'.
754				' JOIN hosts h ON i.hostid=h.hostid'.
755			' WHERE '.dbConditionInt('f.functionid', array_keys($macros))
756		));
757
758		$functions = CMacrosResolverHelper::resolveItemNames($functions);
759
760		// False passed to DBfetch to get data without null converted to 0, which is done by default.
761		foreach ($functions as $function) {
762			foreach ($macros[$function['functionid']] as $m => $tokens) {
763				$clock = null;
764				$value = null;
765
766				switch ($m) {
767					case 'ITEM.VALUE':
768						if ($options['events']) {
769							$trigger = $triggers[$function['triggerid']];
770							$history = Manager::History()->getValueAt($function, $trigger['clock'], $trigger['ns']);
771
772							if (is_array($history)) {
773								if (array_key_exists('clock', $history)) {
774									$clock = $history['clock'];
775								}
776								if (array_key_exists('value', $history)) {
777									$value = $history['value'];
778								}
779							}
780							break;
781						}
782						// break; is not missing here
783
784					case 'ITEM.LASTVALUE':
785						$history = Manager::History()->getLastValues([$function], 1, ZBX_HISTORY_PERIOD);
786
787						if (array_key_exists($function['itemid'], $history)) {
788							$clock = $history[$function['itemid']][0]['clock'];
789							$value = $history[$function['itemid']][0]['value'];
790						}
791						break;
792				}
793
794				foreach ($tokens as $token) {
795					$macro_value = UNRESOLVED_MACRO_STRING;
796
797					if ($value !== null) {
798						if (array_key_exists('function', $token)) {
799							if ($token['function'] !== 'regsub' && $token['function'] !== 'iregsub') {
800								continue;
801							}
802
803							if (count($token['parameters']) != 2) {
804								continue;
805							}
806
807							$ci = ($token['function'] === 'iregsub') ? 'i' : '';
808
809							set_error_handler(function ($errno, $errstr) {});
810							$rc = preg_match('/'.$token['parameters'][0].'/'.$ci, $value, $matches);
811							restore_error_handler();
812
813							if ($rc === false) {
814								continue;
815							}
816
817							$macro_value = $token['parameters'][1];
818							$matched_macros = $this->getMacroPositions($macro_value, ['replacements' => true]);
819
820							foreach (array_reverse($matched_macros, true) as $pos => $macro) {
821								$macro_value = substr_replace($macro_value,
822									array_key_exists($macro[1], $matches) ? $matches[$macro[1]] : '',
823									$pos, strlen($macro)
824								);
825							}
826						}
827						else {
828							$macro_value = formatHistoryValue($value, $function);
829						}
830					}
831
832					if ($options['html']) {
833						$macro_value = str_replace(["\r\n", "\n"], [" "], $macro_value);
834						$hint_table = (new CTable())
835							->addClass('list-table')
836							->addRow([
837								new CCol($function['name_expanded']),
838								new CCol(
839									($clock !== null)
840										? zbx_date2str(DATE_TIME_FORMAT_SECONDS, $clock)
841										: UNRESOLVED_MACRO_STRING
842								),
843								new CCol($macro_value),
844								new CCol(
845									($function['value_type'] == ITEM_VALUE_TYPE_FLOAT
846											|| $function['value_type'] == ITEM_VALUE_TYPE_UINT64)
847										? new CLink(_('Graph'), (new CUrl('history.php'))
848											->setArgument('action', HISTORY_GRAPH)
849											->setArgument('itemids[]', $function['itemid'])
850											->getUrl()
851										)
852										: new CLink(_('History'), (new CUrl('history.php'))
853											->setArgument('action', HISTORY_VALUES)
854											->setArgument('itemids[]', $function['itemid'])
855											->getUrl()
856										)
857								)
858							]);
859						$macro_value = new CSpan([
860							(new CSpan())
861								->addClass('main-hint')
862								->setHint($hint_table),
863							(new CLinkAction($macro_value))
864								->addClass('hint-item')
865								->setAttribute('data-hintbox', '1')
866						]);
867					}
868
869					$macro_values[$function['triggerid']][$token['token']] = $macro_value;
870				}
871			}
872		}
873
874		return $macro_values;
875	}
876
877	/**
878	 * Get host macros.
879	 *
880	 * @param array $macros
881	 * @param array $macros[<functionid>]
882	 * @param array $macros[<functionid>][<macro>]  an array of the tokens
883	 * @param array $macro_values
884	 *
885	 * @return array
886	 */
887	protected function getHostMacros(array $macros, array $macro_values) {
888		if (!$macros) {
889			return $macro_values;
890		}
891
892		$result = DBselect(
893			'SELECT f.triggerid,f.functionid,h.hostid,h.host,h.name'.
894			' FROM functions f'.
895				' JOIN items i ON f.itemid=i.itemid'.
896				' JOIN hosts h ON i.hostid=h.hostid'.
897			' WHERE '.dbConditionInt('f.functionid', array_keys($macros))
898		);
899
900		while ($row = DBfetch($result)) {
901			foreach ($macros[$row['functionid']] as $macro => $tokens) {
902				switch ($macro) {
903					case 'HOST.ID':
904						$value = $row['hostid'];
905						break;
906
907					case 'HOSTNAME':
908					case 'HOST.HOST':
909						$value = $row['host'];
910						break;
911
912					case 'HOST.NAME':
913						$value = $row['name'];
914						break;
915				}
916
917				foreach ($tokens as $token) {
918					$macro_values[$row['triggerid']][$token['token']] = $value;
919				}
920			}
921		}
922
923		return $macro_values;
924	}
925
926	/**
927	 * Is type available.
928	 *
929	 * @param string $type
930	 *
931	 * @return bool
932	 */
933	protected function isTypeAvailable($type) {
934		return in_array($type, $this->configs[$this->config]['types']);
935	}
936
937	/**
938	 * Get source field.
939	 *
940	 * @return string
941	 */
942	protected function getSource() {
943		return $this->configs[$this->config]['source'];
944	}
945
946	/**
947	 * Get macros with values.
948	 *
949	 * @param array $data
950	 * @param array $data[n]['hostids']  The list of host ids; [<hostid1>, ...].
951	 * @param array $data[n]['macros']   The list of user macros to resolve, ['<usermacro1>' => null, ...].
952	 * @param bool  $unset_undefined     Unset undefined macros.
953	 *
954	 * @return array
955	 */
956	protected function getUserMacros(array $data, bool $unset_undefined = false) {
957		if (!$data) {
958			return $data;
959		}
960
961		// User macros.
962		$hostids = [];
963		foreach ($data as $element) {
964			foreach ($element['hostids'] as $hostid) {
965				$hostids[$hostid] = true;
966			}
967		}
968
969		$user_macro_parser = new CUserMacroParser();
970
971		/*
972		 * @var array $host_templates
973		 * @var array $host_templates[<hostid>]		array of templates
974		 */
975		$host_templates = [];
976
977		/*
978		 * @var array  $host_macros
979		 * @var array  $host_macros[<hostid>]
980		 * @var array  $host_macros[<hostid>][<macro>]				macro base without curly braces
981		 * @var string $host_macros[<hostid>][<macro>]['value']		base macro value (without context and regex);
982		 * 															can be null
983		 * @var array  $host_macros[<hostid>][<macro>]['contexts']	context values; ['<context1>' => '<value1>', ...]
984		 * @var array  $host_macros[<hostid>][<macro>]['regex']		regex values; ['<regex1>' => '<value1>', ...]
985		 */
986		$host_macros = [];
987
988		if ($hostids) {
989			do {
990				$hostids = array_keys($hostids);
991
992				$db_host_macros = API::UserMacro()->get([
993					'output' => ['macro', 'value', 'type', 'hostid'],
994					'hostids' => $hostids
995				]);
996
997				foreach ($db_host_macros as $db_host_macro) {
998					if ($user_macro_parser->parse($db_host_macro['macro']) != CParser::PARSE_SUCCESS) {
999						continue;
1000					}
1001
1002					$hostid = $db_host_macro['hostid'];
1003					$macro = $user_macro_parser->getMacro();
1004					$context = $user_macro_parser->getContext();
1005					$regex = $user_macro_parser->getRegex();
1006					$value = self::getMacroValue($db_host_macro);
1007
1008					if (!array_key_exists($hostid, $host_macros)) {
1009						$host_macros[$hostid] = [];
1010					}
1011
1012					if (!array_key_exists($macro, $host_macros[$hostid])) {
1013						$host_macros[$hostid][$macro] = ['value' => null, 'contexts' => [], 'regex' => []];
1014					}
1015
1016					if ($context === null && $regex === null) {
1017						$host_macros[$hostid][$macro]['value'] = $value;
1018					}
1019					elseif ($regex !== null) {
1020						$host_macros[$hostid][$macro]['regex'][$regex] = $value;
1021					}
1022					else {
1023						$host_macros[$hostid][$macro]['contexts'][$context] = $value;
1024					}
1025				}
1026
1027				foreach ($hostids as $hostid) {
1028					$host_templates[$hostid] = [];
1029				}
1030
1031				$templateids = [];
1032				$db_host_templates = DBselect(
1033					'SELECT ht.hostid,ht.templateid'.
1034					' FROM hosts_templates ht'.
1035					' WHERE '.dbConditionInt('ht.hostid', $hostids)
1036				);
1037				while ($db_host_template = DBfetch($db_host_templates)) {
1038					$host_templates[$db_host_template['hostid']][] = $db_host_template['templateid'];
1039					$templateids[$db_host_template['templateid']] = true;
1040				}
1041
1042				// only unprocessed templates will be populated
1043				$hostids = [];
1044				foreach (array_keys($templateids) as $templateid) {
1045					if (!array_key_exists($templateid, $host_templates)) {
1046						$hostids[$templateid] = true;
1047					}
1048				}
1049			} while ($hostids);
1050		}
1051
1052		// Reordering only regex array.
1053		$host_macros = self::sortRegexHostMacros($host_macros);
1054
1055		$all_macros_resolved = true;
1056
1057		foreach ($data as &$element) {
1058			$hostids = [];
1059			foreach ($element['hostids'] as $hostid) {
1060				$hostids[$hostid] = true;
1061			}
1062
1063			$hostids = array_keys($hostids);
1064			natsort($hostids);
1065
1066			foreach ($element['macros'] as $usermacro => &$value) {
1067				if ($user_macro_parser->parse($usermacro) == CParser::PARSE_SUCCESS) {
1068					$value = $this->getHostUserMacros($hostids, $user_macro_parser->getMacro(),
1069						$user_macro_parser->getContext(), $host_templates, $host_macros
1070					);
1071
1072					if ($value['value'] === null) {
1073						$all_macros_resolved = false;
1074					}
1075				}
1076				else {
1077					// This macro cannot be resolved.
1078					$value = ['value' => $usermacro, 'value_default' => null];
1079				}
1080			}
1081			unset($value);
1082		}
1083		unset($element);
1084
1085		if (!$all_macros_resolved) {
1086			// Global macros.
1087			$db_global_macros = API::UserMacro()->get([
1088				'output' => ['macro', 'value', 'type'],
1089				'globalmacro' => true
1090			]);
1091
1092			/*
1093			 * @var array  $global_macros
1094			 * @var array  $global_macros[<macro>]				macro base without curly braces
1095			 * @var string $global_macros[<macro>]['value']		base macro value (without context and regex);
1096			 * 													can be null
1097			 * @var array  $global_macros[<macro>]['contexts']	context values; ['<context1>' => '<value1>', ...]
1098			 * @var array  $global_macros[<macro>]['regex']		regex values; ['<regex1>' => '<value1>', ...]
1099			 */
1100			$global_macros = [];
1101
1102			foreach ($db_global_macros as $db_global_macro) {
1103				if ($user_macro_parser->parse($db_global_macro['macro']) == CParser::PARSE_SUCCESS) {
1104					$macro = $user_macro_parser->getMacro();
1105					$context = $user_macro_parser->getContext();
1106					$regex = $user_macro_parser->getRegex();
1107					$value = self::getMacroValue($db_global_macro);
1108
1109					if (!array_key_exists($macro, $global_macros)) {
1110						$global_macros[$macro] = ['value' => null, 'contexts' => [], 'regex' => []];
1111					}
1112
1113					if ($context === null && $regex === null) {
1114						$global_macros[$macro]['value'] = $value;
1115					}
1116					elseif ($regex !== null) {
1117						$global_macros[$macro]['regex'][$regex] = $value;
1118					}
1119					else {
1120						$global_macros[$macro]['contexts'][$context] = $value;
1121					}
1122				}
1123			}
1124
1125			// Reordering only regex array.
1126			$global_macros = self::sortRegexGlobalMacros($global_macros);
1127
1128			foreach ($data as &$element) {
1129				foreach ($element['macros'] as $usermacro => &$value) {
1130					if ($value['value'] === null && $user_macro_parser->parse($usermacro) == CParser::PARSE_SUCCESS) {
1131						$macro = $user_macro_parser->getMacro();
1132						$context = $user_macro_parser->getContext();
1133
1134						if (array_key_exists($macro, $global_macros)) {
1135							if ($context !== null && array_key_exists($context, $global_macros[$macro]['contexts'])) {
1136								$value['value'] = $global_macros[$macro]['contexts'][$context];
1137							}
1138							elseif ($context !== null && count($global_macros[$macro]['regex'])) {
1139								foreach ($global_macros[$macro]['regex'] as $regex => $val) {
1140									if (preg_match('/'.strtr(trim($regex, '/'), ['/' => '\\/']).'/', $context) === 1) {
1141										$value['value'] = $val;
1142										break;
1143									}
1144								}
1145							}
1146
1147							if ($value['value'] === null && $global_macros[$macro]['value'] !== null) {
1148								if ($context === null) {
1149									$value['value'] = $global_macros[$macro]['value'];
1150								}
1151								elseif ($value['value_default'] === null) {
1152									$value['value_default'] = $global_macros[$macro]['value'];
1153								}
1154							}
1155						}
1156					}
1157				}
1158				unset($value);
1159			}
1160			unset($element);
1161		}
1162
1163		foreach ($data as $key => $element) {
1164			foreach ($element['macros'] as $usermacro => $value) {
1165				if ($value['value'] !== null) {
1166					$data[$key]['macros'][$usermacro] = $value['value'];
1167				}
1168				elseif ($value['value_default'] !== null) {
1169					$data[$key]['macros'][$usermacro] = $value['value_default'];
1170				}
1171				// Unresolved macro.
1172				elseif ($unset_undefined) {
1173					unset($data[$key]['macros'][$usermacro]);
1174				}
1175				else {
1176					$data[$key]['macros'][$usermacro] = $usermacro;
1177				}
1178			}
1179		}
1180
1181		return $data;
1182	}
1183
1184	/**
1185	 * Get user macro from the requested hosts.
1186	 *
1187	 * Use the base value returned by host macro as default value when expanding expand global macro. This will ensure
1188	 * the following user macro resolving priority:
1189	 *  1) host/template context macro
1190	 *  2) global context macro
1191	 *  3) host/template base (default) macro
1192	 *  4) global base (default) macro
1193	 *
1194	 * @param array  $hostids			The sorted list of hosts where macros will be looked for (hostid => hostid)
1195	 * @param string $macro				Macro base without curly braces, for example: SNMP_COMMUNITY
1196	 * @param string $context			Macro context to resolve
1197	 * @param array  $host_templates	The list of linked templates (see getUserMacros() for more details)
1198	 * @param array  $host_macros		The list of macros on hosts (see getUserMacros() for more details)
1199	 * @param string $value_default		Value
1200	 *
1201	 * @return array
1202	 */
1203	private function getHostUserMacros(array $hostids, $macro, $context, array $host_templates, array $host_macros,
1204			$value_default = null) {
1205		foreach ($hostids as $hostid) {
1206			if (array_key_exists($hostid, $host_macros) && array_key_exists($macro, $host_macros[$hostid])) {
1207				// Searching context coincidence with macro contexts.
1208				if ($context !== null && array_key_exists($context, $host_macros[$hostid][$macro]['contexts'])) {
1209					return [
1210						'value' => $host_macros[$hostid][$macro]['contexts'][$context],
1211						'value_default' => $value_default
1212					];
1213				}
1214				// Searching context coincidence, if regex array not empty.
1215				elseif ($context !== null && count($host_macros[$hostid][$macro]['regex'])) {
1216					foreach ($host_macros[$hostid][$macro]['regex'] as $regex => $val) {
1217						if (preg_match('/'.strtr(trim($regex, '/'), ['/' => '\\/']).'/', $context) === 1) {
1218							return [
1219								'value' => $val,
1220								'value_default' => $value_default
1221							];
1222						}
1223					}
1224				}
1225
1226				if ($host_macros[$hostid][$macro]['value'] !== null) {
1227					if ($context === null) {
1228						return ['value' => $host_macros[$hostid][$macro]['value'], 'value_default' => $value_default];
1229					}
1230					elseif ($value_default === null) {
1231						$value_default = $host_macros[$hostid][$macro]['value'];
1232					}
1233				}
1234			}
1235		}
1236
1237		if (!$host_templates) {
1238			return ['value' => null, 'value_default' => $value_default];
1239		}
1240
1241		$templateids = [];
1242
1243		foreach ($hostids as $hostid) {
1244			if (array_key_exists($hostid, $host_templates)) {
1245				foreach ($host_templates[$hostid] as $templateid) {
1246					$templateids[$templateid] = true;
1247				}
1248			}
1249		}
1250
1251		if ($templateids) {
1252			$templateids = array_keys($templateids);
1253			natsort($templateids);
1254
1255			return $this->getHostUserMacros($templateids, $macro, $context, $host_templates, $host_macros,
1256				$value_default
1257			);
1258		}
1259
1260		return ['value' => null, 'value_default' => $value_default];
1261	}
1262
1263	/**
1264	 * Get macro value refer by type.
1265	 *
1266	 * @static
1267	 *
1268	 * @param array $macro
1269	 *
1270	 * @return string
1271	 */
1272	public static function getMacroValue(array $macro): string {
1273		return ($macro['type'] == ZBX_MACRO_TYPE_SECRET) ? ZBX_SECRET_MASK : $macro['value'];
1274	}
1275
1276	/**
1277	 * Sorting host macros.
1278	 *
1279	 * @param array $host_macros
1280	 *
1281	 * @return array
1282	 */
1283	private static function sortRegexHostMacros(array $host_macros): array {
1284		foreach ($host_macros as &$macros) {
1285			foreach ($macros as &$value) {
1286				$value['regex'] = self::sortRegex($value['regex']);
1287			}
1288			unset($value);
1289		}
1290		unset($macros);
1291
1292		return $host_macros;
1293	}
1294
1295	/**
1296	 * Sorting global macros.
1297	 *
1298	 * @static
1299	 *
1300	 * @param array $global_macros
1301	 *
1302	 * @return array
1303	 */
1304	private static function sortRegexGlobalMacros(array $global_macros): array {
1305		foreach ($global_macros as &$value) {
1306			$value['regex'] = self::sortRegex($value['regex']);
1307		}
1308		unset($value);
1309
1310		return $global_macros;
1311	}
1312
1313	/**
1314	 * Sort regex.
1315	 *
1316	 * @static
1317	 *
1318	 * @param array $macros
1319	 *
1320	 * @return array
1321	 */
1322	private static function sortRegex(array $macros): array {
1323		$keys = array_keys($macros);
1324
1325		usort($keys, 'strcmp');
1326
1327		$new_array = [];
1328
1329		foreach($keys as $key) {
1330			$new_array[$key] = $macros[$key];
1331		}
1332
1333		return $new_array;
1334	}
1335}
1336