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 * Verify that function exists and can be called as a function.
24 *
25 * @param array		$names
26 *
27 * @return bool
28 */
29function zbx_is_callable(array $names) {
30	foreach ($names as $name) {
31		if (!is_callable($name)) {
32			return false;
33		}
34	}
35
36	return true;
37}
38
39/************ REQUEST ************/
40function redirect($url) {
41	$curl = (new CUrl($url))->removeArgument('sid');
42	header('Location: '.$curl->getUrl());
43	exit;
44}
45
46function jsRedirect($url, $timeout = null) {
47	$script = is_numeric($timeout)
48		? 'setTimeout(\'window.location="'.$url.'"\', '.($timeout * 1000).')'
49		: 'window.location.replace("'.$url.'");';
50
51	insert_js($script);
52}
53
54/**
55 * Check if request exist.
56 *
57 * @param string	$name
58 *
59 * @return bool
60 */
61function hasRequest($name) {
62	return isset($_REQUEST[$name]);
63}
64
65/**
66 * Check request, if exist request - return request value, else return default value.
67 *
68 * @param string	$name
69 * @param mixed		$def
70 *
71 * @return mixed
72 */
73function getRequest($name, $def = null) {
74	return isset($_REQUEST[$name]) ? $_REQUEST[$name] : $def;
75}
76
77function countRequest($str = null) {
78	if (!empty($str)) {
79		$count = 0;
80
81		foreach ($_REQUEST as $name => $value) {
82			if (strpos($name, $str) !== false) {
83				$count++;
84			}
85		}
86
87		return $count;
88	}
89	else {
90		return count($_REQUEST);
91	}
92}
93
94/************ COOKIES ************/
95function get_cookie($name, $default_value = null) {
96	if (isset($_COOKIE[$name])) {
97		return $_COOKIE[$name];
98	}
99
100	return $default_value;
101}
102
103function zbx_setcookie($name, $value, $time = null) {
104	setcookie($name, $value, isset($time) ? $time : 0, CSession::getDefaultCookiePath(), null, HTTPS, true);
105	$_COOKIE[$name] = $value;
106}
107
108function zbx_unsetcookie($name) {
109	zbx_setcookie($name, null, -99999);
110	unset($_COOKIE[$name]);
111}
112
113/************* DATE *************/
114function getMonthCaption($num) {
115	switch ($num) {
116		case 1: return _('January');
117		case 2: return _('February');
118		case 3: return _('March');
119		case 4: return _('April');
120		case 5: return _('May');
121		case 6: return _('June');
122		case 7: return _('July');
123		case 8: return _('August');
124		case 9: return _('September');
125		case 10: return _('October');
126		case 11: return _('November');
127		case 12: return _('December');
128	}
129
130	return _s('[Wrong value for month: "%s" ]', $num);
131}
132
133function getDayOfWeekCaption($num) {
134	switch ($num) {
135		case 1: return _('Monday');
136		case 2: return _('Tuesday');
137		case 3: return _('Wednesday');
138		case 4: return _('Thursday');
139		case 5: return _('Friday');
140		case 6: return _('Saturday');
141		case 0:
142		case 7: return _('Sunday');
143	}
144
145	return _s('[Wrong value for day: "%s" ]', $num);
146}
147
148// Convert seconds (0..SEC_PER_WEEK) to string representation. For example, 212400 -> 'Tuesday 11:00'
149function dowHrMinToStr($value, $display24Hours = false) {
150	$dow = $value - $value % SEC_PER_DAY;
151	$hr = $value - $dow;
152	$hr -= $hr % SEC_PER_HOUR;
153	$min = $value - $dow - $hr;
154	$min -= $min % SEC_PER_MIN;
155
156	$dow /= SEC_PER_DAY;
157	$hr /= SEC_PER_HOUR;
158	$min /= SEC_PER_MIN;
159
160	if ($display24Hours && $hr == 0 && $min == 0) {
161		$dow--;
162		$hr = 24;
163	}
164
165	return sprintf('%s %02d:%02d', getDayOfWeekCaption($dow), $hr, $min);
166}
167
168// Convert Day Of Week, Hours and Minutes to seconds representation. For example, 2 11:00 -> 212400. false if error occurred
169function dowHrMinToSec($dow, $hr, $min) {
170	if (zbx_empty($dow) || zbx_empty($hr) || zbx_empty($min) || !zbx_ctype_digit($dow) || !zbx_ctype_digit($hr) || !zbx_ctype_digit($min)) {
171		return false;
172	}
173
174	if ($dow == 7) {
175		$dow = 0;
176	}
177
178	if ($dow < 0 || $dow > 6) {
179		return false;
180	}
181
182	if ($hr < 0 || $hr > 24) {
183		return false;
184	}
185
186	if ($min < 0 || $min > 59) {
187		return false;
188	}
189
190	return $dow * SEC_PER_DAY + $hr * SEC_PER_HOUR + $min * SEC_PER_MIN;
191}
192
193// Convert timestamp to string representation. Return 'Never' if 0.
194function zbx_date2str($format, $value = null) {
195	static $weekdaynames, $weekdaynameslong, $months, $monthslong;
196
197	$prefix = '';
198
199	if ($value === null) {
200		$value = time();
201	}
202	elseif ($value > ZBX_MAX_DATE) {
203		$prefix = '> ';
204		$value = ZBX_MAX_DATE;
205	}
206	elseif (!$value) {
207		return _('Never');
208	}
209
210	if (!is_array($weekdaynames)) {
211		$weekdaynames = [
212			0 => _('Sun'),
213			1 => _('Mon'),
214			2 => _('Tue'),
215			3 => _('Wed'),
216			4 => _('Thu'),
217			5 => _('Fri'),
218			6 => _('Sat')
219		];
220	}
221
222	if (!is_array($weekdaynameslong)) {
223		$weekdaynameslong = [
224			0 => _('Sunday'),
225			1 => _('Monday'),
226			2 => _('Tuesday'),
227			3 => _('Wednesday'),
228			4 => _('Thursday'),
229			5 => _('Friday'),
230			6 => _('Saturday')
231		];
232	}
233
234	if (!is_array($months)) {
235		$months = [
236			1 => _('Jan'),
237			2 => _('Feb'),
238			3 => _('Mar'),
239			4 => _('Apr'),
240			5 => _x('May', 'May short'),
241			6 => _('Jun'),
242			7 => _('Jul'),
243			8 => _('Aug'),
244			9 => _('Sep'),
245			10 => _('Oct'),
246			11 => _('Nov'),
247			12 => _('Dec')
248		];
249	}
250
251	if (!is_array($monthslong)) {
252		$monthslong = [
253			1 => _('January'),
254			2 => _('February'),
255			3 => _('March'),
256			4 => _('April'),
257			5 => _('May'),
258			6 => _('June'),
259			7 => _('July'),
260			8 => _('August'),
261			9 => _('September'),
262			10 => _('October'),
263			11 => _('November'),
264			12 => _('December')
265		];
266	}
267
268	$rplcs = [
269		'l' => $weekdaynameslong[date('w', $value)],
270		'F' => $monthslong[date('n', $value)],
271		'D' => $weekdaynames[date('w', $value)],
272		'M' => $months[date('n', $value)]
273	];
274
275	$output = $part = '';
276	$length = strlen($format);
277
278	for ($i = 0; $i < $length; $i++) {
279		$pchar = ($i > 0) ? substr($format, $i - 1, 1) : '';
280		$char = substr($format, $i, 1);
281
282		if ($pchar != '\\' && isset($rplcs[$char])) {
283			$output .= (strlen($part) ? date($part, $value) : '').$rplcs[$char];
284			$part = '';
285		}
286		else {
287			$part .= $char;
288		}
289	}
290
291	$output .= (strlen($part) > 0) ? date($part, $value) : '';
292
293	return $prefix.$output;
294}
295
296/**
297 * Calculates and converts timestamp to string representation.
298 *
299 * @param int|string $start_date  Start date timestamp.
300 * @param int|string $end_date    End date timestamp.
301 *
302 * @return string
303 */
304function zbx_date2age($start_date, $end_date = 0) {
305	$end_date = ($end_date != 0) ? $end_date : time();
306
307	return convertUnitsS($end_date - $start_date);
308}
309
310function zbxDateToTime($strdate) {
311	if (6 == sscanf($strdate, '%04d%02d%02d%02d%02d%02d', $year, $month, $date, $hours, $minutes, $seconds)) {
312		return mktime($hours, $minutes, $seconds, $month, $date, $year);
313	}
314	elseif (5 == sscanf($strdate, '%04d%02d%02d%02d%02d', $year, $month, $date, $hours, $minutes)) {
315		return mktime($hours, $minutes, 0, $month, $date, $year);
316	}
317	else {
318		return ($strdate && is_numeric($strdate)) ? $strdate : time();
319	}
320}
321
322/**
323 * Correcting adding one unix timestamp to another.
324 *
325 * @param int		$sec
326 * @param mixed		$unixtime	Can accept values:
327 *									1) int - unix timestamp,
328 *									2) string - date in YmdHis or YmdHi formats,
329 *									3) null - current unixtime stamp will be used
330 *
331 * @return int
332 */
333function zbxAddSecondsToUnixtime($sec, $unixtime) {
334	return strtotime('+'.$sec.' seconds', zbxDateToTime($unixtime));
335}
336
337/*************** CONVERTING ******************/
338/**
339 * Convert the Windows new line (CR+LF) to Linux style line feed (LF).
340 *
341 * @param string $string  Input string that will be converted.
342 *
343 * @return string
344 */
345function CRLFtoLF($string) {
346	return str_replace("\r\n", "\n", $string);
347}
348
349function rgb2hex($color) {
350	$HEX = [
351		dechex($color[0]),
352		dechex($color[1]),
353		dechex($color[2])
354	];
355	foreach ($HEX as $id => $value) {
356		if (strlen($value) != 2) {
357			$HEX[$id] = '0'.$value;
358		}
359	}
360
361	return $HEX[0].$HEX[1].$HEX[2];
362}
363
364function hex2rgb($color) {
365	if ($color[0] == '#') {
366		$color = substr($color, 1);
367	}
368
369	if (strlen($color) == 6) {
370		list($r, $g, $b) = [$color[0].$color[1], $color[2].$color[3], $color[4].$color[5]];
371	}
372	elseif (strlen($color) == 3) {
373		list($r, $g, $b) = [$color[0].$color[0], $color[1].$color[1], $color[2].$color[2]];
374	}
375	else {
376		return false;
377	}
378
379	return [hexdec($r), hexdec($g), hexdec($b)];
380}
381
382function getColorVariations($color, $variations_requested = 1) {
383	if ($variations_requested <= 1) {
384		return [$color];
385	}
386
387	$change = hex2rgb('#ffffff'); // Color which is increased/decreased in variations.
388	$max = 50;
389
390	$color = hex2rgb($color);
391	$variations = [];
392
393	$range = range(-1 * $max, $max, $max * 2 / $variations_requested);
394
395	// Remove redundant values.
396	while (count($range) > $variations_requested) {
397		(count($range) % 2) ? array_shift($range) : array_pop($range);
398	}
399
400	// Calculate colors.
401	foreach ($range as $var) {
402		$r = $color[0] + ($change[0] / 100 * $var);
403		$g = $color[1] + ($change[1] / 100 * $var);
404		$b = $color[2] + ($change[2] / 100 * $var);
405
406		$variations[] = '#' . rgb2hex([
407			$r < 0 ? 0 : ($r > 255 ? 255 : (int) $r),
408			$g < 0 ? 0 : ($g > 255 ? 255 : (int) $g),
409			$b < 0 ? 0 : ($b > 255 ? 255 : (int) $b)
410		]);
411	}
412
413	return $variations;
414}
415
416function zbx_num2bitstr($num, $rev = false) {
417	if (!is_numeric($num)) {
418		return 0;
419	}
420
421	$sbin = 0;
422	$strbin = '';
423
424	$len = 32;
425	if (bccomp($num, ZBX_MAX_INT32) > 0) {
426		$len = 64;
427	}
428
429	for ($i = 0; $i < $len; $i++) {
430		$sbin = 1 << $i;
431		$bit = ($sbin & $num) ? '1' : '0';
432		if ($rev) {
433			$strbin .= $bit;
434		}
435		else {
436			$strbin = $bit.$strbin;
437		}
438	}
439
440	return $strbin;
441}
442
443/**
444 * Converts strings like 2M or 5k to bytes.
445 *
446 * @param string $val
447 *
448 * @return int
449 */
450function str2mem($val) {
451	$val = trim($val);
452	$last = strtolower(substr($val, -1));
453	$val = (int) $val;
454
455	switch ($last) {
456		case 'g':
457			$val *= ZBX_GIBIBYTE;
458			break;
459		case 'm':
460			$val *= ZBX_MEBIBYTE;
461			break;
462		case 'k':
463			$val *= ZBX_KIBIBYTE;
464			break;
465	}
466
467	return $val;
468}
469
470/**
471 * Converts bytes into human-readable form.
472 *
473 * @param string|int $size
474 *
475 * @return string
476 */
477function mem2str($size) {
478	$prefix = 'B';
479	if ($size > ZBX_MEBIBYTE) {
480		$size = $size / ZBX_MEBIBYTE;
481		$prefix = 'M';
482	}
483	elseif ($size > ZBX_KIBIBYTE) {
484		$size = $size / ZBX_KIBIBYTE;
485		$prefix = 'K';
486	}
487
488	return round($size, ZBX_UNITS_ROUNDOFF_LOWER_LIMIT).$prefix;
489}
490
491function convertUnitsUptime($value) {
492	if (($secs = round($value)) < 0) {
493		$value = '-';
494		$secs = -$secs;
495	}
496	else {
497		$value = '';
498	}
499
500	$days = floor($secs / SEC_PER_DAY);
501	$secs -= $days * SEC_PER_DAY;
502
503	$hours = floor($secs / SEC_PER_HOUR);
504	$secs -= $hours * SEC_PER_HOUR;
505
506	$mins = floor($secs / SEC_PER_MIN);
507	$secs -= $mins * SEC_PER_MIN;
508
509	if ($days != 0) {
510		$value .= _n('%1$d day', '%1$d days', $days).', ';
511	}
512	$value .= sprintf('%02d:%02d:%02d', $hours, $mins, $secs);
513
514	return $value;
515}
516
517/**
518 * Converts a time period to a human-readable format.
519 *
520 * The following units are used: years, months, days, hours, minutes, seconds and milliseconds.
521 *
522 * Only the three highest units are displayed: #y #m #d, #m #d #h, #d #h #mm and so on.
523 *
524 * If some value is equal to zero, it is omitted. For example, if the period is 1y 0m 4d, it will be displayed as
525 * 1y 4d, not 1y 0m 4d or 1y 4d #h.
526 *
527 * @param int  $value            Time period in seconds.
528 * @param bool $ignore_millisec  Without ms (1s 200 ms = 1.2s).
529 *
530 * @return string
531 */
532function convertUnitsS($value, $ignore_millisec = false) {
533	$secs = round($value * 1000, ZBX_UNITS_ROUNDOFF_UPPER_LIMIT) / 1000;
534	if ($secs < 0) {
535		$secs = -$secs;
536		$str = '-';
537	}
538	else {
539		$str = '';
540	}
541
542	$values = ['y' => null, 'm' => null, 'd' => null, 'h' => null, 'mm' => null, 's' => null, 'ms' => null];
543
544	/*
545	 * $n_unit == 4,	(#y #m #d)
546	 * $n_unit == 3,	(#m #d #h)
547	 * $n_unit == 2,	(#d #h #mm)
548	 * $n_unit == 1,	(#h #mm #s)
549	 * $n_unit == 0,	(#mm #s) or (#mm #s #ms)
550	 */
551	$n_unit = 0;
552
553	$n = floor($secs / SEC_PER_YEAR);
554	if ($n != 0) {
555		$secs -= $n * SEC_PER_YEAR;
556		$n_unit = 4;
557
558		$values['y'] = $n;
559	}
560
561	$n = floor($secs / SEC_PER_MONTH);
562	$secs -= $n * SEC_PER_MONTH;
563
564	if ($n == 12) {
565		$values['y']++;
566	}
567	else {
568		if ($n != 0) {
569			$values['m'] = $n;
570			if ($n_unit == 0) {
571				$n_unit = 3;
572			}
573		}
574
575		$n = floor($secs / SEC_PER_DAY);
576		if ($n != 0) {
577			$secs -= $n * SEC_PER_DAY;
578			$values['d'] = $n;
579			if ($n_unit == 0) {
580				$n_unit = 2;
581			}
582		}
583
584		$n = floor($secs / SEC_PER_HOUR);
585		if ($n_unit < 4 && $n != 0) {
586			$secs -= $n * SEC_PER_HOUR;
587			$values['h'] = $n;
588			if ($n_unit == 0) {
589				$n_unit = 1;
590			}
591		}
592
593		$n = floor($secs / SEC_PER_MIN);
594		if ($n_unit < 3 && $n != 0) {
595			$secs -= $n * SEC_PER_MIN;
596			$values['mm'] = $n;
597		}
598
599		$n = floor($secs);
600		if ($n_unit < 2 && $n != 0) {
601			$secs -= $n;
602			$values['s'] = $n;
603		}
604
605		if ($ignore_millisec) {
606			$n = round($secs, ZBX_UNITS_ROUNDOFF_UPPER_LIMIT);
607			if ($n_unit < 1 && $n != 0) {
608				$values['s'] += $n;
609			}
610		}
611		else {
612			$n = round($secs * 1000, ZBX_UNITS_ROUNDOFF_UPPER_LIMIT);
613			if ($n_unit < 1 && $n != 0) {
614				$values['ms'] = $n;
615			}
616		}
617	}
618
619	$units = [
620		'y' => _x('y', 'year short'),
621		'm' => _x('m', 'month short'),
622		'd' => _x('d', 'day short'),
623		'h' => _x('h', 'hour short'),
624		'mm' => _x('m', 'minute short'),
625		's' => _x('s', 'second short'),
626		'ms' => _x('ms', 'millisecond short')
627	];
628
629	foreach (array_filter($values) as $unit => $value) {
630		$str .= ' '.$value.$units[$unit];
631	}
632
633	return $str ? trim($str) : '0';
634}
635
636/**
637 * Converts value to actual value.
638 * Example:
639 * 	6442450944 B convert to 6 GB
640 *
641 * @param array  $options
642 * @param string $options['value']
643 * @param string $options['units']
644 * @param string $options['convert']
645 * @param string $options['byteStep']
646 * @param string $options['pow']
647 * @param bool   $options['ignoreMillisec']
648 * @param string $options['length']
649 *
650 * @return string
651 */
652function convert_units($options = []) {
653	$defOptions = [
654		'value' => null,
655		'units' => null,
656		'convert' => ITEM_CONVERT_WITH_UNITS,
657		'byteStep' => false,
658		'pow' => false,
659		'ignoreMillisec' => false,
660		'length' => false
661	];
662
663	$options = zbx_array_merge($defOptions, $options);
664
665	// special processing for unix timestamps
666	if ($options['units'] == 'unixtime') {
667		return zbx_date2str(DATE_TIME_FORMAT_SECONDS, $options['value']);
668	}
669
670	// special processing of uptime
671	if ($options['units'] == 'uptime') {
672		return convertUnitsUptime($options['value']);
673	}
674
675	// special processing for seconds
676	if ($options['units'] == 's') {
677		return convertUnitsS($options['value'], $options['ignoreMillisec']);
678	}
679
680	// black list of units that should have no multiplier prefix (K, M, G etc) applied
681	$blackList = ['%', 'ms', 'rpm', 'RPM'];
682
683	// add to the blacklist if unit is prefixed with '!'
684	if ($options['units'] !== null && $options['units'] !== '' && $options['units'][0] === '!') {
685		$options['units'] = substr($options['units'], 1);
686		$blackList[] = $options['units'];
687	}
688
689	// any other unit
690	if (in_array($options['units'], $blackList) || (zbx_empty($options['units'])
691			&& ($options['convert'] == ITEM_CONVERT_WITH_UNITS))) {
692		if (preg_match('/\.\d+$/', $options['value'])) {
693			$format = (abs($options['value']) >= ZBX_UNITS_ROUNDOFF_THRESHOLD)
694				? '%.'.ZBX_UNITS_ROUNDOFF_MIDDLE_LIMIT.'f'
695				: '%.'.ZBX_UNITS_ROUNDOFF_LOWER_LIMIT.'f';
696			$options['value'] = sprintf($format, $options['value']);
697		}
698		$options['value'] = preg_replace('/^([\-0-9]+)(\.)([0-9]*)[0]+$/U', '$1$2$3', $options['value']);
699		$options['value'] = rtrim($options['value'], '.');
700
701		return trim($options['value'].' '.$options['units']);
702	}
703
704	// if one or more items is B or Bps, then Y-scale use base 8 and calculated in bytes
705	if ($options['byteStep']) {
706		$step = ZBX_KIBIBYTE;
707	}
708	else {
709		switch ($options['units']) {
710			case 'Bps':
711			case 'B':
712				$step = ZBX_KIBIBYTE;
713				$options['convert'] = $options['convert'] ? $options['convert'] : ITEM_CONVERT_NO_UNITS;
714				break;
715			case 'b':
716			case 'bps':
717				$options['convert'] = $options['convert'] ? $options['convert'] : ITEM_CONVERT_NO_UNITS;
718			default:
719				$step = 1000;
720		}
721	}
722
723	if ($options['value'] < 0) {
724		$abs = bcmul($options['value'], '-1');
725	}
726	else {
727		$abs = $options['value'];
728	}
729
730	if (bccomp($abs, 1) == -1) {
731		$options['value'] = round($options['value'], ZBX_UNITS_ROUNDOFF_MIDDLE_LIMIT);
732		$options['value'] = ($options['length'] && $options['value'] != 0)
733			? sprintf('%.'.$options['length'].'f',$options['value']) : $options['value'];
734
735		return trim($options['value'].' '.$options['units']);
736	}
737
738	// init intervals
739	static $digitUnits;
740	if (is_null($digitUnits)) {
741		$digitUnits = [];
742	}
743
744	if (!isset($digitUnits[$step])) {
745		$digitUnits[$step] = [
746			['pow' => 0, 'short' => ''],
747			['pow' => 1, 'short' => 'K'],
748			['pow' => 2, 'short' => 'M'],
749			['pow' => 3, 'short' => 'G'],
750			['pow' => 4, 'short' => 'T'],
751			['pow' => 5, 'short' => 'P'],
752			['pow' => 6, 'short' => 'E'],
753			['pow' => 7, 'short' => 'Z'],
754			['pow' => 8, 'short' => 'Y']
755		];
756
757		foreach ($digitUnits[$step] as $dunit => $data) {
758			// skip milli & micro for values without units
759			$digitUnits[$step][$dunit]['value'] = bcpow($step, $data['pow'], 9);
760		}
761	}
762
763
764	$valUnit = ['pow' => 0, 'short' => '', 'value' => $options['value']];
765
766	if ($options['pow'] === false || $options['value'] == 0) {
767		foreach ($digitUnits[$step] as $dnum => $data) {
768			if (bccomp($abs, $data['value']) > -1) {
769				$valUnit = $data;
770			}
771			else {
772				break;
773			}
774		}
775	}
776	else {
777		foreach ($digitUnits[$step] as $data) {
778			if ($options['pow'] == $data['pow']) {
779				$valUnit = $data;
780				break;
781			}
782		}
783	}
784
785	if (round($valUnit['value'], ZBX_UNITS_ROUNDOFF_MIDDLE_LIMIT) > 0) {
786		$valUnit['value'] = bcdiv(sprintf('%.10f',$options['value']), sprintf('%.10f', $valUnit['value'])
787			, ZBX_PRECISION_10);
788	}
789	else {
790		$valUnit['value'] = 0;
791	}
792
793	switch ($options['convert']) {
794		case 0: $options['units'] = trim($options['units']);
795		case 1: $desc = $valUnit['short']; break;
796	}
797
798	$options['value'] = preg_replace('/^([\-0-9]+)(\.)([0-9]*)[0]+$/U','$1$2$3', round($valUnit['value'],
799		ZBX_UNITS_ROUNDOFF_UPPER_LIMIT));
800
801	$options['value'] = rtrim($options['value'], '.');
802
803	// fix negative zero
804	if (bccomp($options['value'], 0) == 0) {
805		$options['value'] = 0;
806	}
807
808	return trim(sprintf('%s %s%s', $options['length']
809		? sprintf('%.'.$options['length'].'f',$options['value'])
810		: $options['value'], $desc, $options['units']));
811}
812
813/**
814 * Convert time format with suffixes to seconds.
815 * Examples:
816 *		10m = 600
817 *		3d = 10800
818 *		-10m = -600
819 *
820 * @param string $time
821 *
822 * @return null|string
823 */
824function timeUnitToSeconds($time) {
825	preg_match('/^(?<sign>[\-+])?(?<number>(\d)+)(?<suffix>['.ZBX_TIME_SUFFIXES.'])?$/', $time, $matches);
826
827	$is_negative = (array_key_exists('sign', $matches) && $matches['sign'] === '-');
828
829	if (!array_key_exists('number', $matches)) {
830		return null;
831	}
832
833	if (array_key_exists('suffix', $matches)) {
834		$time = $matches['number'];
835
836		switch ($matches['suffix']) {
837			case 's':
838				$sec = $time;
839				break;
840			case 'm':
841				$sec = bcmul($time, SEC_PER_MIN);
842				break;
843			case 'h':
844				$sec = bcmul($time, SEC_PER_HOUR);
845				break;
846			case 'd':
847				$sec = bcmul($time, SEC_PER_DAY);
848				break;
849			case 'w':
850				$sec = bcmul($time, SEC_PER_WEEK);
851				break;
852		}
853	}
854	else {
855		$sec = $matches['number'];
856	}
857
858	return $is_negative ? bcmul($sec, -1) : $sec;
859}
860
861/**
862 * Converts value with suffix to actual value.
863 * Supported time suffixes: s, m, h, d, w
864 * Supported metric suffixes: K, M, G, T
865 *
866 * @param string $value
867 * @param int    $scale  The number of digits after the decimal place in the result.
868 *
869 * @return string
870 */
871function convertFunctionValue($value, $scale = 0) {
872	$suffix = substr($value, -1);
873
874	if (ctype_digit($suffix)) {
875		return $value;
876	}
877
878	$value = substr($value, 0, -1);
879
880	switch ($suffix) {
881		case 'm':
882			return bcmul($value, '60', $scale);
883
884		case 'h':
885			return bcmul($value, '3600', $scale);
886
887		case 'd':
888			return bcmul($value, '86400', $scale);
889
890		case 'w':
891			return bcmul($value, '604800', $scale);
892
893		case 'K':
894			return bcmul($value, ZBX_KIBIBYTE, $scale);
895
896		case 'M':
897			return bcmul($value, ZBX_MEBIBYTE, $scale);
898
899		case 'G':
900			return bcmul($value, ZBX_GIBIBYTE, $scale);
901
902		case 'T':
903			return bcmul($value, '1099511627776', $scale);
904
905		case 's':
906		default:
907			return $value;
908	}
909}
910
911/************* ZBX MISC *************/
912
913/**
914 * Swap two values.
915 *
916 * @param mixed $a first value
917 * @param mixed $b second value
918 */
919function zbx_swap(&$a, &$b) {
920	$tmp = $a;
921	$a = $b;
922	$b = $tmp;
923}
924
925function zbx_avg($values) {
926	zbx_value2array($values);
927	$sum = 0;
928	foreach ($values as $value) {
929		$sum = bcadd($sum, $value);
930	}
931
932	return bcdiv($sum, count($values));
933}
934
935/**
936 * Check if every character in given string value is a decimal digit.
937 *
938 * @param string | int   $x Value to check.
939 *
940 * @return boolean
941 */
942function zbx_ctype_digit($x) {
943	return ctype_digit(strval($x));
944}
945
946/**
947 * Returns true if the value is an empty string, empty array or null.
948 *
949 * @deprecated use strict comparison instead
950 *
951 * @param $value
952 *
953 * @return bool
954 */
955function zbx_empty($value) {
956	if ($value === null) {
957		return true;
958	}
959	if (is_array($value) && empty($value)) {
960		return true;
961	}
962	if (is_string($value) && $value === '') {
963		return true;
964	}
965
966	return false;
967}
968
969function zbx_is_int($var) {
970	if (is_int($var)) {
971		return true;
972	}
973
974	if (is_string($var)) {
975		if (function_exists('ctype_digit') && ctype_digit($var) || strcmp(intval($var), $var) == 0) {
976			return true;
977		}
978	}
979	else {
980		if ($var > 0 && zbx_ctype_digit($var)) {
981			return true;
982		}
983	}
984
985	return preg_match("/^\-?\d{1,20}+$/", $var);
986}
987
988/**
989 * Look for two arrays field value and create 3 array lists, one with arrays where field value exists only in first array
990 * second with arrays where field values are only in second array and both where field values are in both arrays.
991 *
992 * @param array  $primary
993 * @param array  $secondary
994 * @param string $field field that is searched in arrays
995 *
996 * @return array
997 */
998function zbx_array_diff(array $primary, array $secondary, $field) {
999	$fields1 = zbx_objectValues($primary, $field);
1000	$fields2 = zbx_objectValues($secondary, $field);
1001
1002	$first = array_diff($fields1, $fields2);
1003	$first = zbx_toHash($first);
1004
1005	$second = array_diff($fields2, $fields1);
1006	$second = zbx_toHash($second);
1007
1008	$result = [
1009		'first' => [],
1010		'second' => [],
1011		'both' => []
1012	];
1013
1014	foreach ($primary as $array) {
1015		if (!isset($array[$field])) {
1016			$result['first'][] = $array;
1017		}
1018		elseif (isset($first[$array[$field]])) {
1019			$result['first'][] = $array;
1020		}
1021		else {
1022			$result['both'][$array[$field]] = $array;
1023		}
1024	}
1025
1026	foreach ($secondary as $array) {
1027		if (!isset($array[$field])) {
1028			$result['second'][] = $array;
1029		}
1030		elseif (isset($second[$array[$field]])) {
1031			$result['second'][] = $array;
1032		}
1033	}
1034
1035	return $result;
1036}
1037
1038function zbx_array_push(&$array, $add) {
1039	foreach ($array as $key => $value) {
1040		foreach ($add as $newKey => $newValue) {
1041			$array[$key][$newKey] = $newValue;
1042		}
1043	}
1044}
1045
1046/**
1047 * Find if array has any duplicate values and return an array with info about them.
1048 * In case of no duplicates, empty array is returned.
1049 * Example of usage:
1050 *     $result = zbx_arrayFindDuplicates(
1051 *         array('a', 'b', 'c', 'c', 'd', 'd', 'd', 'e')
1052 *     );
1053 *     array(
1054 *         'd' => 3,
1055 *         'c' => 2,
1056 *     )
1057 *
1058 * @param array $array
1059 *
1060 * @return array
1061 */
1062function zbx_arrayFindDuplicates(array $array) {
1063	$countValues = array_count_values($array); // counting occurrences of every value in array
1064	foreach ($countValues as $value => $count) {
1065		if ($count <= 1) {
1066			unset($countValues[$value]);
1067		}
1068	}
1069	arsort($countValues); // sorting, so that the most duplicates would be at the top
1070
1071	return $countValues;
1072}
1073
1074/************* STRING *************/
1075function zbx_nl2br($str) {
1076	$str_res = [];
1077	foreach (explode("\n", $str) as $str_line) {
1078		array_push($str_res, $str_line, BR());
1079	}
1080	array_pop($str_res);
1081
1082	return $str_res;
1083}
1084
1085function zbx_formatDomId($value) {
1086	return str_replace(['[', ']'], ['_', ''], $value);
1087}
1088
1089/**
1090 * Sort an array of objects so that the objects whose $column value matches $pattern are at the top.
1091 * Return the first $limit objects.
1092 *
1093 * @param array 	$table		array of objects to sort
1094 * @param string 	$column		name of the $column to search
1095 * @param string 	$pattern	string to match the value of $column against
1096 * @param int		$limit		number of objects to return
1097 *
1098 * @return array
1099 */
1100function selectByPattern(array $table, $column, $pattern, $limit) {
1101	$chunk_size = $limit;
1102
1103	$rsTable = [];
1104	foreach ($table as $num => $row) {
1105		if (mb_strtolower($row[$column]) === mb_strtolower($pattern)) {
1106			$rsTable = [$num => $row] + $rsTable;
1107		}
1108		elseif ($limit > 0) {
1109			$rsTable[$num] = $row;
1110		}
1111		else {
1112			continue;
1113		}
1114		$limit--;
1115	}
1116
1117	if (!empty($rsTable)) {
1118		$rsTable = array_chunk($rsTable, $chunk_size, true);
1119		$rsTable = $rsTable[0];
1120	}
1121
1122	return $rsTable;
1123}
1124
1125/************* SORT *************/
1126function natksort(&$array) {
1127	$keys = array_keys($array);
1128	natcasesort($keys);
1129
1130	$new_array = [];
1131
1132	foreach ($keys as $k) {
1133		$new_array[$k] = $array[$k];
1134	}
1135
1136	$array = $new_array;
1137
1138	return true;
1139}
1140
1141// recursively sort an array by key
1142function zbx_rksort(&$array, $flags = null) {
1143	if (is_array($array)) {
1144		foreach ($array as $id => $data) {
1145			zbx_rksort($array[$id]);
1146		}
1147		ksort($array, $flags);
1148	}
1149
1150	return $array;
1151}
1152
1153/**
1154 * Sorts the data using a natural sort algorithm.
1155 *
1156 * Not suitable for sorting macros, use order_macros() instead.
1157 *
1158 * @param $data
1159 * @param null $sortfield
1160 * @param string $sortorder
1161 *
1162 * @return bool
1163 *
1164 * @see order_macros()
1165 */
1166function order_result(&$data, $sortfield = null, $sortorder = ZBX_SORT_UP) {
1167	if (empty($data)) {
1168		return false;
1169	}
1170
1171	if (is_null($sortfield)) {
1172		natcasesort($data);
1173		if ($sortorder != ZBX_SORT_UP) {
1174			$data = array_reverse($data, true);
1175		}
1176		return true;
1177	}
1178
1179	$sort = [];
1180	foreach ($data as $key => $arr) {
1181		if (!isset($arr[$sortfield])) {
1182			return false;
1183		}
1184		$sort[$key] = $arr[$sortfield];
1185	}
1186	natcasesort($sort);
1187
1188	if ($sortorder != ZBX_SORT_UP) {
1189		$sort = array_reverse($sort, true);
1190	}
1191
1192	$tmp = $data;
1193	$data = [];
1194	foreach ($sort as $key => $val) {
1195		$data[$key] = $tmp[$key];
1196	}
1197
1198	return true;
1199}
1200
1201/**
1202 * Sorts the macros in the given order. Supports user and LLD macros.
1203 *
1204 * order_result() is not suitable for sorting macros, because it treats the "}" as a symbol with a lower priority
1205 * then any alphanumeric character, and the result will be invalid.
1206 *
1207 * E.g: order_result() will sort array('{$DD}', '{$D}', '{$D1}') as
1208 * array('{$D1}', '{$DD}', '{$D}') while the correct result is array('{$D}', '{$D1}', '{$DD}').
1209 *
1210 * @param array $macros
1211 * @param string $sortfield
1212 * @param string $order
1213 *
1214 * @return array
1215 */
1216function order_macros(array $macros, $sortfield, $order = ZBX_SORT_UP) {
1217	$temp = [];
1218	foreach ($macros as $key => $macro) {
1219		$temp[$key] = substr($macro[$sortfield], 2, strlen($macro[$sortfield]) - 3);
1220	}
1221	order_result($temp, null, $order);
1222
1223	$rs = [];
1224	foreach ($temp as $key => $macroLabel) {
1225		$rs[$key] = $macros[$key];
1226	}
1227
1228	return $rs;
1229}
1230
1231// preserve keys
1232function zbx_array_merge() {
1233	$args = func_get_args();
1234	$result = [];
1235	foreach ($args as &$array) {
1236		if (!is_array($array)) {
1237			return false;
1238		}
1239		foreach ($array as $key => $value) {
1240			$result[$key] = $value;
1241		}
1242	}
1243	unset($array);
1244
1245	return $result;
1246}
1247
1248function uint_in_array($needle, $haystack) {
1249	foreach ($haystack as $value) {
1250		if (bccomp($needle, $value) == 0) {
1251			return true;
1252		}
1253	}
1254
1255	return false;
1256}
1257
1258function str_in_array($needle, $haystack, $strict = false) {
1259	if (is_array($needle)) {
1260		return in_array($needle, $haystack, $strict);
1261	}
1262	elseif ($strict) {
1263		foreach ($haystack as $value) {
1264			if ($needle === $value) {
1265				return true;
1266			}
1267		}
1268	}
1269	else {
1270		foreach ($haystack as $value) {
1271			if (strcmp($needle, $value) == 0) {
1272				return true;
1273			}
1274		}
1275	}
1276
1277	return false;
1278}
1279
1280function zbx_value2array(&$values) {
1281	if (!is_array($values) && !is_null($values)) {
1282		$tmp = [];
1283		if (is_object($values)) {
1284			$tmp[] = $values;
1285		}
1286		else {
1287			$tmp[$values] = $values;
1288		}
1289		$values = $tmp;
1290	}
1291}
1292
1293// creates chain of relation parent -> child, for all chain levels
1294function createParentToChildRelation(&$chain, $link, $parentField, $childField) {
1295	if (!isset($chain[$link[$parentField]])) {
1296		$chain[$link[$parentField]] = [];
1297	}
1298
1299	$chain[$link[$parentField]][$link[$childField]] = $link[$childField];
1300	if (isset($chain[$link[$childField]])) {
1301		$chain[$link[$parentField]] = zbx_array_merge($chain[$link[$parentField]], $chain[$link[$childField]]);
1302	}
1303}
1304
1305// object or array of objects to hash
1306function zbx_toHash($value, $field = null) {
1307	if (is_null($value)) {
1308		return $value;
1309	}
1310	$result = [];
1311
1312	if (!is_array($value)) {
1313		$result = [$value => $value];
1314	}
1315	elseif (isset($value[$field])) {
1316		$result[$value[$field]] = $value;
1317	}
1318	else {
1319		foreach ($value as $val) {
1320			if (!is_array($val)) {
1321				$result[$val] = $val;
1322			}
1323			elseif (isset($val[$field])) {
1324				$result[$val[$field]] = $val;
1325			}
1326		}
1327	}
1328
1329	return $result;
1330}
1331
1332/**
1333 * Transforms a single or an array of values to an array of objects, where the values are stored under the $field
1334 * key.
1335 *
1336 * E.g:
1337 * zbx_toObject(array(1, 2), 'hostid')            // returns array(array('hostid' => 1), array('hostid' => 2))
1338 * zbx_toObject(3, 'hostid')                      // returns array(array('hostid' => 3))
1339 * zbx_toObject(array('a' => 1), 'hostid', true)  // returns array('a' => array('hostid' => 1))
1340 *
1341 * @param $value
1342 * @param $field
1343 * @param $preserve_keys
1344 *
1345 * @return array
1346 */
1347function zbx_toObject($value, $field, $preserve_keys = false) {
1348	if (is_null($value)) {
1349		return $value;
1350	}
1351	$result = [];
1352
1353	// Value or Array to Object or Array of objects
1354	if (!is_array($value)) {
1355		$result = [[$field => $value]];
1356	}
1357	elseif (!isset($value[$field])) {
1358		foreach ($value as $key => $val) {
1359			if (!is_array($val)) {
1360				$result[$key] = [$field => $val];
1361			}
1362		}
1363
1364		if (!$preserve_keys) {
1365			$result = array_values($result);
1366		}
1367	}
1368
1369	return $result;
1370}
1371
1372/**
1373 * Converts the given value to a numeric array:
1374 * - a scalar value will be converted to an array and added as the only element;
1375 * - an array with first element key containing only numeric characters will be converted to plain zero-based numeric array.
1376 * This is used for resetting nonsequential numeric arrays;
1377 * - an associative array will be returned in an array as the only element, except if first element key contains only numeric characters.
1378 *
1379 * @param mixed $value
1380 *
1381 * @return array
1382 */
1383function zbx_toArray($value) {
1384	if ($value === null) {
1385		return $value;
1386	}
1387
1388	if (is_array($value)) {
1389		// reset() is needed to move internal array pointer to the beginning of the array
1390		reset($value);
1391
1392		if (zbx_ctype_digit(key($value))) {
1393			$result = array_values($value);
1394		}
1395		elseif (!empty($value)) {
1396			$result = [$value];
1397		}
1398		else {
1399			$result = [];
1400		}
1401	}
1402	else {
1403		$result = [$value];
1404	}
1405
1406	return $result;
1407}
1408
1409// value OR object OR array of objects TO an array
1410function zbx_objectValues($value, $field) {
1411	if (is_null($value)) {
1412		return $value;
1413	}
1414
1415	if (!is_array($value)) {
1416		$result = [$value];
1417	}
1418	elseif (isset($value[$field])) {
1419		$result = [$value[$field]];
1420	}
1421	else {
1422		$result = [];
1423
1424		foreach ($value as $val) {
1425			if (!is_array($val)) {
1426				$result[] = $val;
1427			}
1428			elseif (isset($val[$field])) {
1429				$result[] = $val[$field];
1430			}
1431		}
1432	}
1433
1434	return $result;
1435}
1436
1437function zbx_cleanHashes(&$value) {
1438	if (is_array($value)) {
1439		// reset() is needed to move internal array pointer to the beginning of the array
1440		reset($value);
1441		if (zbx_ctype_digit(key($value))) {
1442			$value = array_values($value);
1443		}
1444	}
1445
1446	return $value;
1447}
1448
1449function zbx_toCSV($values) {
1450	$csv = '';
1451	$glue = '","';
1452	foreach ($values as $row) {
1453		if (!is_array($row)) {
1454			$row = [$row];
1455		}
1456		foreach ($row as $num => $value) {
1457			if (is_null($value)) {
1458				unset($row[$num]);
1459			}
1460			else {
1461				$row[$num] = str_replace('"', '""', $value);
1462			}
1463		}
1464		$csv .= '"'.implode($glue, $row).'"'."\n";
1465	}
1466
1467	return $csv;
1468}
1469
1470function zbx_array_mintersect($keys, $array) {
1471	$result = [];
1472
1473	foreach ($keys as $field) {
1474		if (is_array($field)) {
1475			foreach ($field as $sub_field) {
1476				if (isset($array[$sub_field])) {
1477					$result[$sub_field] = $array[$sub_field];
1478					break;
1479				}
1480			}
1481		}
1482		elseif (isset($array[$field])) {
1483			$result[$field] = $array[$field];
1484		}
1485	}
1486
1487	return $result;
1488}
1489
1490function zbx_str2links($text) {
1491	$result = [];
1492
1493	foreach (explode("\n", $text) as $line) {
1494		$line = rtrim($line, "\r ");
1495
1496		preg_match_all('#https?://[^\n\t\r ]+#u', $line, $matches);
1497
1498		$start = 0;
1499
1500		foreach ($matches[0] as $match) {
1501			if (($pos = mb_strpos($line, $match, $start)) !== false) {
1502				if ($pos != $start) {
1503					$result[] = mb_substr($line, $start, $pos - $start);
1504				}
1505				$result[] = new CLink(CHtml::encode($match), $match);
1506				$start = $pos + mb_strlen($match);
1507			}
1508		}
1509
1510		if (mb_strlen($line) != $start) {
1511			$result[] = mb_substr($line, $start);
1512		}
1513
1514		$result[] = BR();
1515	}
1516
1517	array_pop($result);
1518
1519	return $result;
1520}
1521
1522function zbx_subarray_push(&$mainArray, $sIndex, $element = null, $key = null) {
1523	if (!isset($mainArray[$sIndex])) {
1524		$mainArray[$sIndex] = [];
1525	}
1526	if ($key) {
1527		$mainArray[$sIndex][$key] = is_null($element) ? $sIndex : $element;
1528	}
1529	else {
1530		$mainArray[$sIndex][] = is_null($element) ? $sIndex : $element;
1531	}
1532}
1533
1534/*************** PAGE SORTING ******************/
1535
1536/**
1537 * Returns header with sorting options.
1538 *
1539 * @param string obj			Header item.
1540 * @param string $tabfield		Table field.
1541 * @param string $sortField		Sorting field.
1542 * @param string $sortOrder		Sorting order.
1543 * @param string $link			Sorting link.
1544 *
1545 * @return CColHeader
1546 */
1547function make_sorting_header($obj, $tabfield, $sortField, $sortOrder, $link = null) {
1548	$sortorder = ($sortField == $tabfield && $sortOrder == ZBX_SORT_UP) ? ZBX_SORT_DOWN : ZBX_SORT_UP;
1549
1550	$link = CUrlFactory::getContextUrl($link);
1551
1552	$link->setArgument('sort', $tabfield);
1553	$link->setArgument('sortorder', $sortorder);
1554
1555	zbx_value2array($obj);
1556
1557	$arrow = null;
1558	if ($tabfield == $sortField) {
1559		if ($sortorder == ZBX_SORT_UP) {
1560			$arrow = (new CSpan())->addClass(ZBX_STYLE_ARROW_DOWN);
1561		}
1562		else {
1563			$arrow = (new CSpan())->addClass(ZBX_STYLE_ARROW_UP);
1564		}
1565	}
1566
1567	return new CColHeader(new CLink([$obj, $arrow], $link->getUrl()));
1568}
1569
1570/**
1571 * Returns the list page number for the current page.
1572 *
1573 * The functions first looks for a page number in the HTTP request. If no number is given, falls back to the profile.
1574 * Defaults to 1.
1575 *
1576 * @return int
1577 */
1578function getPageNumber() {
1579	global $page;
1580
1581	$pageNumber = getRequest('page');
1582	if (!$pageNumber) {
1583		$lastPage = CProfile::get('web.paging.lastpage');
1584		// For MVC pages $page is not set so we use action instead
1585		if (isset($page['file']) && $lastPage == $page['file']) {
1586			$pageNumber = CProfile::get('web.paging.page', 1);
1587		}
1588		elseif (isset($_REQUEST['action']) && $lastPage == $_REQUEST['action']) {
1589			$pageNumber = CProfile::get('web.paging.page', 1);
1590		}
1591		else {
1592			$pageNumber = 1;
1593		}
1594	}
1595
1596	return $pageNumber;
1597}
1598
1599/**
1600 * Returns paging line and recursively slice $items of current page.
1601 *
1602 * @param array  $items				list of elements
1603 * @param string $sortorder			the order in which items are sorted ASC or DESC
1604 * @param CUrl $url					URL object containing arguments and query
1605 *
1606 * @return CDiv
1607 */
1608function getPagingLine(&$items, $sortorder, CUrl $url) {
1609	global $page;
1610
1611	$rowsPerPage = (int) CWebUser::$data['rows_per_page'];
1612	$config = select_config();
1613
1614	$itemsCount = count($items);
1615	$limit_exceeded = ($config['search_limit'] < $itemsCount);
1616	$offset = 0;
1617
1618	if ($limit_exceeded) {
1619		if ($sortorder == ZBX_SORT_DOWN) {
1620			$offset = $itemsCount - $config['search_limit'];
1621		}
1622		$itemsCount = $config['search_limit'];
1623	}
1624
1625	$pagesCount = ($itemsCount > 0) ? ceil($itemsCount / $rowsPerPage) : 1;
1626	$currentPage = getPageNumber();
1627
1628	if ($currentPage < 1) {
1629		$currentPage = 1;
1630	}
1631	elseif ($currentPage > $pagesCount) {
1632		$currentPage = $pagesCount;
1633	}
1634
1635	$tags = [];
1636
1637	if ($pagesCount > 1) {
1638		// For MVC pages $page is not set
1639		if (isset($page['file'])) {
1640			CProfile::update('web.paging.lastpage', $page['file'], PROFILE_TYPE_STR);
1641			CProfile::update('web.paging.page', $currentPage, PROFILE_TYPE_INT);
1642		}
1643		elseif (isset($_REQUEST['action'])) {
1644			CProfile::update('web.paging.lastpage', $_REQUEST['action'], PROFILE_TYPE_STR);
1645			CProfile::update('web.paging.page', $currentPage, PROFILE_TYPE_INT);
1646		}
1647
1648		// viewed pages (better to use odd)
1649		$pagingNavRange = 11;
1650
1651		$endPage = $currentPage + floor($pagingNavRange / 2);
1652		if ($endPage < $pagingNavRange) {
1653			$endPage = $pagingNavRange;
1654		}
1655		if ($endPage > $pagesCount) {
1656			$endPage = $pagesCount;
1657		}
1658
1659		$startPage = ($endPage > $pagingNavRange) ? $endPage - $pagingNavRange + 1 : 1;
1660
1661		if ($startPage > 1) {
1662			$url->setArgument('page', 1);
1663			$tags[] = new CLink(_x('First', 'page navigation'), $url->getUrl());
1664		}
1665
1666		if ($currentPage > 1) {
1667			$url->setArgument('page', $currentPage - 1);
1668			$tags[] = new CLink(
1669				(new CSpan())->addClass(ZBX_STYLE_ARROW_LEFT), $url->getUrl()
1670			);
1671		}
1672
1673		for ($p = $startPage; $p <= $endPage; $p++) {
1674			$url->setArgument('page', $p);
1675			$link = new CLink($p, $url->getUrl());
1676			if ($p == $currentPage) {
1677				$link->addClass(ZBX_STYLE_PAGING_SELECTED);
1678			}
1679
1680			$tags[] = $link;
1681		}
1682
1683		if ($currentPage < $pagesCount) {
1684			$url->setArgument('page', $currentPage + 1);
1685			$tags[] = new CLink((new CSpan())->addClass(ZBX_STYLE_ARROW_RIGHT), $url->getUrl());
1686		}
1687
1688		if ($p < $pagesCount) {
1689			$url->setArgument('page', $pagesCount);
1690			$tags[] = new CLink(_x('Last', 'page navigation'), $url->getUrl());
1691		}
1692	}
1693
1694	$total = $limit_exceeded ? $itemsCount.'+' : $itemsCount;
1695	$start = ($currentPage - 1) * $rowsPerPage;
1696	$end = $start + $rowsPerPage;
1697
1698	if ($end > $itemsCount) {
1699		$end = $itemsCount;
1700	}
1701
1702	if ($pagesCount == 1) {
1703		$table_stats = _s('Displaying %1$s of %2$s found', $itemsCount, $total);
1704	}
1705	else {
1706		$table_stats = _s('Displaying %1$s to %2$s of %3$s found', $start + 1, $end, $total);
1707	}
1708
1709	// Trim array with elements to contain elements for current page.
1710	$items = array_slice($items, $start + $offset, $end - $start, true);
1711
1712	return (new CDiv())
1713		->addClass(ZBX_STYLE_TABLE_PAGING)
1714		->addItem(
1715			(new CDiv())
1716				->addClass(ZBX_STYLE_PAGING_BTN_CONTAINER)
1717				->addItem($tags)
1718				->addItem(
1719					(new CDiv())
1720						->addClass(ZBX_STYLE_TABLE_STATS)
1721						->addItem($table_stats)
1722				)
1723		);
1724}
1725
1726/************* MATH *************/
1727function bcfloor($number) {
1728	if (strpos($number, '.') !== false) {
1729		if (($tmp = preg_replace('/\.0+$/', '', $number)) !== $number) {
1730			$number = $tmp;
1731		}
1732		elseif ($number[0] != '-') {
1733			$number = bcadd($number, 0, 0);
1734		}
1735		else {
1736			$number = bcsub($number, 1, 0);
1737		}
1738	}
1739
1740	return $number == '-0' ? '0' : $number;
1741}
1742
1743function bcceil($number) {
1744	if (strpos($number, '.') !== false) {
1745		if (($tmp = preg_replace('/\.0+$/', '', $number)) !== $number) {
1746			$number = $tmp;
1747		}
1748		elseif ($number[0] != '-') {
1749			$number = bcadd($number, 1, 0);
1750		}
1751		else {
1752			$number = bcsub($number, 0, 0);
1753		}
1754	}
1755
1756	return $number == '-0' ? '0' : $number;
1757}
1758
1759/**
1760 * Converts number to letter representation.
1761 * From A to Z, then from AA to ZZ etc.
1762 * Example: 0 => A, 25 => Z, 26 => AA, 27 => AB, 52 => BA, ...
1763 *
1764 * Keep in sync with JS num2letter().
1765 *
1766 * @param int $number
1767 *
1768 * @return string
1769 */
1770function num2letter($number) {
1771	$start = ord('A');
1772	$base = 26;
1773	$str = '';
1774	$level = 0;
1775
1776	do {
1777		if ($level++ > 0) {
1778			$number--;
1779		}
1780		$remainder = $number % $base;
1781		$number = ($number - $remainder) / $base;
1782		$str = chr($start + $remainder).$str;
1783	} while (0 != $number);
1784
1785	return $str;
1786}
1787
1788/**
1789 * Renders an "access denied" message and stops the execution of the script.
1790 *
1791 * The $mode parameters controls the layout of the message for logged in users:
1792 * - ACCESS_DENY_OBJECT     - render the message when denying access to a specific object
1793 * - ACCESS_DENY_PAGE       - render a complete access denied page
1794 *
1795 * If visitor is without any access permission then layout of the message is same as in ACCESS_DENY_PAGE mode.
1796 *
1797 * @param int $mode
1798 */
1799function access_deny($mode = ACCESS_DENY_OBJECT) {
1800	// deny access to an object
1801	if ($mode == ACCESS_DENY_OBJECT && CWebUser::isLoggedIn()) {
1802		require_once dirname(__FILE__).'/page_header.php';
1803		show_error_message(_('No permissions to referred object or it does not exist!'));
1804		require_once dirname(__FILE__).'/page_footer.php';
1805	}
1806	// deny access to a page
1807	else {
1808		// url to redirect the user to after he logs in
1809		$url = (new CUrl(!empty($_REQUEST['request']) ? $_REQUEST['request'] : ''))->removeArgument('sid');
1810		$config = select_config();
1811
1812		if ($config['http_login_form'] == ZBX_AUTH_FORM_HTTP && $config['http_auth_enabled'] == ZBX_AUTH_HTTP_ENABLED
1813				&& (!CWebUser::isLoggedIn() || CWebUser::isGuest())) {
1814			$redirect_to = (new CUrl('index_http.php'))->setArgument('request', $url->toString());
1815			redirect($redirect_to->toString());
1816
1817			exit;
1818		}
1819
1820		$url = urlencode($url->toString());
1821
1822		// if the user is logged in - render the access denied message
1823		if (CWebUser::isLoggedIn()) {
1824			$data = [
1825				'header' => _('Access denied'),
1826				'messages' => [
1827					_s('You are logged in as "%1$s".', CWebUser::$data['alias']).' '._('You have no permissions to access this page.'),
1828					_('If you think this message is wrong, please consult your administrators about getting the necessary permissions.')
1829				],
1830				'buttons' => []
1831			];
1832
1833			// display the login button only for guest users
1834			if (CWebUser::isGuest()) {
1835				$data['buttons'][] = (new CButton('login', _('Login')))
1836					->onClick('javascript: document.location = "index.php?request='.$url.'";');
1837			}
1838			$data['buttons'][] = (new CButton('back', _('Go to dashboard')))
1839				->onClick('javascript: document.location = "zabbix.php?action=dashboard.view"');
1840		}
1841		// if the user is not logged in - offer to login
1842		else {
1843			$data = [
1844				'header' => _('You are not logged in'),
1845				'messages' => [
1846					_('You must login to view this page.'),
1847					_('If you think this message is wrong, please consult your administrators about getting the necessary permissions.')
1848				],
1849				'buttons' => [
1850					(new CButton('login', _('Login')))->onClick('javascript: document.location = "index.php?request='.$url.'";')
1851				]
1852			];
1853		}
1854
1855		$data['theme'] = getUserTheme(CWebUser::$data);
1856
1857		if (detect_page_type() == PAGE_TYPE_JS) {
1858			(new CView('layout.json', ['main_block' => json_encode(['error' => $data['header']])]))->render();
1859		}
1860		else {
1861			(new CView('general.warning', $data))->render();
1862		}
1863		exit;
1864	}
1865}
1866
1867function detect_page_type($default = PAGE_TYPE_HTML) {
1868	if (isset($_REQUEST['output'])) {
1869		switch (strtolower($_REQUEST['output'])) {
1870			case 'text':
1871				return PAGE_TYPE_TEXT;
1872			case 'ajax':
1873				return PAGE_TYPE_JS;
1874			case 'json':
1875				return PAGE_TYPE_JSON;
1876			case 'json-rpc':
1877				return PAGE_TYPE_JSON_RPC;
1878			case 'html':
1879				return PAGE_TYPE_HTML_BLOCK;
1880			case 'img':
1881				return PAGE_TYPE_IMAGE;
1882			case 'css':
1883				return PAGE_TYPE_CSS;
1884		}
1885	}
1886
1887	return $default;
1888}
1889
1890function makeMessageBox($good, array $messages, $title = null, $show_close_box = true, $show_details = false)
1891{
1892	$class = $good ? ZBX_STYLE_MSG_GOOD : ZBX_STYLE_MSG_BAD;
1893	$msg_details = null;
1894	$link_details = null;
1895
1896	if ($messages) {
1897		if ($title !== null) {
1898			$link_details = (new CLinkAction())
1899				->addItem(_('Details'))
1900				->addItem(' ') // space
1901				->addItem((new CSpan())
1902					->setId('details-arrow')
1903					->addClass($show_details ? ZBX_STYLE_ARROW_UP : ZBX_STYLE_ARROW_DOWN)
1904				)
1905				->setAttribute('aria-expanded', $show_details ? 'true' : 'false')
1906				->onClick('javascript: '.
1907					'showHide(jQuery(this).siblings(\'.'.ZBX_STYLE_MSG_DETAILS.'\')'.
1908						'.find(\'.'.ZBX_STYLE_MSG_DETAILS_BORDER.'\'));'.
1909					'jQuery("#details-arrow", $(this)).toggleClass("'.ZBX_STYLE_ARROW_UP.' '.ZBX_STYLE_ARROW_DOWN.'");'.
1910					'jQuery(this).attr(\'aria-expanded\', jQuery(this).find(\'.'.ZBX_STYLE_ARROW_DOWN.'\').length == 0)'
1911				);
1912		}
1913
1914		$list = new CList();
1915		if ($title !== null) {
1916			$list->addClass(ZBX_STYLE_MSG_DETAILS_BORDER);
1917
1918			if (!$show_details) {
1919				$list->setAttribute('style', 'display: none;');
1920			}
1921		}
1922		foreach ($messages as $message) {
1923			foreach (explode("\n", $message['message']) as $message_part) {
1924				$list->addItem($message_part);
1925			}
1926		}
1927		$msg_details = (new CDiv())->addClass(ZBX_STYLE_MSG_DETAILS)->addItem($list);
1928	}
1929
1930	if ($title !== null) {
1931		$title = new CSpan($title);
1932	}
1933
1934	// Details link should be in front of title.
1935	$msg_box = (new CTag('output', true, [$link_details, $title, $msg_details]))
1936		->addClass($class)
1937		->setAttribute('role', 'contentinfo')
1938		->setAttribute('aria-label', $good ? _('Success message') : _('Error message'));
1939
1940	if ($show_close_box) {
1941		$msg_box->addItem((new CSimpleButton())
1942			->addClass(ZBX_STYLE_OVERLAY_CLOSE_BTN)
1943			->onClick('jQuery(this).closest(\'.'.$class.'\').remove();')
1944			->setTitle(_('Close')));
1945	}
1946
1947	return $msg_box;
1948}
1949
1950/**
1951 * Filters messages that can be displayed to user based on defines (see ZBX_SHOW_TECHNICAL_ERRORS) and user settings.
1952 *
1953 * @param array $messages	List of messages to filter.
1954 *
1955 * @return array
1956 */
1957function filter_messages(array $messages = []) {
1958	if (!ZBX_SHOW_TECHNICAL_ERRORS && CWebUser::getType() != USER_TYPE_SUPER_ADMIN && !CWebUser::getDebugMode()) {
1959		$filtered_messages = [];
1960		$generic_exists = false;
1961
1962		foreach ($messages as $message) {
1963			if (array_key_exists('src', $message) && ($message['src'] === 'sql' || $message['src'] === 'php')) {
1964				if (!$generic_exists) {
1965					$message['message'] = _('System error occurred. Please contact Zabbix administrator.');
1966					$filtered_messages[] = $message;
1967					$generic_exists = true;
1968				}
1969			}
1970			else {
1971				$filtered_messages[] = $message;
1972			}
1973		}
1974		$messages = $filtered_messages;
1975	}
1976
1977	return $messages;
1978}
1979
1980/**
1981 * Returns the message box when messages are present; null otherwise
1982 *
1983 * @param  boolean	$good			Parameter passed to makeMessageBox to specify message box style.
1984 * @param  string	$title			Message box title.
1985 * @global array	$ZBX_MESSAGES
1986 *
1987 * @return CDiv|null
1988 */
1989function getMessages($good = false, $title = null) {
1990	global $ZBX_MESSAGES;
1991
1992	$messages = (isset($ZBX_MESSAGES) && $ZBX_MESSAGES) ? filter_messages($ZBX_MESSAGES) : [];
1993
1994	$message_box = ($title || $messages)
1995		? makeMessageBox($good, $messages, $title)
1996		: null;
1997
1998	$ZBX_MESSAGES = [];
1999
2000	return $message_box;
2001}
2002
2003function show_messages($good = false, $okmsg = null, $errmsg = null) {
2004	global $page, $ZBX_MESSAGES;
2005
2006	if (!defined('PAGE_HEADER_LOADED')) {
2007//		return null;
2008	}
2009	if (defined('ZBX_API_REQUEST')) {
2010		return null;
2011	}
2012	if (!isset($page['type'])) {
2013		$page['type'] = PAGE_TYPE_HTML;
2014	}
2015
2016	$imageMessages = [];
2017
2018	$title = $good ? $okmsg : $errmsg;
2019	$messages = isset($ZBX_MESSAGES) ? filter_messages($ZBX_MESSAGES) : [];
2020	$ZBX_MESSAGES = [];
2021
2022	switch ($page['type']) {
2023		case PAGE_TYPE_IMAGE:
2024			if ($title !== null) {
2025				$imageMessages[] = [
2026					'text' => $title,
2027					'color' => (!$good) ? ['R' => 255, 'G' => 0, 'B' => 0] : ['R' => 34, 'G' => 51, 'B' => 68]
2028				];
2029			}
2030
2031			foreach ($messages as $message) {
2032				$imageMessages[] = [
2033					'text' => $message['message'],
2034					'color' => $message['type'] == 'error'
2035						? ['R' => 255, 'G' => 55, 'B' => 55]
2036						: ['R' => 155, 'G' => 155, 'B' => 55]
2037				];
2038			}
2039			break;
2040		case PAGE_TYPE_HTML:
2041		default:
2042			if ($title || $messages) {
2043				makeMessageBox($good, $messages, $title, true, !$good)->show();
2044			}
2045			break;
2046	}
2047
2048	// draw an image with the messages
2049	if ($imageMessages) {
2050		$imageFontSize = 8;
2051
2052		// calculate the size of the text
2053		$imageWidth = 0;
2054		$imageHeight = 0;
2055		foreach ($imageMessages as &$msg) {
2056			$size = imageTextSize($imageFontSize, 0, $msg['text']);
2057			$msg['height'] = $size['height'] - $size['baseline'];
2058
2059			// calculate the total size of the image
2060			$imageWidth = max($imageWidth, $size['width']);
2061			$imageHeight += $size['height'] + 1;
2062		}
2063		unset($msg);
2064
2065		// additional padding
2066		$imageWidth += 2;
2067		$imageHeight += 2;
2068
2069		// create the image
2070		$canvas = imagecreate($imageWidth, $imageHeight);
2071		imagefilledrectangle($canvas, 0, 0, $imageWidth, $imageHeight, imagecolorallocate($canvas, 255, 255, 255));
2072
2073		// draw each message
2074		$y = 1;
2075		foreach ($imageMessages as $msg) {
2076			$y += $msg['height'];
2077			imageText($canvas, $imageFontSize, 0, 1, $y,
2078				imagecolorallocate($canvas, $msg['color']['R'], $msg['color']['G'], $msg['color']['B']),
2079				$msg['text']
2080			);
2081		}
2082		imageOut($canvas);
2083		imagedestroy($canvas);
2084	}
2085}
2086
2087function show_message($msg) {
2088	show_messages(true, $msg, '');
2089}
2090
2091function show_error_message($msg) {
2092	show_messages(false, '', $msg);
2093}
2094
2095function info($msgs) {
2096	global $ZBX_MESSAGES;
2097
2098	if (!isset($ZBX_MESSAGES)) {
2099		$ZBX_MESSAGES = [];
2100	}
2101
2102	zbx_value2array($msgs);
2103
2104	foreach ($msgs as $msg) {
2105		$ZBX_MESSAGES[] = ['type' => 'info', 'message' => $msg];
2106	}
2107}
2108
2109/*
2110 * Add an error to global message array.
2111 *
2112 * @param string | array $msg	Error message text.
2113 * @param string		 $src	The source of error message.
2114 */
2115function error($msgs, $src = '') {
2116	global $ZBX_MESSAGES;
2117
2118	if (!isset($ZBX_MESSAGES)) {
2119		$ZBX_MESSAGES = [];
2120	}
2121
2122	$msgs = zbx_toArray($msgs);
2123
2124	foreach ($msgs as $msg) {
2125		$ZBX_MESSAGES[] = [
2126			'type' => 'error',
2127			'message' => $msg,
2128			'src' => $src
2129		];
2130	}
2131}
2132
2133/**
2134 * Add multiple errors under single header.
2135 *
2136 * @param array  $data
2137 * @param string $data['header']  common header for all error messages
2138 * @param array  $data['msgs']    array of error messages
2139 */
2140function error_group($data) {
2141	foreach (zbx_toArray($data['msgs']) as $msg) {
2142		error($data['header'] . ' ' . $msg);
2143	}
2144}
2145
2146function clear_messages($count = null) {
2147	global $ZBX_MESSAGES;
2148
2149	if ($count != null) {
2150		$result = [];
2151
2152		while ($count-- > 0) {
2153			array_unshift($result, array_pop($ZBX_MESSAGES));
2154		}
2155	}
2156	else {
2157		$result = $ZBX_MESSAGES;
2158		$ZBX_MESSAGES = [];
2159	}
2160
2161	return $result ? filter_messages($result) : $result;
2162}
2163
2164function fatal_error($msg) {
2165	require_once dirname(__FILE__).'/page_header.php';
2166	show_error_message($msg);
2167	require_once dirname(__FILE__).'/page_footer.php';
2168}
2169
2170function parse_period($str) {
2171	$out = null;
2172	$time_periods_parser = new CTimePeriodsParser();
2173
2174	if ($time_periods_parser->parse($str) != CParser::PARSE_SUCCESS) {
2175		return null;
2176	}
2177
2178	foreach ($time_periods_parser->getPeriods() as $period) {
2179		if (!preg_match('/^([1-7])-([1-7]),([0-9]{1,2}):([0-9]{1,2})-([0-9]{1,2}):([0-9]{1,2})$/', $period, $matches)) {
2180			return null;
2181		}
2182
2183		for ($i = $matches[1]; $i <= $matches[2]; $i++) {
2184			if (!isset($out[$i])) {
2185				$out[$i] = [];
2186			}
2187			array_push($out[$i], [
2188				'start_h' => $matches[3],
2189				'start_m' => $matches[4],
2190				'end_h' => $matches[5],
2191				'end_m' => $matches[6]
2192			]);
2193		}
2194	}
2195
2196	return $out;
2197}
2198
2199function get_status() {
2200	global $ZBX_SERVER, $ZBX_SERVER_PORT;
2201
2202	$status = [
2203		'is_running' => false,
2204		'has_status' => false
2205	];
2206
2207	$server = new CZabbixServer($ZBX_SERVER, $ZBX_SERVER_PORT, ZBX_SOCKET_TIMEOUT, ZBX_SOCKET_BYTES_LIMIT);
2208	$status['is_running'] = $server->isRunning(get_cookie(ZBX_SESSION_NAME));
2209
2210	if ($status['is_running'] === false) {
2211		return $status;
2212	}
2213
2214	$server = new CZabbixServer($ZBX_SERVER, $ZBX_SERVER_PORT, 15, ZBX_SOCKET_BYTES_LIMIT);
2215	$server_status = $server->getStatus(get_cookie(ZBX_SESSION_NAME));
2216	$status['has_status'] = (bool) $server_status;
2217
2218	if ($server_status === false) {
2219		error($server->getError());
2220		return $status;
2221	}
2222
2223	$status += [
2224		'triggers_count_disabled' => 0,
2225		'triggers_count_off' => 0,
2226		'triggers_count_on' => 0,
2227		'items_count_monitored' => 0,
2228		'items_count_disabled' => 0,
2229		'items_count_not_supported' => 0,
2230		'hosts_count_monitored' => 0,
2231		'hosts_count_not_monitored' => 0,
2232		'hosts_count_template' => 0,
2233		'users_count' => 0,
2234		'users_online' => 0
2235	];
2236
2237	// hosts
2238	foreach ($server_status['template stats'] as $stats) {
2239		$status['hosts_count_template'] += $stats['count'];
2240	}
2241
2242	foreach ($server_status['host stats'] as $stats) {
2243		if ($stats['attributes']['proxyid'] == 0) {
2244			switch ($stats['attributes']['status']) {
2245				case HOST_STATUS_MONITORED:
2246					$status['hosts_count_monitored'] += $stats['count'];
2247					break;
2248
2249				case HOST_STATUS_NOT_MONITORED:
2250					$status['hosts_count_not_monitored'] += $stats['count'];
2251					break;
2252			}
2253		}
2254	}
2255	$status['hosts_count'] = $status['hosts_count_monitored'] + $status['hosts_count_not_monitored']
2256			+ $status['hosts_count_template'];
2257
2258	// items
2259	foreach ($server_status['item stats'] as $stats) {
2260		if ($stats['attributes']['proxyid'] == 0) {
2261			switch ($stats['attributes']['status']) {
2262				case ITEM_STATUS_ACTIVE:
2263					if (array_key_exists('state', $stats['attributes'])) {
2264						switch ($stats['attributes']['state']) {
2265							case ITEM_STATE_NORMAL:
2266								$status['items_count_monitored'] += $stats['count'];
2267								break;
2268
2269							case ITEM_STATE_NOTSUPPORTED:
2270								$status['items_count_not_supported'] += $stats['count'];
2271								break;
2272						}
2273					}
2274					break;
2275
2276				case ITEM_STATUS_DISABLED:
2277					$status['items_count_disabled'] += $stats['count'];
2278					break;
2279			}
2280		}
2281	}
2282	$status['items_count'] = $status['items_count_monitored'] + $status['items_count_disabled']
2283			+ $status['items_count_not_supported'];
2284
2285	// triggers
2286	foreach ($server_status['trigger stats'] as $stats) {
2287		switch ($stats['attributes']['status']) {
2288			case TRIGGER_STATUS_ENABLED:
2289				if (array_key_exists('value', $stats['attributes'])) {
2290					switch ($stats['attributes']['value']) {
2291						case TRIGGER_VALUE_FALSE:
2292							$status['triggers_count_off'] += $stats['count'];
2293							break;
2294
2295						case TRIGGER_VALUE_TRUE:
2296							$status['triggers_count_on'] += $stats['count'];
2297							break;
2298					}
2299				}
2300				break;
2301
2302			case TRIGGER_STATUS_DISABLED:
2303				$status['triggers_count_disabled'] += $stats['count'];
2304				break;
2305		}
2306	}
2307	$status['triggers_count_enabled'] = $status['triggers_count_off'] + $status['triggers_count_on'];
2308	$status['triggers_count'] = $status['triggers_count_enabled'] + $status['triggers_count_disabled'];
2309
2310	// users
2311	foreach ($server_status['user stats'] as $stats) {
2312		switch ($stats['attributes']['status']) {
2313			case ZBX_SESSION_ACTIVE:
2314				$status['users_online'] += $stats['count'];
2315				break;
2316
2317			case ZBX_SESSION_PASSIVE:
2318				$status['users_count'] += $stats['count'];
2319				break;
2320		}
2321	}
2322	$status['users_count'] += $status['users_online'];
2323
2324	// performance
2325	if (array_key_exists('required performance', $server_status)) {
2326		$status['vps_total'] = 0;
2327
2328		foreach ($server_status['required performance'] as $stats) {
2329			if ($stats['attributes']['proxyid'] == 0) {
2330				$status['vps_total'] += $stats['count'];
2331			}
2332		}
2333	}
2334
2335	return $status;
2336}
2337
2338function set_image_header() {
2339	global $IMAGE_FORMAT_DEFAULT;
2340
2341	switch ($IMAGE_FORMAT_DEFAULT) {
2342		case IMAGE_FORMAT_JPEG:
2343			header('Content-type: image/jpeg');
2344			break;
2345
2346		case IMAGE_FORMAT_TEXT:
2347			header('Content-type: text/html');
2348			break;
2349
2350		default:
2351			header('Content-type: image/png');
2352	}
2353
2354	header('Expires: Mon, 17 Aug 1998 12:51:50 GMT');
2355}
2356
2357function imageOut(&$image, $format = null) {
2358	global $page, $IMAGE_FORMAT_DEFAULT;
2359
2360	if (is_null($format)) {
2361		$format = $IMAGE_FORMAT_DEFAULT;
2362	}
2363
2364	ob_start();
2365
2366	if (IMAGE_FORMAT_JPEG == $format) {
2367		imagejpeg($image);
2368	}
2369	else {
2370		imagepng($image);
2371	}
2372
2373	$imageSource = ob_get_contents();
2374	ob_end_clean();
2375
2376	if ($page['type'] != PAGE_TYPE_IMAGE) {
2377		$imageId = md5(strlen($imageSource));
2378		CSession::setValue('image_id', [$imageId => $imageSource]);
2379	}
2380
2381	switch ($page['type']) {
2382		case PAGE_TYPE_IMAGE:
2383			echo $imageSource;
2384			break;
2385		case PAGE_TYPE_JSON:
2386			$json = new CJson();
2387			echo $json->encode(['result' => $imageId]);
2388			break;
2389		case PAGE_TYPE_TEXT:
2390		default:
2391			echo $imageId;
2392	}
2393}
2394
2395/**
2396 * Check if we have error messages to display.
2397 *
2398 * @global array $ZBX_MESSAGES
2399 *
2400 * @return bool
2401 */
2402function hasErrorMesssages() {
2403	global $ZBX_MESSAGES;
2404
2405	if (isset($ZBX_MESSAGES)) {
2406		foreach ($ZBX_MESSAGES as $message) {
2407			if ($message['type'] === 'error') {
2408				return true;
2409			}
2410		}
2411	}
2412
2413	return false;
2414}
2415
2416/**
2417 * Clears table rows selection's cookies.
2418 *
2419 * @param string $parentid  parent ID, is used as sessionStorage suffix
2420 * @param array  $keepids   checked rows ids
2421 */
2422function uncheckTableRows($parentid = null, $keepids = []) {
2423	$key = implode('_', array_filter(['cb', basename($_SERVER['SCRIPT_NAME'], '.php'), $parentid]));
2424
2425	if ($keepids) {
2426		// If $keepids will not have same key as value, it will create mess, when new checkbox will be checked.
2427		$keepids = array_combine($keepids, $keepids);
2428
2429		insert_js('sessionStorage.setItem("'.$key.'", JSON.stringify('.CJs::encodeJson($keepids).'))');
2430	}
2431	else {
2432		insert_js('sessionStorage.removeItem("'.$key.'")');
2433	}
2434}
2435
2436/**
2437 * Trim each element of the script path. For example, " a / b / c d " => "a/b/c d"
2438 *
2439 * @param string $name
2440 *
2441 * @return string
2442 */
2443function trimPath($name) {
2444	$path = splitPath($name);
2445	$path = array_map('trim', $path);
2446	$path = str_replace(['\\', '/'], ['\\\\', '\\/'], $path);
2447	return implode('/', $path);
2448}
2449
2450/**
2451 * Splitting string using slashes with escape backslash support and non-pair backslash cleanup.
2452 *
2453 * @param string $path
2454 *
2455 * @return array
2456 */
2457function splitPath($path) {
2458	$path_items = [];
2459	$path_item = '';
2460
2461	for ($i = 0; isset($path[$i]); $i++) {
2462		switch ($path[$i]) {
2463			case '/':
2464				$path_items[] = $path_item;
2465				$path_item = '';
2466				break;
2467
2468			case '\\':
2469				if (isset($path[++$i])) {
2470					$path_item .= $path[$i];
2471				}
2472				break;
2473
2474			default:
2475				$path_item .= $path[$i];
2476		}
2477	}
2478
2479	$path_items[] = $path_item;
2480
2481	return $path_items;
2482}
2483
2484/**
2485 * Allocate color for an image.
2486 *
2487 * @param resource 	$image
2488 * @param string	$color		a hexadecimal color identifier like "1F2C33"
2489 * @param int 		$alpha
2490 */
2491function get_color($image, $color, $alpha = 0) {
2492	$red = hexdec('0x'.substr($color, 0, 2));
2493	$green = hexdec('0x'.substr($color, 2, 2));
2494	$blue = hexdec('0x'.substr($color, 4, 2));
2495
2496	if (function_exists('imagecolorexactalpha') && function_exists('imagecreatetruecolor')
2497			&& @imagecreatetruecolor(1, 1)) {
2498		return imagecolorexactalpha($image, $red, $green, $blue, $alpha);
2499	}
2500
2501	return imagecolorallocate($image, $red, $green, $blue);
2502}
2503
2504/**
2505 * Get graphic theme based on user configuration.
2506 *
2507 * @return array
2508 */
2509function getUserGraphTheme() {
2510	$themes = DB::find('graph_theme', [
2511		'theme' => getUserTheme(CWebUser::$data)
2512	]);
2513
2514	if ($themes) {
2515		return $themes[0];
2516	}
2517
2518	return [
2519		'theme' => 'blue-theme',
2520		'textcolor' => '1F2C33',
2521		'highlightcolor' => 'E33734',
2522		'backgroundcolor' => 'FFFFFF',
2523		'graphcolor' => 'FFFFFF',
2524		'gridcolor' => 'CCD5D9',
2525		'maingridcolor' => 'ACBBC2',
2526		'gridbordercolor' => 'ACBBC2',
2527		'nonworktimecolor' => 'EBEBEB',
2528		'leftpercentilecolor' => '429E47',
2529		'righttpercentilecolor' => 'E33734',
2530		'colorpalette' => '1A7C11,F63100,2774A4,A54F10,FC6EA3,6C59DC,AC8C14,611F27,F230E0,5CCD18,BB2A02,5A2B57,'.
2531			'89ABF8,7EC25C,274482,2B5429,8048B4,FD5434,790E1F,87AC4D,E89DF4'
2532	];
2533}
2534
2535/**
2536 * Custom error handler for PHP errors.
2537 *
2538 * @param int     $errno Level of the error raised.
2539 * @param string  $errstr Error message.
2540 * @param string  $errfile Filename that the error was raised in.
2541 * @param int     $errline Line number the error was raised in.
2542 */
2543function zbx_err_handler($errno, $errstr, $errfile, $errline) {
2544	// Necessary to suppress errors when calling with error control operator like @function_name().
2545	if (error_reporting() === 0) {
2546		return true;
2547	}
2548
2549	// Don't show the call to this handler function.
2550	error($errstr.' ['.CProfiler::getInstance()->formatCallStack().']', 'php');
2551}
2552
2553/**
2554 * Creates an array with all possible variations of time units.
2555 * For example: '14d' => ['1209600', '1209600s', '20160m', '336h', '14d', '2w']
2556 *
2557 * @param string|array $values
2558 *
2559 * @return array
2560 */
2561function getTimeUnitFilters($values) {
2562	if (is_array($values)) {
2563		$res = [];
2564
2565		foreach ($values as $value) {
2566			$res = array_merge($res, getTimeUnitFilters($value));
2567		}
2568
2569		return array_unique($res, SORT_STRING);
2570	}
2571
2572	$simple_interval_parser = new CSimpleIntervalParser();
2573
2574	if ($simple_interval_parser->parse($values) != CParser::PARSE_SUCCESS) {
2575		return [$values];
2576	}
2577
2578	$sec = timeUnitToSeconds($values);
2579
2580	$res = [$sec, $sec.'s'];
2581
2582	if (bcmod($sec, SEC_PER_MIN) == 0) {
2583		$res[] = bcdiv($sec, SEC_PER_MIN, 0).'m';
2584	}
2585
2586	if (bcmod($sec, SEC_PER_HOUR) == 0) {
2587		$res[] = bcdiv($sec, SEC_PER_HOUR, 0).'h';
2588	}
2589
2590	if (bcmod($sec, SEC_PER_DAY) == 0) {
2591		$res[] = bcdiv($sec, SEC_PER_DAY, 0).'d';
2592	}
2593
2594	if (bcmod($sec, SEC_PER_WEEK) == 0) {
2595		$res[] = bcdiv($sec, SEC_PER_WEEK, 0).'w';
2596	}
2597
2598	return $res;
2599}
2600
2601/**
2602 * Creates SQL filter to search all possible variations of time units.
2603 *
2604 * @param string       $field_name
2605 * @param string|array $values
2606 *
2607 * @return string
2608 */
2609function makeUpdateIntervalFilter($field_name, $values) {
2610	$filters = [];
2611
2612	foreach (getTimeUnitFilters($values) as $filter) {
2613		$filter = str_replace("!", "!!", $filter);
2614		$filter = str_replace("%", "!%", $filter);
2615		$filter = str_replace("_", "!_", $filter);
2616
2617		$filters[] = $field_name.' LIKE '.zbx_dbstr($filter).' ESCAPE '.zbx_dbstr('!');
2618		$filters[] = $field_name.' LIKE '.zbx_dbstr($filter.';%').' ESCAPE '.zbx_dbstr('!');
2619	}
2620
2621	$res = $filters ? implode(' OR ', $filters) : '';
2622
2623	if (count($filters) > 1) {
2624		$res = '('.$res.')';
2625	}
2626
2627	return $res;
2628}
2629
2630/**
2631 * Update profile with new time selector range.
2632 *
2633 * @param array       $options
2634 * @param string      $options['profileIdx']
2635 * @param int         $options['profileIdx2']
2636 * @param string|null $options['from']
2637 * @param string|null $options['to']
2638 */
2639function updateTimeSelectorPeriod(array $options) {
2640	if ($options['from'] !== null && $options['to'] !== null) {
2641		CProfile::update($options['profileIdx'].'.from', $options['from'], PROFILE_TYPE_STR, $options['profileIdx2']);
2642		CProfile::update($options['profileIdx'].'.to', $options['to'], PROFILE_TYPE_STR, $options['profileIdx2']);
2643	}
2644}
2645
2646/**
2647 * Get profile stored 'from' and 'to'. If profileIdx is null then default values will be returned. If one of fields
2648 * not exist in $options array 'from' and 'to' value will be read from user profile. Calculates from_ts, to_ts.
2649 *
2650 * @param array $options  Array with period fields data: profileIdx, profileIdx2, from, to.
2651 *
2652 * @return array
2653 */
2654function getTimeSelectorPeriod(array $options) {
2655	$profileIdx = array_key_exists('profileIdx', $options) ? $options['profileIdx'] : null;
2656	$profileIdx2 = array_key_exists('profileIdx2', $options) ? $options['profileIdx2'] : null;
2657
2658	if ($profileIdx === null) {
2659		$options['from'] = ZBX_PERIOD_DEFAULT_FROM;
2660		$options['to'] = ZBX_PERIOD_DEFAULT_TO;
2661	}
2662	elseif (!array_key_exists('from', $options) || !array_key_exists('to', $options)
2663			|| $options['from'] === null || $options['to'] === null) {
2664		$options['from'] = CProfile::get($profileIdx.'.from', ZBX_PERIOD_DEFAULT_FROM, $profileIdx2);
2665		$options['to'] = CProfile::get($profileIdx.'.to', ZBX_PERIOD_DEFAULT_TO, $profileIdx2);
2666	}
2667
2668	$range_time_parser = new CRangeTimeParser();
2669
2670	$range_time_parser->parse($options['from']);
2671	$options['from_ts'] = $range_time_parser->getDateTime(true)->getTimestamp();
2672	$range_time_parser->parse($options['to']);
2673	$options['to_ts'] = $range_time_parser->getDateTime(false)->getTimestamp();
2674
2675	return $options;
2676}
2677
2678/**
2679 * Convert relative date range string to translated string. Function does not check is passed date range correct.
2680 *
2681 * @param string $from     Start date of date range.
2682 * @param string $to       End date of date range.
2683 *
2684 * @return string
2685 */
2686function relativeDateToText($from, $to) {
2687	$key = $from.':'.$to;
2688	$ranges = [
2689		'now-1d/d:now-1d/d' => _('Yesterday'),
2690		'now-2d/d:now-2d/d' => _('Day before yesterday'),
2691		'now-1w/d:now-1w/d' => _('This day last week'),
2692		'now-1w/w:now-1w/w' => _('Previous week'),
2693		'now-1M/M:now-1M/M' => _('Previous month'),
2694		'now-1y/y:now-1y/y' => _('Previous year'),
2695		'now/d:now/d' => _('Today'),
2696		'now/d:now' => _('Today so far'),
2697		'now/w:now/w' => _('This week'),
2698		'now/w:now' => _('This week so far'),
2699		'now/M:now/M' => _('This month'),
2700		'now/M:now' => _('This month so far'),
2701		'now/y:now/y' => _('This year'),
2702		'now/y:now' => _('This year so far')
2703	];
2704
2705	if (array_key_exists($key, $ranges)) {
2706		return $ranges[$key];
2707	}
2708
2709	if ($to === 'now') {
2710		$relative_time_parser = new CRelativeTimeParser();
2711
2712		if ($relative_time_parser->parse($from) == CParser::PARSE_SUCCESS) {
2713			$tokens = $relative_time_parser->getTokens();
2714
2715			if (count($tokens) == 1 && $tokens[0]['type'] == CRelativeTimeParser::ZBX_TOKEN_OFFSET
2716					&& $tokens[0]['sign'] === '-') {
2717				$suffix = $tokens[0]['suffix'];
2718				$value = (int) $tokens[0]['value'];
2719
2720				switch ($suffix) {
2721					case 's':
2722						if ($value < 60 || $value % 60 != 0) {
2723							return _n('Last %1$d second', 'Last %1$d seconds', $value);
2724						}
2725						$value /= 60;
2726						// break; is not missing here.
2727
2728					case 'm':
2729						if ($value < 60 || $value % 60 != 0) {
2730							return _n('Last %1$d minute', 'Last %1$d minutes', $value);
2731						}
2732						$value /= 60;
2733						// break; is not missing here.
2734
2735					case 'h':
2736						if ($value < 24 || $value % 24 != 0) {
2737							return _n('Last %1$d hour', 'Last %1$d hours', $value);
2738						}
2739						$value /= 24;
2740						// break; is not missing here.
2741
2742					case 'd':
2743						return _n('Last %1$d day', 'Last %1$d days', $value);
2744
2745					case 'M':
2746						return _n('Last %1$d month', 'Last %1$d months', $value);
2747
2748					case 'y':
2749						return _n('Last %1$d year', 'Last %1$d years', $value);
2750				}
2751			}
2752		}
2753	}
2754
2755	return $from.' – '.$to;
2756}
2757