You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2739 lines
69 KiB

1 year ago
<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
/**
* Verify that function exists and can be called as a function.
*
* @param array $names
*
* @return bool
*/
function zbx_is_callable(array $names) {
foreach ($names as $name) {
if (!is_callable($name)) {
return false;
}
}
return true;
}
/************ REQUEST ************/
function redirect($url) {
$curl = (new CUrl($url))->removeArgument(CCsrfTokenHelper::CSRF_TOKEN_NAME);
header('Location: '.$curl->getUrl());
exit;
}
/**
* Check the HTTP request method.
*
* @param string $method HTTP request method
*
* @return bool true, if the request method matches
*/
function isRequestMethod($method) {
return (strtolower($method) === strtolower($_SERVER['REQUEST_METHOD']));
}
/**
* Check if request exist.
*
* @param string $name
*
* @return bool
*/
function hasRequest($name) {
return isset($_REQUEST[$name]);
}
/**
* Check request, if exist request - return request value, else return default value.
*
* @param string $name
* @param mixed $def
*
* @return mixed
*/
function getRequest($name, $def = null) {
return isset($_REQUEST[$name]) ? $_REQUEST[$name] : $def;
}
function countRequest($str = null) {
if (!empty($str)) {
$count = 0;
foreach ($_REQUEST as $name => $value) {
if (strpos($name, $str) !== false) {
$count++;
}
}
return $count;
}
else {
return count($_REQUEST);
}
}
/************* DATE *************/
function getMonthCaption($num) {
switch ($num) {
case 1: return _('January');
case 2: return _('February');
case 3: return _('March');
case 4: return _('April');
case 5: return _('May');
case 6: return _('June');
case 7: return _('July');
case 8: return _('August');
case 9: return _('September');
case 10: return _('October');
case 11: return _('November');
case 12: return _('December');
}
return _s('[Wrong value for month: "%1$s" ]', $num);
}
function getDayOfWeekCaption($num) {
switch ($num) {
case 1: return _('Monday');
case 2: return _('Tuesday');
case 3: return _('Wednesday');
case 4: return _('Thursday');
case 5: return _('Friday');
case 6: return _('Saturday');
case 0:
case 7: return _('Sunday');
}
return _s('[Wrong value for day: "%1$s" ]', $num);
}
// Convert seconds (0..SEC_PER_WEEK) to string representation. For example, 212400 -> 'Tuesday 11:00'
function dowHrMinToStr($value, $display24Hours = false) {
$dow = $value - $value % SEC_PER_DAY;
$hr = $value - $dow;
$hr -= $hr % SEC_PER_HOUR;
$min = $value - $dow - $hr;
$min -= $min % SEC_PER_MIN;
$dow /= SEC_PER_DAY;
$hr /= SEC_PER_HOUR;
$min /= SEC_PER_MIN;
if ($display24Hours && $hr == 0 && $min == 0) {
$dow--;
$hr = 24;
}
return sprintf('%s %02d:%02d', getDayOfWeekCaption($dow), $hr, $min);
}
// Convert Day Of Week, Hours and Minutes to seconds representation. For example, 2 11:00 -> 212400. false if error occurred
function dowHrMinToSec($dow, $hr, $min) {
if (zbx_empty($dow) || zbx_empty($hr) || zbx_empty($min) || !zbx_ctype_digit($dow) || !zbx_ctype_digit($hr) || !zbx_ctype_digit($min)) {
return false;
}
if ($dow == 7) {
$dow = 0;
}
if ($dow < 0 || $dow > 6) {
return false;
}
if ($hr < 0 || $hr > 24) {
return false;
}
if ($min < 0 || $min > 59) {
return false;
}
return $dow * SEC_PER_DAY + $hr * SEC_PER_HOUR + $min * SEC_PER_MIN;
}
/**
* Convert time to a string representation. Return 'Never' if timestamp is 0.
*
* @param $format
* @param null $time
* @param string|null $timezone
*
* @throws Exception
*
* @return string
*/
function zbx_date2str($format, $time = null, string $timezone = null) {
static $weekdaynames, $weekdaynameslong, $months, $monthslong;
if ($time === null) {
$time = time();
}
if ($time == 0) {
return _('Never');
}
if ($time > ZBX_MAX_DATE) {
$prefix = '> ';
$datetime = new DateTime('@'.ZBX_MAX_DATE);
}
else {
$prefix = '';
$datetime = new DateTime('@'.(int) $time);
}
$datetime->setTimezone(new DateTimeZone($timezone ?? date_default_timezone_get()));
if ($weekdaynames === null) {
$weekdaynames = [
0 => _('Sun'),
1 => _('Mon'),
2 => _('Tue'),
3 => _('Wed'),
4 => _('Thu'),
5 => _('Fri'),
6 => _('Sat')
];
}
if ($weekdaynameslong === null) {
$weekdaynameslong = [
0 => _('Sunday'),
1 => _('Monday'),
2 => _('Tuesday'),
3 => _('Wednesday'),
4 => _('Thursday'),
5 => _('Friday'),
6 => _('Saturday')
];
}
if ($months === null) {
$months = [
1 => _('Jan'),
2 => _('Feb'),
3 => _('Mar'),
4 => _('Apr'),
5 => _x('May', 'May short'),
6 => _('Jun'),
7 => _('Jul'),
8 => _('Aug'),
9 => _('Sep'),
10 => _('Oct'),
11 => _('Nov'),
12 => _('Dec')
];
}
if ($monthslong === null) {
$monthslong = [
1 => _('January'),
2 => _('February'),
3 => _('March'),
4 => _('April'),
5 => _('May'),
6 => _('June'),
7 => _('July'),
8 => _('August'),
9 => _('September'),
10 => _('October'),
11 => _('November'),
12 => _('December')
];
}
$replacements = [
'l' => $weekdaynameslong[$datetime->format('w')],
'F' => $monthslong[$datetime->format('n')],
'D' => $weekdaynames[$datetime->format('w')],
'M' => $months[$datetime->format('n')]
];
$output = '';
$length = strlen($format);
for ($i = 0; $i < $length; $i++) {
$char = $format[$i];
$char_escaped = $i > 0 && $format[$i - 1] === '\\';
if (!$char_escaped && array_key_exists($char, $replacements)) {
$output .= $replacements[$char];
}
else {
$output .= $datetime->format($char);
}
}
return $prefix.$output;
}
/**
* Calculates and converts timestamp to string representation.
*
* @param int|string $start_date Start date timestamp.
* @param int|string $end_date End date timestamp.
*
* @return string
*/
function zbx_date2age($start_date, $end_date = 0) {
$end_date = ($end_date != 0) ? $end_date : time();
return convertUnitsS($end_date - $start_date);
}
function zbxDateToTime($strdate) {
if (6 == sscanf($strdate, '%04d%02d%02d%02d%02d%02d', $year, $month, $date, $hours, $minutes, $seconds)) {
return mktime($hours, $minutes, $seconds, $month, $date, $year);
}
elseif (5 == sscanf($strdate, '%04d%02d%02d%02d%02d', $year, $month, $date, $hours, $minutes)) {
return mktime($hours, $minutes, 0, $month, $date, $year);
}
else {
return ($strdate && is_numeric($strdate)) ? $strdate : time();
}
}
/*************** CONVERTING ******************/
/**
* Convert the Windows new line (CR+LF) to Linux style line feed (LF).
*
* @param string $string Input string that will be converted.
*
* @return string
*/
function CRLFtoLF($string) {
return str_replace("\r\n", "\n", $string);
}
function rgb2hex($color) {
$HEX = [
dechex($color[0]),
dechex($color[1]),
dechex($color[2])
];
foreach ($HEX as $id => $value) {
if (strlen($value) != 2) {
$HEX[$id] = '0'.$value;
}
}
return $HEX[0].$HEX[1].$HEX[2];
}
function hex2rgb($color) {
if ($color[0] == '#') {
$color = substr($color, 1);
}
if (strlen($color) == 6) {
list($r, $g, $b) = [$color[0].$color[1], $color[2].$color[3], $color[4].$color[5]];
}
elseif (strlen($color) == 3) {
list($r, $g, $b) = [$color[0].$color[0], $color[1].$color[1], $color[2].$color[2]];
}
else {
return false;
}
return [hexdec($r), hexdec($g), hexdec($b)];
}
function getColorVariations($color, $variations_requested = 1) {
if ($variations_requested <= 1) {
return [$color];
}
$change = hex2rgb('#ffffff'); // Color which is increased/decreased in variations.
$max = 50;
$color = hex2rgb($color);
$variations = [];
$range = range(-1 * $max, $max, $max * 2 / $variations_requested);
// Remove redundant values.
while (count($range) > $variations_requested) {
(count($range) % 2) ? array_shift($range) : array_pop($range);
}
// Calculate colors.
foreach ($range as $var) {
$r = $color[0] + ($change[0] / 100 * $var);
$g = $color[1] + ($change[1] / 100 * $var);
$b = $color[2] + ($change[2] / 100 * $var);
$variations[] = '#' . rgb2hex([
$r < 0 ? 0 : ($r > 255 ? 255 : (int) $r),
$g < 0 ? 0 : ($g > 255 ? 255 : (int) $g),
$b < 0 ? 0 : ($b > 255 ? 255 : (int) $b)
]);
}
return $variations;
}
/**
* Convert suffixed string to decimal bytes ('10K' => 10240).
* Note: this function must not depend on optional PHP libraries, since it is used in Zabbix setup.
*
* @param string $value
*
* @return int
*/
function str2mem($value) {
$value = trim($value);
$suffix = strtoupper(substr($value, -1));
if (ctype_digit($suffix)) {
return (int) $value;
}
$value = (int) substr($value, 0, -1);
if ($suffix === 'G') {
$value *= ZBX_GIBIBYTE;
}
elseif ($suffix === 'M') {
$value *= ZBX_MEBIBYTE;
}
elseif ($suffix === 'K') {
$value *= ZBX_KIBIBYTE;
}
return $value;
}
/**
* Convert decimal bytes to suffixed string (10240 => '10K').
* Note: this function must not depend on optional PHP libraries, since it is used in Zabbix setup.
*
* @param int $bytes
*
* @return string
*/
function mem2str($bytes) {
if ($bytes > ZBX_GIBIBYTE) {
return round($bytes / ZBX_GIBIBYTE, ZBX_UNITS_ROUNDOFF_SUFFIXED).'G';
}
elseif ($bytes > ZBX_MEBIBYTE) {
return round($bytes / ZBX_MEBIBYTE, ZBX_UNITS_ROUNDOFF_SUFFIXED).'M';
}
elseif ($bytes > ZBX_KIBIBYTE) {
return round($bytes / ZBX_KIBIBYTE, ZBX_UNITS_ROUNDOFF_SUFFIXED).'K';
}
else {
return round($bytes).'B';
}
}
function convertUnitsUptime($value) {
$value = round($value);
$value_abs = abs($value);
$result = $value < 0 ? '-' : '';
$days = floor($value_abs / SEC_PER_DAY);
if ($days != 0) {
$result .= _n('%1$d day', '%1$d days', formatFloat($days));
}
// Is original value precise enough for showing detailed data?
if (strlen($value_abs) <= ZBX_FLOAT_DIG) {
if ($days != 0) {
$result .= ', ';
}
$value_abs = $value_abs - $days * SEC_PER_DAY;
$hours = floor($value_abs / SEC_PER_HOUR);
$value_abs -= $hours * SEC_PER_HOUR;
$minutes = floor($value_abs / SEC_PER_MIN);
$seconds = $value_abs - $minutes * SEC_PER_MIN;
$result .= sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
}
return $result;
}
/**
* Convert time period to a human-readable format.
* The following units will be used: years, months, days, hours, minutes, seconds and milliseconds.
* Only the 3 most significant units will be displayed: #y #m #d, #m #d #h, #d #h #mm and so on, omitting empty ones.
*
* @param int $value Time period in seconds.
* @param bool $ignore_millisec Without ms (1s 200 ms = 1.2s).
*
* @return string
*/
function convertUnitsS($value, $ignore_millisec = false) {
$value = (float) $value;
$value_abs = abs($value);
$parts = [];
$start = null;
$value_abs_int = floor($value_abs);
if (($v = floor($value_abs_int / SEC_PER_YEAR)) > 0) {
$parts['years'] = $v;
$value_abs_int -= $v * SEC_PER_YEAR;
$start = 0;
}
$v = floor($value_abs_int / SEC_PER_MONTH);
if ($v == 12) {
$parts['years'] = $start === null ? 1 : $parts['years'] + 1;
$start = 0;
}
elseif ($start === null || ceil(log10($parts['years'])) <= ZBX_FLOAT_DIG) {
if ($v > 0) {
$parts['months'] = $v;
$value_abs_int -= $v * SEC_PER_MONTH;
$start = $start === null ? 1 : $start;
}
$level = 2;
foreach ([
'days' => SEC_PER_DAY,
'hours' => SEC_PER_HOUR,
'minutes' => SEC_PER_MIN
] as $part => $sec_per_part) {
$v = floor($value_abs_int / $sec_per_part);
if ($v > 0) {
$parts[$part] = $v;
$value_abs_int -= $v * $sec_per_part;
$start = $start === null ? $level : $start;
}
if ($start !== null && $level - $start >= 2) {
break;
}
$level++;
}
if ($start === null || $start >= 3) {
if ($ignore_millisec) {
$v = $value_abs_int + round(fmod($value_abs, 1), ZBX_UNITS_ROUNDOFF_SUFFIXED);
if ($v > 0) {
$parts['seconds'] = $v;
}
}
else {
$parts['seconds'] = $value_abs_int;
if ($start === null || $start >= 4) {
$v = fmod($value_abs, 1) * 1000;
if ($v > 0) {
$parts['milliseconds'] = formatFloat($v, ['decimals' => ZBX_UNITS_ROUNDOFF_SUFFIXED]);
}
}
}
}
}
$units = [
'years' => _x('y', 'year short'),
'months' => _x('M', 'month short'),
'days' => _x('d', 'day short'),
'hours' => _x('h', 'hour short'),
'minutes' => _x('m', 'minute short'),
'seconds' => _x('s', 'second short'),
'milliseconds' => _x('ms', 'millisecond short')
];
$result = [];
foreach (array_filter($parts) as $part_unit => $part_value) {
$result[] = formatFloat($part_value, ['decimals' => ZBX_UNITS_ROUNDOFF_SUFFIXED]).$units[$part_unit];
}
return $result ? ($value < 0 ? '-' : '').implode(' ', $result) : '0';
}
/**
* Convert time period to a human-readable format.
* The following units will be used: weeks, days, hours, minutes and seconds.
* Only the 3 most significant units will be displayed: #w #d #h, #d #h #m or #h #m #s, omitting empty ones.
*
* @param int $value Time period in seconds.
*
* @return string
*/
function convertSecondsToTimeUnits(int $value): string {
$parts = [];
$start = null;
if (($v = floor($value / SEC_PER_WEEK)) > 0) {
$parts['weeks'] = $v;
$value -= $v * SEC_PER_WEEK;
$start = 0;
}
$level = 1;
foreach ([
'days' => SEC_PER_DAY,
'hours' => SEC_PER_HOUR,
'minutes' => SEC_PER_MIN
] as $part => $sec_per_part) {
$v = floor($value / $sec_per_part);
if ($v > 0) {
$parts[$part] = $v;
$value -= $v * $sec_per_part;
$start = $start === null ? $level : $start;
}
if ($start !== null && $level - $start >= 2) {
break;
}
$level++;
}
if ($start === null || $start >= 2) {
$v = $value + round(fmod($value, 1), ZBX_UNITS_ROUNDOFF_SUFFIXED);
if ($v > 0) {
$parts['seconds'] = $v;
}
}
$units = [
'weeks' => _x('w', 'week short'),
'days' => _x('d', 'day short'),
'hours' => _x('h', 'hour short'),
'minutes' => _x('m', 'minute short'),
'seconds' => _x('s', 'second short')
];
$result = [];
foreach ($parts as $part_unit => $part_value) {
$result[] = $part_value.$units[$part_unit];
}
return $result ? implode(' ', $result) : '0';
}
/**
* Converts a raw value to a user-friendly representation based on unit and other parameters.
* Example: 6442450944 B => 6 GB.
*
* @see convertUnitsRaw
*
* @param array $options
*
* @return string
*/
function convertUnits(array $options): string {
[
'value' => $value,
'units' => $units
] = convertUnitsRaw($options);
$result = $value;
if ($units !== '') {
$result .= ' '.$units;
}
return $result;
}
/**
* Converts a raw value to a user-friendly representation based on unit and other parameters.
* Example: 6442450944 B => 6 GB.
*
* @param array $options
*
* $options = [
* 'value' => (string) Value to convert.
* 'units' => (string) Units to base the conversion on. Default: ''.
* 'convert' => (int) Default: ITEM_CONVERT_WITH_UNITS. Set to ITEM_CONVERT_NO_UNITS to
* force-convert a value with empty units.
* 'power' => (int) Convert to the specified power of "unit_base" (0 => '', 1 => K, 2 => M, ..).
* By default, the power will be calculated automatically.
* 'unit_base' => (string) 1000 or 1024. By default, will only use 1024 for "B" and "Bps" units.
* 'ignore_milliseconds' => (bool) Ignore milliseconds in time conversion ("s" units).
* 'precision' => (int) Max number of significant digits to take into account.
* Default: ZBX_FLOAT_DIG.
* 'decimals' => (int|null) Max number of first non-zero decimals to display. If null is specified,
* ZBX_UNITS_ROUNDOFF_SUFFIXED or ZBX_UNITS_ROUNDOFF_UNSUFFIXED will be used,
* depending on whether the units have been prefixed.
* 'decimals_exact' => (bool) Display exactly this number of decimals instead of first non-zeros.
* Default: false.
* 'small_scientific' => (bool) Allow scientific notation for small numbers. Default: true.
* 'zero_as_zero' => (bool) Return zero as '0', regardless of other options. Default: true.
* ]
*
* @return array
*/
function convertUnitsRaw(array $options): array {
static $power_table = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
$options += [
'value' => '',
'units' => '',
'convert' => ITEM_CONVERT_WITH_UNITS,
'power' => null,
'unit_base' => null,
'ignore_milliseconds' => false,
'precision' => ZBX_FLOAT_DIG,
'decimals' => null,
'decimals_exact' => false,
'small_scientific' => true,
'zero_as_zero' => true
];
$value = $options['value'] !== null ? $options['value'] : '';
if (!is_numeric($value)) {
return [
'value' => $value,
'units' => '',
'is_numeric' => false
];
}
$units = $options['units'] !== null ? $options['units'] : '';
if ($units === 'unixtime') {
return [
'value' => zbx_date2str(DATE_TIME_FORMAT_SECONDS, $value),
'units' => '',
'is_numeric' => false
];
}
if ($units === 'uptime') {
return [
'value' => convertUnitsUptime($value),
'units' => '',
'is_numeric' => false
];
}
if ($units === 's') {
return [
'value' => convertUnitsS($value, $options['ignore_milliseconds']),
'units' => '',
'is_numeric' => false
];
}
$blacklist = ['%', 'ms', 'rpm', 'RPM'];
if ($units !== '' && $units[0] === '!') {
$units = substr($units, 1);
$blacklist[] = $units;
}
$value = (float) $value;
$value_abs = abs($value);
$do_convert = $units !== '' || $options['convert'] == ITEM_CONVERT_NO_UNITS;
if (in_array($units, $blacklist) || !$do_convert || $value_abs < 1) {
return [
'value' => formatFloat($value, [
'precision' => $options['precision'],
'decimals' => $options['decimals'] !== null ? $options['decimals'] : ZBX_UNITS_ROUNDOFF_UNSUFFIXED,
'decimals_exact' => $options['decimals_exact'],
'small_scientific' => $options['small_scientific'],
'zero_as_zero' => $options['zero_as_zero']
]),
'units' => $units,
'is_numeric' => true
];
}
$unit_base = $options['unit_base'];
if ($unit_base != 1000 && $unit_base != ZBX_KIBIBYTE) {
$unit_base = isBinaryUnits($units) ? ZBX_KIBIBYTE : 1000;
}
if ($options['power'] === null) {
$result = null;
$unit_prefix = null;
foreach ($power_table as $power => $prefix) {
$result = formatFloat($value / pow($unit_base, $power), [
'precision' => $options['precision'],
'decimals' => $options['decimals'] !== null
? $options['decimals']
: ($prefix === '' ? ZBX_UNITS_ROUNDOFF_UNSUFFIXED : ZBX_UNITS_ROUNDOFF_SUFFIXED),
'decimals_exact' => $options['decimals_exact'],
'small_scientific' => $options['small_scientific'],
'zero_as_zero' => $options['zero_as_zero']
]);
$unit_prefix = $prefix;
if (abs($result) < $unit_base) {
break;
}
}
}
else {
$unit_power = array_key_exists($options['power'], $power_table) ? $options['power'] : count($power_table) - 1;
$unit_prefix = $power_table[$unit_power];
$result = formatFloat($value / pow($unit_base, $unit_power), [
'precision' => $options['precision'],
'decimals' => $options['decimals'] !== null
? $options['decimals']
: ($unit_prefix === '' ? ZBX_UNITS_ROUNDOFF_UNSUFFIXED : ZBX_UNITS_ROUNDOFF_SUFFIXED),
'decimals_exact' => $options['decimals_exact'],
'small_scientific' => $options['small_scientific'],
'zero_as_zero' => $options['zero_as_zero']
]);
}
$result_units = ($result == 0 ? '' : $unit_prefix).$units;
return [
'value' => $result,
'units' => $result_units,
'is_numeric' => true
];
}
/**
* Validate and convert time to seconds.
* Examples: '100' => '100'; '10m' => '600'; '-10m' => '-600'; '3d' => '259200'.
*
* @param string $time Decimal integer with optional time suffix.
* @param bool $with_year Additionally parse year suffixes.
*
* @return int|null Decimal integer seconds or null on error.
*/
function timeUnitToSeconds($time, $with_year = false) {
$suffixes = $with_year ? ZBX_TIME_SUFFIXES_WITH_YEAR : ZBX_TIME_SUFFIXES;
if (!preg_match('/^'.ZBX_PREG_INT.'(?<suffix>['.$suffixes.'])?$/', $time, $matches)) {
return null;
}
$suffix = array_key_exists('suffix', $matches) ? $matches['suffix'] : 's';
return $matches['int'] * ZBX_TIME_SUFFIX_MULTIPLIERS[$suffix];
}
/************* ZBX MISC *************/
/**
* Check if every character in given string value is a decimal digit.
*
* @param string | int $x Value to check.
*
* @return boolean
*/
function zbx_ctype_digit($x) {
return ctype_digit(strval($x));
}
/**
* Returns true if the value is an empty string, empty array or null.
*
* @deprecated use strict comparison instead
*
* @param $value
*
* @return bool
*/
function zbx_empty($value) {
if ($value === null) {
return true;
}
if (is_array($value) && empty($value)) {
return true;
}
if (is_string($value) && $value === '') {
return true;
}
return false;
}
function zbx_is_int($var) {
if (is_array($var)) {
return false;
}
if (is_int($var)) {
return true;
}
if (is_string($var)) {
if (function_exists('ctype_digit') && ctype_digit($var) || strcmp(intval($var), $var) == 0) {
return true;
}
}
else {
if ($var > 0 && zbx_ctype_digit($var)) {
return true;
}
}
return preg_match("/^\-?\d{1,20}+$/", $var);
}
/**
* Look for two arrays field value and create 3 array lists, one with arrays where field value exists only in first array
* second with arrays where field values are only in second array and both where field values are in both arrays.
*
* @param array $primary
* @param array $secondary
* @param string $field field that is searched in arrays
*
* @return array
*/
function zbx_array_diff(array $primary, array $secondary, $field) {
$fields1 = zbx_objectValues($primary, $field);
$fields2 = zbx_objectValues($secondary, $field);
$first = array_diff($fields1, $fields2);
$first = zbx_toHash($first);
$second = array_diff($fields2, $fields1);
$second = zbx_toHash($second);
$result = [
'first' => [],
'second' => [],
'both' => []
];
foreach ($primary as $array) {
if (!isset($array[$field])) {
$result['first'][] = $array;
}
elseif (isset($first[$array[$field]])) {
$result['first'][] = $array;
}
else {
$result['both'][$array[$field]] = $array;
}
}
foreach ($secondary as $array) {
if (!isset($array[$field])) {
$result['second'][] = $array;
}
elseif (isset($second[$array[$field]])) {
$result['second'][] = $array;
}
}
return $result;
}
function zbx_array_push(&$array, $add) {
foreach ($array as $key => $value) {
foreach ($add as $newKey => $newValue) {
$array[$key][$newKey] = $newValue;
}
}
}
/**
* Find if array has any duplicate values and return an array with info about them.
* In case of no duplicates, empty array is returned.
* Example of usage:
* $result = zbx_arrayFindDuplicates(
* array('a', 'b', 'c', 'c', 'd', 'd', 'd', 'e')
* );
* array(
* 'd' => 3,
* 'c' => 2,
* )
*
* @param array $array
*
* @return array
*/
function zbx_arrayFindDuplicates(array $array) {
$countValues = array_count_values($array); // counting occurrences of every value in array
foreach ($countValues as $value => $count) {
if ($count <= 1) {
unset($countValues[$value]);
}
}
arsort($countValues); // sorting, so that the most duplicates would be at the top
return $countValues;
}
/************* STRING *************/
function zbx_nl2br($str) {
$str_res = [];
foreach (explode("\n", $str) as $str_line) {
array_push($str_res, $str_line, BR());
}
array_pop($str_res);
return $str_res;
}
function zbx_formatDomId($value) {
return str_replace(['[', ']'], ['_', ''], $value);
}
/************* SORT *************/
function natksort(&$array) {
$keys = array_keys($array);
natcasesort($keys);
$new_array = [];
foreach ($keys as $k) {
$new_array[$k] = $array[$k];
}
$array = $new_array;
return true;
}
// recursively sort an array by key
function zbx_rksort(&$array, $flags = null) {
if (is_array($array)) {
foreach ($array as $id => $data) {
zbx_rksort($array[$id]);
}
ksort($array, $flags);
}
return $array;
}
/**
* Sorts the data using a natural sort algorithm.
*
* Not suitable for sorting macros, use order_macros() instead.
*
* @param $data
* @param null $sortfield
* @param string $sortorder
*
* @return bool
*
* @see order_macros()
*/
function order_result(&$data, $sortfield = null, $sortorder = ZBX_SORT_UP) {
if (empty($data)) {
return false;
}
if (is_null($sortfield)) {
natcasesort($data);
if ($sortorder != ZBX_SORT_UP) {
$data = array_reverse($data, true);
}
return true;
}
$sort = [];
foreach ($data as $key => $arr) {
if (!isset($arr[$sortfield])) {
return false;
}
$sort[$key] = $arr[$sortfield];
}
natcasesort($sort);
if ($sortorder != ZBX_SORT_UP) {
$sort = array_reverse($sort, true);
}
$tmp = $data;
$data = [];
foreach ($sort as $key => $val) {
$data[$key] = $tmp[$key];
}
return true;
}
/**
* Sorts the macros in the given order. Supports user and LLD macros.
*
* order_result() is not suitable for sorting macros, because it treats the "}" as a symbol with a lower priority
* then any alphanumeric character, and the result will be invalid.
*
* E.g: order_result() will sort array('{$DD}', '{$D}', '{$D1}') as
* array('{$D1}', '{$DD}', '{$D}') while the correct result is array('{$D}', '{$D1}', '{$DD}').
*
* @param array $macros
* @param string $sortfield
* @param string $order
*
* @return array
*/
function order_macros(array $macros, $sortfield, $order = ZBX_SORT_UP) {
$temp = [];
foreach ($macros as $key => $macro) {
$temp[$key] = substr($macro[$sortfield], 2, strlen($macro[$sortfield]) - 3);
}
order_result($temp, null, $order);
$rs = [];
foreach ($temp as $key => $macroLabel) {
$rs[$key] = $macros[$key];
}
return $rs;
}
// preserve keys
function zbx_array_merge() {
$args = func_get_args();
$result = [];
foreach ($args as &$array) {
if (!is_array($array)) {
return false;
}
foreach ($array as $key => $value) {
$result[$key] = $value;
}
}
unset($array);
return $result;
}
function uint_in_array($needle, $haystack) {
foreach ($haystack as $value) {
if (bccomp($needle, $value) == 0) {
return true;
}
}
return false;
}
function str_in_array($needle, $haystack, $strict = false) {
if (is_array($needle)) {
return in_array($needle, $haystack, $strict);
}
elseif ($strict) {
foreach ($haystack as $value) {
if ($needle === $value) {
return true;
}
}
}
else {
foreach ($haystack as $value) {
if (strcmp($needle, $value) == 0) {
return true;
}
}
}
return false;
}
function zbx_value2array(&$values) {
if (!is_array($values) && !is_null($values)) {
$tmp = [];
if (is_object($values)) {
$tmp[] = $values;
}
else {
$tmp[$values] = $values;
}
$values = $tmp;
}
}
// creates chain of relation parent -> child, for all chain levels
function createParentToChildRelation(&$chain, $link, $parentField, $childField) {
if (!isset($chain[$link[$parentField]])) {
$chain[$link[$parentField]] = [];
}
$chain[$link[$parentField]][$link[$childField]] = $link[$childField];
if (isset($chain[$link[$childField]])) {
$chain[$link[$parentField]] = zbx_array_merge($chain[$link[$parentField]], $chain[$link[$childField]]);
}
}
// object or array of objects to hash
function zbx_toHash($value, $field = null) {
if (is_null($value)) {
return $value;
}
$result = [];
if (!is_array($value)) {
$result = [$value => $value];
}
elseif (isset($value[$field])) {
$result[$value[$field]] = $value;
}
else {
foreach ($value as $val) {
if (!is_array($val)) {
$result[$val] = $val;
}
elseif (isset($val[$field])) {
$result[$val[$field]] = $val;
}
}
}
return $result;
}
/**
* Transforms a single or an array of values to an array of objects, where the values are stored under the $field
* key.
*
* E.g:
* zbx_toObject(array(1, 2), 'hostid') // returns array(array('hostid' => 1), array('hostid' => 2))
* zbx_toObject(3, 'hostid') // returns array(array('hostid' => 3))
* zbx_toObject(array('a' => 1), 'hostid', true) // returns array('a' => array('hostid' => 1))
*
* @param $value
* @param $field
* @param $preserve_keys
*
* @return array
*/
function zbx_toObject($value, $field, $preserve_keys = false) {
if (is_null($value)) {
return $value;
}
$result = [];
// Value or Array to Object or Array of objects
if (!is_array($value)) {
$result = [[$field => $value]];
}
elseif (!isset($value[$field])) {
foreach ($value as $key => $val) {
if (!is_array($val)) {
$result[$key] = [$field => $val];
}
}
if (!$preserve_keys) {
$result = array_values($result);
}
}
return $result;
}
/**
* Converts the given value to a numeric array:
* - a scalar value will be converted to an array and added as the only element;
* - an array with first element key containing only numeric characters will be converted to plain zero-based numeric array.
* This is used for resetting nonsequential numeric arrays;
* - an associative array will be returned in an array as the only element, except if first element key contains only numeric characters.
*
* @param mixed $value
*
* @return array
*/
function zbx_toArray($value) {
if ($value === null) {
return $value;
}
if (is_array($value)) {
// reset() is needed to move internal array pointer to the beginning of the array
reset($value);
if (zbx_ctype_digit(key($value))) {
$result = array_values($value);
}
elseif (!empty($value)) {
$result = [$value];
}
else {
$result = [];
}
}
else {
$result = [$value];
}
return $result;
}
/**
* Converts value OR object OR array of objects TO an array.
*
* @deprecated Use array_column() instead.
*
* @param $value
* @param $field
*
* @return array
*/
function zbx_objectValues($value, $field) {
if (is_null($value)) {
return $value;
}
if (!is_array($value)) {
$result = [$value];
}
elseif (isset($value[$field])) {
$result = [$value[$field]];
}
else {
$result = [];
foreach ($value as $val) {
if (!is_array($val)) {
$result[] = $val;
}
elseif (isset($val[$field])) {
$result[] = $val[$field];
}
}
}
return $result;
}
function zbx_cleanHashes(&$value) {
if (is_array($value)) {
// reset() is needed to move internal array pointer to the beginning of the array
reset($value);
if (zbx_ctype_digit(key($value))) {
$value = array_values($value);
}
}
return $value;
}
function zbx_toCSV($values) {
$csv = '';
$glue = '","';
foreach ($values as $row) {
if (!is_array($row)) {
$row = [$row];
}
foreach ($row as $num => $value) {
$row[$num] = str_replace('"', '""', $value);
}
$csv .= '"'.implode($glue, $row).'"'."\n";
}
return $csv;
}
function zbx_str2links($text) {
$result = [];
foreach (explode("\n", $text) as $line) {
$line = rtrim($line, "\r ");
preg_match_all('#https?://[^\n\t\r ]+#u', $line, $matches);
$start = 0;
foreach ($matches[0] as $match) {
if (($pos = mb_strpos($line, $match, $start)) !== false) {
if ($pos != $start) {
$result[] = mb_substr($line, $start, $pos - $start);
}
$result[] = new CLink($match, $match);
$start = $pos + mb_strlen($match);
}
}
if (mb_strlen($line) != $start) {
$result[] = mb_substr($line, $start);
}
$result[] = BR();
}
array_pop($result);
return $result;
}
function zbx_subarray_push(&$mainArray, $sIndex, $element = null, $key = null) {
if (!isset($mainArray[$sIndex])) {
$mainArray[$sIndex] = [];
}
if ($key) {
$mainArray[$sIndex][$key] = is_null($element) ? $sIndex : $element;
}
else {
$mainArray[$sIndex][] = is_null($element) ? $sIndex : $element;
}
}
/*************** PAGE SORTING ******************/
/**
* Returns header with sorting options.
*
* @param string obj Header item.
* @param string $tabfield Table field.
* @param string $sortField Sorting field.
* @param string $sortOrder Sorting order.
* @param string $link Sorting link.
*
* @return CColHeader
*/
function make_sorting_header($obj, $tabfield, $sortField, $sortOrder, $link = null) {
$sortorder = ($sortField == $tabfield && $sortOrder == ZBX_SORT_UP) ? ZBX_SORT_DOWN : ZBX_SORT_UP;
$link = CUrlFactory::getContextUrl($link);
$link->setArgument('sort', $tabfield);
$link->setArgument('sortorder', $sortorder);
zbx_value2array($obj);
$arrow = null;
if ($tabfield == $sortField) {
if ($sortorder == ZBX_SORT_UP) {
$arrow = (new CSpan())->addClass(ZBX_STYLE_ARROW_DOWN);
}
else {
$arrow = (new CSpan())->addClass(ZBX_STYLE_ARROW_UP);
}
}
return new CColHeader(new CLink([$obj, $arrow], $link->getUrl()));
}
/**
* Get decimal point and thousands separator for number formatting according to the current locale.
*
* @return array 'decimal_point' and 'thousands_sep' values.
*/
function getNumericFormatting(): array {
static $numeric_formatting = null;
if ($numeric_formatting === null) {
$numeric_formatting = array_intersect_key(localeconv(), array_flip(['decimal_point', 'thousands_sep']));
}
return $numeric_formatting;
}
/**
* Format floating-point number in the best possible way for displaying.
*
* @param float $number Valid number in decimal or scientific notation.
* @param array $options Formatting options.
*
* $options = [
* 'precision' => (int) Max number of significant digits to take into account. Default: ZBX_FLOAT_DIG.
* 'decimals' => (int) Max number of first non-zero decimals to display. Default: 0.
* 'decimals_exact' => (bool) Display exactly this number of decimals instead of first non-zeros. Default: false.
* 'small_scientific' => (bool) Allow scientific notation for small numbers. Default: true.
* 'zero_as_zero' => (bool) Return zero as '0', regardless of other options. Default: true.
* ]
*
* Note: $decimals must be less than $precision.
*
* @return string
*/
function formatFloat(float $number, array $options = []): string {
if ($number == INF) {
return _('Infinity');
}
if ($number == -INF) {
return '-'._('Infinity');
}
$defaults = [
'precision' => ZBX_FLOAT_DIG,
'decimals' => 0,
'decimals_exact' => false,
'small_scientific' => true,
'zero_as_zero' => true
];
[
'precision' => $precision,
'decimals' => $decimals,
'decimals_exact' => $decimals_exact,
'small_scientific' => $small_scientific,
'zero_as_zero' => $zero_as_zero
] = $options + $defaults;
if ($zero_as_zero && $number == 0) {
return '0';
}
$number_original = $number;
$exponent = (int) explode('E', sprintf('%.'.($precision - 1).'E', $number))[1];
if ($exponent < 0) {
for ($i = 1; $i >= 0; $i--) {
$round_precision = $decimals - $exponent - $i;
// PHP rounding bug when precision is set more than 294.
if ($round_precision > 294) {
$decimal_shift = pow(10, $round_precision - 294);
$test = round($number * $decimal_shift, 294) / $decimal_shift;
}
else {
$test = round($number, $round_precision);
}
$test_number = sprintf('%.'.($precision - 1).'E', $test);
$test_digits = $precision == 1
? 1
: strlen(rtrim(explode('E', $test_number)[0], '0')) - ($test_number[0] === '-' ? 2 : 1);
if (!$small_scientific || $test_digits - $exponent < $precision) {
break;
}
}
$number = $test_number;
$digits = $test_digits;
}
else {
if ($exponent >= $precision) {
if ($exponent >= min(PHP_FLOAT_DIG, $precision + 3)
|| round($number, $precision - $exponent - 1) != $number) {
$number = round($number, $decimals - $exponent);
}
}
else {
$number = round($number, min($decimals, $precision - $exponent - 1));
}
$number = sprintf('%.'.($precision - 1).'E', $number);
$digits = $precision == 1 ? 1 : strlen(rtrim(explode('E', $number)[0], '0')) - ($number[0] === '-' ? 2 : 1);
}
if ($zero_as_zero && $number == 0) {
return '0';
}
[
'decimal_point' => $decimal_point,
'thousands_sep' => $thousands_sep
] = getNumericFormatting();
$exponent = (int) explode('E', sprintf('%.'.($precision - 1).'E', $number))[1];
if ($exponent < 0) {
if (!$small_scientific
|| $digits - $exponent <= ($decimals_exact ? min($decimals + 1, $precision) : $precision)) {
return number_format($number, $decimals_exact ? $decimals : $digits - $exponent - 1, $decimal_point,
$thousands_sep
);
}
else {
return str_replace('.', $decimal_point,
sprintf('%.'.($decimals_exact ? $decimals : min($digits - 1, $decimals)).'E', $number)
);
}
}
elseif ($exponent >= min(PHP_FLOAT_DIG, $precision + 3)
|| ($exponent >= $precision && $number != $number_original)) {
return str_replace('.', $decimal_point,
sprintf('%.'.($decimals_exact ? $decimals : min($digits - 1, $decimals)).'E', $number)
);
}
else {
return number_format($number, $decimals_exact ? $decimals : max(0, min($digits - $exponent - 1, $decimals)),
$decimal_point, $thousands_sep
);
}
}
/**
* Truncate float to the amount of significant digits, to allow safe float comparison.
*
* @param float $number
*
* @return float
*/
function truncateFloat(float $number): float {
if (is_infinite($number)) {
return $number;
}
return (float) sprintf('%.'.(ZBX_FLOAT_DIG - 1).'E', $number);
}
/**
* Get number of digits after the decimal dot.
*
* @param float $number Valid number in decimal or scientific notation.
*
* @return int
*/
function getNumDecimals(float $number): int {
[$mantissa, $exponent] = explode('E', sprintf('%.'.(ZBX_FLOAT_DIG - 1).'E', $number));
$significant_size = strlen(rtrim($mantissa, '0')) - ($number < 0 ? 2 : 1);
return max(0, $significant_size - 1 - $exponent);
}
/**
* Converts number to letter representation.
* From A to Z, then from AA to ZZ etc.
* Example: 0 => A, 25 => Z, 26 => AA, 27 => AB, 52 => BA, ...
*
* Keep in sync with JS num2letter().
*
* @param int $number
*
* @return string
*/
function num2letter($number) {
$start = ord('A');
$base = 26;
$str = '';
$level = 0;
do {
if ($level++ > 0) {
$number--;
}
$remainder = $number % $base;
$number = ($number - $remainder) / $base;
$str = chr($start + $remainder).$str;
} while (0 != $number);
return $str;
}
/**
* Renders an "access denied" message and stops the execution of the script.
*
* The $mode parameters controls the layout of the message for logged in users:
* - ACCESS_DENY_OBJECT - render the message when denying access to a specific object
* - ACCESS_DENY_PAGE - render a complete access denied page
*
* If visitor is without any access permission then layout of the message is same as in ACCESS_DENY_PAGE mode.
*
* @param int $mode
*/
function access_deny($mode = ACCESS_DENY_OBJECT) {
// deny access to an object
if ($mode == ACCESS_DENY_OBJECT && CWebUser::isLoggedIn()) {
show_error_message(_('No permissions to referred object or it does not exist!'));
require_once dirname(__FILE__).'/page_header.php';
(new CHtmlPage())->show();
require_once dirname(__FILE__).'/page_footer.php';
}
// deny access to a page
else {
// url to redirect the user to after he logs in
$url = (new CUrl(!empty($_REQUEST['request']) ? $_REQUEST['request'] : ''))
->removeArgument(CCsrfTokenHelper::CSRF_TOKEN_NAME);
if (CAuthenticationHelper::get(CAuthenticationHelper::HTTP_LOGIN_FORM) == ZBX_AUTH_FORM_HTTP
&& CAuthenticationHelper::get(CAuthenticationHelper::HTTP_AUTH_ENABLED) == ZBX_AUTH_HTTP_ENABLED
&& (!CWebUser::isLoggedIn() || CWebUser::isGuest())) {
$redirect_to = (new CUrl('index_http.php'))->setArgument('request', $url->toString());
redirect($redirect_to->toString());
}
$url = urlencode($url->toString());
// if the user is logged in - render the access denied message
if (CWebUser::isLoggedIn()) {
$data = [
'header' => _('Access denied'),
'messages' => [
_s('You are logged in as "%1$s".',
CWebUser::$data['username']).' '._('You have no permissions to access this page.'
),
_('If you think this message is wrong, please consult your administrators about getting the necessary permissions.')
],
'buttons' => []
];
// display the login button only for guest users
if (CWebUser::isGuest()) {
$data['buttons'][] = (new CButton('login', _('Login')))
->setAttribute('data-url', $url)
->onClick('document.location = "index.php?request=" + this.dataset.url;');
}
$data['buttons'][] = (new CButton('back', _s('Go to "%1$s"', CMenuHelper::getFirstLabel())))
->setAttribute('data-url', CMenuHelper::getFirstUrl())
->onClick('document.location = this.dataset.url');
}
// if the user is not logged in - offer to login
else {
$data = [
'header' => _('You are not logged in'),
'messages' => [
_('You must login to view this page.'),
_('If you think this message is wrong, please consult your administrators about getting the necessary permissions.')
],
'buttons' => [
(new CButton('login', _('Login')))
->setAttribute('data-url', $url)
->onClick('document.location = "index.php?request=" + this.dataset.url;')
]
];
}
$data['theme'] = getUserTheme(CWebUser::$data);
if (detect_page_type() == PAGE_TYPE_JS) {
echo (new CView('layout.json', ['main_block' => json_encode(['error' => $data['header']])]))->getOutput();
}
else {
echo (new CView('general.warning', $data))->getOutput();
}
session_write_close();
exit();
}
}
function detect_page_type($default = PAGE_TYPE_HTML) {
if (isset($_REQUEST['output'])) {
switch (strtolower($_REQUEST['output'])) {
case 'text':
return PAGE_TYPE_TEXT;
case 'ajax':
return PAGE_TYPE_JS;
case 'json':
return PAGE_TYPE_JSON;
case 'json-rpc':
return PAGE_TYPE_JSON_RPC;
case 'html':
return PAGE_TYPE_HTML_BLOCK;
case 'img':
return PAGE_TYPE_IMAGE;
case 'css':
return PAGE_TYPE_CSS;
}
}
return $default;
}
/**
* Create a message box.
*
* @param string $class CSS class of the message box. Possible values:
* ZBX_STYLE_MSG_GOOD, ZBX_STYLE_MSG_BAD, ZBX_STYLE_MSG_WARNING.
* @param array $messages An array of messages.
* @param string $messages[]['message'] Message text.
* @param string|null $title (optional) Message box title.
* @param bool $show_close_box (optional) Show or hide close button in error message box.
* @param bool $show_details (optional) Show or hide message details.
*
* @return CTag
*/
function makeMessageBox(string $class, array $messages, string $title = null, bool $show_close_box = true,
bool $show_details = false): CTag {
$msg_details = null;
$link_details = null;
if ($messages) {
if ($title !== null) {
$link_details = (new CLinkAction())
->addItem(_('Details'))
->addItem(' ') // space
->addItem((new CSpan())
->setId('details-arrow')
->addClass($show_details ? ZBX_STYLE_ARROW_UP : ZBX_STYLE_ARROW_DOWN)
)
->setAttribute('aria-expanded', $show_details ? 'true' : 'false')
->onClick('
showHide(jQuery(this).siblings(\'.'.ZBX_STYLE_MSG_DETAILS.'\'));
jQuery("#details-arrow", $(this)).toggleClass("'.ZBX_STYLE_ARROW_UP.' '.ZBX_STYLE_ARROW_DOWN.'");
jQuery(this).attr(\'aria-expanded\', jQuery(this).find(\'.'.ZBX_STYLE_ARROW_DOWN.'\').length == 0);
');
}
$list = (new CList())->addClass(ZBX_STYLE_LIST_DASHED);
foreach ($messages as $message) {
$list->addItem($message['message']);
}
$msg_details = (new CDiv())
->addClass(ZBX_STYLE_MSG_DETAILS)
->addItem($list);
if ($title !== null && !$show_details) {
$msg_details->addStyle('display: none;');
}
}
$aria_labels = [
ZBX_STYLE_MSG_GOOD => _('Success message'),
ZBX_STYLE_MSG_BAD => _('Error message'),
ZBX_STYLE_MSG_WARNING => _('Warning message')
];
// Details link should be in front of title.
$msg_box = (new CTag('output', true, [$link_details, $title !== null ? new CSpan($title) : null, $msg_details]))
->addClass($class)
->setAttribute('role', 'contentinfo')
->setAttribute('aria-label', $aria_labels[$class]);
if ($show_close_box) {
$msg_box->addItem(
(new CSimpleButton())
->addClass(ZBX_STYLE_BTN_OVERLAY_CLOSE)
->onClick('jQuery(this).closest(\'.'.$class.'\').remove();')
->setTitle(_('Close'))
);
}
return $msg_box;
}
/**
* Filters messages that can be displayed to user based on CSettingsHelper::SHOW_TECHNICAL_ERRORS and user settings.
*
* @return array
*/
function filter_messages(): array {
if (!CSettingsHelper::getGlobal(CSettingsHelper::SHOW_TECHNICAL_ERRORS)
&& CWebUser::getType() != USER_TYPE_SUPER_ADMIN && !CWebUser::getDebugMode()) {
$type = CMessageHelper::getType();
$title = CMessageHelper::getTitle();
$messages = CMessageHelper::getMessages();
CMessageHelper::clear();
if ($title !== null) {
if ($type === CMessageHelper::MESSAGE_TYPE_SUCCESS) {
CMessageHelper::setSuccessTitle($title);
}
else {
CMessageHelper::setErrorTitle($title);
}
}
$generic_exists = false;
foreach ($messages as $message) {
if ($message['type'] === CMessageHelper::MESSAGE_TYPE_ERROR && $message['is_technical_error']) {
if (!$generic_exists) {
CMessageHelper::addError(_('System error occurred. Please contact Zabbix administrator.'));
$generic_exists = true;
}
}
else {
CMessageHelper::addMessage($message);
}
}
}
return CMessageHelper::getMessages();
}
/**
* Returns a message box if there are messages. Otherwise, null.
*
* @param bool $good Parameter passed to makeMessageBox to specify message box style.
* @param string $title Message box title.
* @param bool $show_close_box Show or hide close button in error message box.
*
* @return CTag|null
*/
function getMessages(bool $good = false, string $title = null, bool $show_close_box = true): ?CTag {
$messages = get_and_clear_messages();
$message_box = ($title || $messages)
? makeMessageBox($good ? ZBX_STYLE_MSG_GOOD : ZBX_STYLE_MSG_BAD, $messages, $title, $show_close_box, !$good)
: null;
return $message_box;
}
function show_messages($good = null, $okmsg = null, $errmsg = null) {
global $page, $ZBX_MESSAGES_PREPARED;
if (defined('ZBX_API_REQUEST')) {
return null;
}
$messages = get_and_clear_messages();
if ($good === null) {
$has_errors = false;
$has_warnings = false;
foreach ($messages as $message) {
$has_errors = ($has_errors || ($message['type'] === 'error'));
$has_warnings = ($has_warnings || ($message['type'] === 'warning'));
}
if ($has_errors) {
$class = ZBX_STYLE_MSG_BAD;
$good = false;
}
elseif ($has_warnings) {
$class = ZBX_STYLE_MSG_WARNING;
$good = true;
}
else {
$class = ZBX_STYLE_MSG_GOOD;
$good = true;
}
}
else {
$class = $good ? ZBX_STYLE_MSG_GOOD : ZBX_STYLE_MSG_BAD;
}
$title = $good ? $okmsg : $errmsg;
if ($title === null && !$messages) {
return;
}
$page_type = (is_array($page) && array_key_exists('type', $page)) ? $page['type'] : PAGE_TYPE_HTML;
switch ($page_type) {
case PAGE_TYPE_IMAGE:
$image_messages = [];
if ($title !== null) {
$image_messages[] = [
'text' => $title,
'color' => (!$good) ? ['R' => 255, 'G' => 0, 'B' => 0] : ['R' => 34, 'G' => 51, 'B' => 68]
];
}
foreach ($messages as $message) {
$image_messages[] = [
'text' => $message['message'],
'color' => ($message['type'] === 'error')
? ['R' => 255, 'G' => 55, 'B' => 55]
: ['R' => 155, 'G' => 155, 'B' => 55]
];
}
// Draw an image with the messages.
$image_font_size = 8;
// Calculate the size of the text.
$width = 0;
$height = 0;
foreach ($image_messages as &$message) {
$size = imageTextSize($image_font_size, 0, $message['text']);
$message['height'] = $size['height'] - $size['baseline'];
// Calculate the total size of the image.
$width = max($width, $size['width']);
$height += $size['height'] + 1;
}
unset($message);
// Add padding.
$width += 2;
$height += 2;
// Create the image.
$canvas = imagecreate($width, $height);
imagefilledrectangle($canvas, 0, 0, $width, $height, imagecolorallocate($canvas, 255, 255, 255));
// Draw messages.
$y = 1;
foreach ($image_messages as $message) {
$y += $message['height'];
imageText($canvas, $image_font_size, 0, 1, $y,
imagecolorallocate($canvas, $message['color']['R'], $message['color']['G'], $message['color']['B']),
$message['text']
);
}
imageOut($canvas);
imagedestroy($canvas);
break;
default:
if (!is_array($ZBX_MESSAGES_PREPARED)) {
$ZBX_MESSAGES_PREPARED = [];
}
// Prepare messages for inclusion within the layout engine.
$ZBX_MESSAGES_PREPARED[] = [
'class' => $class,
'messages' => $messages,
'title' => $title,
'show_close_box' => true,
'show_details' => ($class === ZBX_STYLE_MSG_BAD)
];
break;
}
}
/**
* Get prepared HTML messages generated by the current request and, optionally, passed by the previous request.
*
* @param array $options['with_auth_warning'] Include unsuccessful authentication warning message.
* @param array $options['with_session_messages'] Include messages passed by the previous request.
* @param array $options['with_current_messages'] Include messages generated by the current request.
*
* @return string|null One or several HTML message boxes.
*/
function get_prepared_messages(array $options = []): ?string {
global $ZBX_MESSAGES_PREPARED;
if (!is_array($ZBX_MESSAGES_PREPARED)) {
$ZBX_MESSAGES_PREPARED = [];
}
$options += [
'with_auth_warning' => false,
'with_session_messages' => false,
'with_current_messages' => false
];
// Process messages of the current request.
if ($options['with_current_messages']) {
show_messages(
null,
CMessageHelper::getTitle(),
CMessageHelper::getTitle()
);
$messages_current = $ZBX_MESSAGES_PREPARED;
$restore_messages = [];
$restore_messages_prepared = [];
}
else {
$messages_current = [];
$restore_messages = CMessageHelper::getMessages();
$restore_messages_prepared = $ZBX_MESSAGES_PREPARED;
CMessageHelper::clear();
}
$ZBX_MESSAGES_PREPARED = [];
// Process authentication warning if user had unsuccessful authentication attempts.
if ($options['with_auth_warning'] && ($failed_attempts = CProfile::get('web.login.attempt.failed', 0))) {
$attempt_ip = CProfile::get('web.login.attempt.ip', '');
$attempt_date = CProfile::get('web.login.attempt.clock', 0);
error(_n('%4$s failed login attempt logged. Last failed attempt was from %1$s on %2$s at %3$s.',
'%4$s failed login attempts logged. Last failed attempt was from %1$s on %2$s at %3$s.',
$attempt_ip,
zbx_date2str(DATE_FORMAT, $attempt_date),
zbx_date2str(TIME_FORMAT, $attempt_date),
$failed_attempts
));
show_messages(
false, // Failed login can be only error message.
CMessageHelper::getTitle(),
CMessageHelper::getTitle()
);
CProfile::update('web.login.attempt.failed', 0, PROFILE_TYPE_INT);
}
$messages_authentication = $ZBX_MESSAGES_PREPARED;
$ZBX_MESSAGES_PREPARED = [];
// Process messages passed by the previous request.
if ($options['with_session_messages']) {
CMessageHelper::restoreScheduleMessages($messages_current);
if (CMessageHelper::getTitle() !== null) {
show_messages(
CMessageHelper::getType() === CMessageHelper::MESSAGE_TYPE_SUCCESS,
CMessageHelper::getTitle(),
CMessageHelper::getTitle()
);
}
}
$messages_session = $ZBX_MESSAGES_PREPARED;
// Create message boxes for all requested messages types in the correct order.
$html = '';
foreach (array_merge($messages_authentication, $messages_session, $messages_current) as $box) {
$html .= makeMessageBox($box['class'], $box['messages'], $box['title'], $box['show_close_box'],
$box['show_details']
)->toString();
}
foreach ($restore_messages as $message) {
CMessageHelper::addMessage($message);
}
$ZBX_MESSAGES_PREPARED = $restore_messages_prepared;
return ($html === '') ? null : $html;
}
function show_message(string $msg): void {
show_messages(true, $msg, '');
}
function show_error_message(string $msg): void {
show_messages(false, '', $msg);
}
function info($msgs): void {
zbx_value2array($msgs);
foreach ($msgs as $msg) {
CMessageHelper::addSuccess($msg);
}
}
/**
* Add warning messages to the global message array.
*
* @param array|string $messages
*/
function warning($messages): void {
zbx_value2array($messages);
foreach ($messages as $message) {
CMessageHelper::addWarning($message);
}
}
/**
* Add an error to global message array.
*
* @param string|array $msgs Error message text.
* @param bool $is_technical_error
*/
function error($msgs, bool $is_technical_error = false): void {
$msgs = zbx_toArray($msgs);
foreach ($msgs as $msg) {
CMessageHelper::addError($msg, $is_technical_error);
}
}
function get_and_clear_messages(): array {
$messages = filter_messages();
CMessageHelper::clear();
return $messages;
}
function fatal_error($msg) {
require_once dirname(__FILE__).'/page_header.php';
show_error_message($msg);
require_once dirname(__FILE__).'/page_footer.php';
}
function parse_period($str) {
$out = null;
$time_periods_parser = new CTimePeriodsParser();
if ($time_periods_parser->parse($str) != CParser::PARSE_SUCCESS) {
return null;
}
foreach ($time_periods_parser->getPeriods() as $period) {
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)) {
return null;
}
for ($i = $matches[1]; $i <= $matches[2]; $i++) {
if (!isset($out[$i])) {
$out[$i] = [];
}
array_push($out[$i], [
'start_h' => $matches[3],
'start_m' => $matches[4],
'end_h' => $matches[5],
'end_m' => $matches[6]
]);
}
}
return $out;
}
/**
* Set image header.
*
* @param integer $format One of IMAGE_FORMAT_* constants. If not set global $IMAGE_FORMAT_DEFAULT will be used.
*/
function set_image_header($format = null) {
global $IMAGE_FORMAT_DEFAULT;
switch ($format !== null ? $format : $IMAGE_FORMAT_DEFAULT) {
case IMAGE_FORMAT_JPEG:
header('Content-type: image/jpeg');
break;
case IMAGE_FORMAT_GIF:
header('Content-type: image/gif');
break;
case IMAGE_FORMAT_TEXT:
header('Content-type: text/html');
break;
default:
header('Content-type: image/png');
}
header('Expires: Mon, 17 Aug 1998 12:51:50 GMT');
}
function imageOut(&$image, $format = null) {
global $page, $IMAGE_FORMAT_DEFAULT;
if (is_null($format)) {
$format = $IMAGE_FORMAT_DEFAULT;
}
ob_start();
if (IMAGE_FORMAT_JPEG == $format) {
imagejpeg($image);
}
else {
imagepng($image);
}
$imageSource = ob_get_contents();
ob_end_clean();
if ($page['type'] != PAGE_TYPE_IMAGE) {
$imageId = md5(strlen($imageSource));
CSessionHelper::set('image_id', [$imageId => $imageSource]);
}
switch ($page['type']) {
case PAGE_TYPE_IMAGE:
echo $imageSource;
break;
case PAGE_TYPE_JSON:
echo json_encode(['result' => $imageId]);
break;
case PAGE_TYPE_TEXT:
default:
echo $imageId;
}
}
/**
* Check if we have error messages to display.
*
* @return bool
*/
function hasErrorMessages() {
return CMessageHelper::getType() === CMessageHelper::MESSAGE_TYPE_ERROR;
}
/**
* Clears table rows selection's cookies.
*
* @param string $name entity name, used as sessionStorage suffix
* @param array $keepids checked rows ids
*/
function uncheckTableRows($name = null, $keepids = []) {
$key = 'cb_'.basename($_SERVER['SCRIPT_NAME'], '.php').($name !== null ? '_'.$name : '');
if ($keepids) {
$keepids = array_fill_keys($keepids, '');
insert_js('sessionStorage.setItem('.json_encode($key).', JSON.stringify('.json_encode($keepids).'));');
}
else {
insert_js('sessionStorage.removeItem('.json_encode($key).');');
}
}
/**
* Trim each element of the script path. For example, " a / b / c d " => "a/b/c d"
*
* @param string $name
*
* @return string
*/
function trimPath($name) {
$path = splitPath($name);
$path = array_map('trim', $path);
$path = str_replace(['\\', '/'], ['\\\\', '\\/'], $path);
return implode('/', $path);
}
/**
* Splitting string using slashes with escape backslash support and non-pair backslash cleanup.
*
* @param string $path
*
* @return array
*/
function splitPath($path) {
$path_items = [];
$path_item = '';
for ($i = 0; isset($path[$i]); $i++) {
switch ($path[$i]) {
case '/':
$path_items[] = $path_item;
$path_item = '';
break;
case '\\':
if (isset($path[++$i])) {
$path_item .= $path[$i];
}
break;
default:
$path_item .= $path[$i];
}
}
$path_items[] = $path_item;
return $path_items;
}
/**
* Allocate color for an image.
*
* @param resource $image
* @param string $color a hexadecimal color identifier like "1F2C33"
* @param int $alpha
*
* @return int
*/
function get_color($image, $color, $alpha = 0) {
$red = hexdec('0x'.substr($color, 0, 2));
$green = hexdec('0x'.substr($color, 2, 2));
$blue = hexdec('0x'.substr($color, 4, 2));
return imagecolorexactalpha($image, $red, $green, $blue, $alpha);
}
/**
* Get graphic theme based on user configuration.
*
* @return array
*/
function getUserGraphTheme() {
$themes = DB::find('graph_theme', [
'theme' => getUserTheme(CWebUser::$data)
]);
if ($themes) {
return $themes[0];
}
return [
'theme' => 'blue-theme',
'textcolor' => '1F2C33',
'highlightcolor' => 'E33734',
'backgroundcolor' => 'FFFFFF',
'graphcolor' => 'FFFFFF',
'gridcolor' => 'CCD5D9',
'maingridcolor' => 'ACBBC2',
'gridbordercolor' => 'ACBBC2',
'nonworktimecolor' => 'EBEBEB',
'leftpercentilecolor' => '429E47',
'righttpercentilecolor' => 'E33734',
'colorpalette' => '1A7C11,F63100,2774A4,A54F10,FC6EA3,6C59DC,AC8C14,611F27,F230E0,5CCD18,BB2A02,5A2B57,'.
'89ABF8,7EC25C,274482,2B5429,8048B4,FD5434,790E1F,87AC4D,E89DF4'
];
}
/**
* Custom error handler for PHP errors.
*
* @param int $errno Level of the error raised.
* @param string $errstr Error message.
* @param string $errfile Filename that the error was raised in.
* @param int $errline Line number the error was raised in.
*
* @return bool
*/
function zbx_err_handler($errno, $errstr, $errfile, $errline) {
// Suppress errors when calling with error control operator @function_name().
if ((error_reporting()
& ~(E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR)) == 0) {
return true;
}
// Don't show the call to this handler function.
error($errstr.' ['.CProfiler::getInstance()->formatCallStack().']', true);
return false;
}
/**
* Creates an array with all possible variations of time units.
* For example: '14d' => ['1209600', '1209600s', '20160m', '336h', '14d', '2w']
*
* @param string|array $values
*
* @return array
*/
function getTimeUnitFilters($values) {
if (is_array($values)) {
$res = [];
foreach ($values as $value) {
$res = array_merge($res, getTimeUnitFilters($value));
}
return array_unique($res, SORT_STRING);
}
$simple_interval_parser = new CSimpleIntervalParser();
if ($simple_interval_parser->parse($values) != CParser::PARSE_SUCCESS) {
return [$values];
}
$sec = timeUnitToSeconds($values);
$res = [$sec, $sec.'s'];
if ($sec % SEC_PER_MIN == 0) {
$res[] = floor($sec / SEC_PER_MIN).'m';
}
if ($sec % SEC_PER_HOUR == 0) {
$res[] = floor($sec / SEC_PER_HOUR).'h';
}
if ($sec % SEC_PER_DAY == 0) {
$res[] = floor($sec / SEC_PER_DAY).'d';
}
if ($sec % SEC_PER_WEEK == 0) {
$res[] = floor($sec / SEC_PER_WEEK).'w';
}
return $res;
}
/**
* Creates SQL filter to search all possible variations of time units.
*
* @param string $field_name
* @param string|array $values
*
* @return string
*/
function makeUpdateIntervalFilter($field_name, $values) {
$filters = [];
foreach (getTimeUnitFilters($values) as $filter) {
$filter = str_replace("!", "!!", $filter);
$filter = str_replace("%", "!%", $filter);
$filter = str_replace("_", "!_", $filter);
$filters[] = $field_name.' LIKE '.zbx_dbstr($filter).' ESCAPE '.zbx_dbstr('!');
$filters[] = $field_name.' LIKE '.zbx_dbstr($filter.';%').' ESCAPE '.zbx_dbstr('!');
}
$res = $filters ? implode(' OR ', $filters) : '';
if (count($filters) > 1) {
$res = '('.$res.')';
}
return $res;
}
/**
* Update profile with new time selector range.
*
* @param array $options
* @param string $options['profileIdx']
* @param int $options['profileIdx2']
* @param string|null $options['from']
* @param string|null $options['to']
*/
function updateTimeSelectorPeriod(array $options) {
if ($options['from'] !== null && $options['to'] !== null) {
CProfile::update($options['profileIdx'].'.from', $options['from'], PROFILE_TYPE_STR, $options['profileIdx2']);
CProfile::update($options['profileIdx'].'.to', $options['to'], PROFILE_TYPE_STR, $options['profileIdx2']);
}
}
/**
* Get profile stored 'from' and 'to'. If profileIdx is null then default values will be returned. If one of fields
* not exist in $options array 'from' and 'to' value will be read from user profile. Calculates from_ts, to_ts.
*
* @param array $options Array with period fields data: profileIdx, profileIdx2, from, to.
*
* @return array
*/
function getTimeSelectorPeriod(array $options) {
$profileIdx = array_key_exists('profileIdx', $options) ? $options['profileIdx'] : null;
$profileIdx2 = array_key_exists('profileIdx2', $options) ? $options['profileIdx2'] : null;
if ($profileIdx === null) {
$options['from'] = 'now-'.CSettingsHelper::get(CSettingsHelper::PERIOD_DEFAULT);
$options['to'] = 'now';
}
elseif (!array_key_exists('from', $options) || !array_key_exists('to', $options)
|| $options['from'] === null || $options['to'] === null) {
$options['from'] = CProfile::get($profileIdx.'.from',
'now-'.CSettingsHelper::get(CSettingsHelper::PERIOD_DEFAULT),
$profileIdx2
);
$options['to'] = CProfile::get($profileIdx.'.to', 'now', $profileIdx2);
}
$range_time_parser = new CRangeTimeParser();
$range_time_parser->parse($options['from']);
$options['from_ts'] = $range_time_parser->getDateTime(true)->getTimestamp();
$range_time_parser->parse($options['to']);
$options['to_ts'] = $range_time_parser->getDateTime(false)->getTimestamp();
return $options;
}
/**
* Get array of action statuses available for defined time range. For incorrect "from" or "to" all actions will be set
* to false.
*
* @param string $from Relative or absolute time, cannot be null.
* @param string $to Relative or absolute time, cannot be null.
*
* @return array
*/
function getTimeselectorActions($from, $to): array {
$ts_now = time();
$parser = new CRangeTimeParser();
$ts_from = ($parser->parse($from) !== CParser::PARSE_FAIL) ? $parser->getDateTime(true)->getTimestamp() : null;
$ts_to = ($parser->parse($to) !== CParser::PARSE_FAIL) ? $parser->getDateTime(false)->getTimestamp() : null;
$valid = ($ts_from !== null && $ts_to !== null);
$parser->parse('now-'.CSettingsHelper::get(CSettingsHelper::MAX_PERIOD));
$max_period = 1 + $ts_now - $parser->getDateTime(true)->getTimestamp();
return [
'can_zoomout' => ($valid && ($ts_to - $ts_from + 1 < $max_period)),
'can_decrement' => ($valid && ($ts_from > 0)),
'can_increment' => ($valid && ($ts_to < $ts_now - ZBX_MIN_PERIOD))
];
}
/**
* Convert relative date range string to translated string. Function does not check is passed date range correct.
*
* @param string $from Start date of date range.
* @param string $to End date of date range.
*
* @return string
*/
function relativeDateToText($from, $to) {
$key = $from.':'.$to;
$ranges = [
'now-1d/d:now-1d/d' => _('Yesterday'),
'now-2d/d:now-2d/d' => _('Day before yesterday'),
'now-1w/d:now-1w/d' => _('This day last week'),
'now-1w/w:now-1w/w' => _('Previous week'),
'now-1M/M:now-1M/M' => _('Previous month'),
'now-1y/y:now-1y/y' => _('Previous year'),
'now/d:now/d' => _('Today'),
'now/d:now' => _('Today so far'),
'now/w:now/w' => _('This week'),
'now/w:now' => _('This week so far'),
'now/M:now/M' => _('This month'),
'now/M:now' => _('This month so far'),
'now/y:now/y' => _('This year'),
'now/y:now' => _('This year so far')
];
if (array_key_exists($key, $ranges)) {
return $ranges[$key];
}
if ($to === 'now') {
$relative_time_parser = new CRelativeTimeParser();
if ($relative_time_parser->parse($from) == CParser::PARSE_SUCCESS) {
$tokens = $relative_time_parser->getTokens();
if (count($tokens) == 1 && $tokens[0]['type'] == CRelativeTimeParser::ZBX_TOKEN_OFFSET
&& $tokens[0]['sign'] === '-') {
$suffix = $tokens[0]['suffix'];
$value = (int) $tokens[0]['value'];
switch ($suffix) {
case 's':
if ($value < 60 || $value % 60 != 0) {
return _n('Last %1$d second', 'Last %1$d seconds', $value);
}
$value /= 60;
// break; is not missing here.
case 'm':
if ($value < 60 || $value % 60 != 0) {
return _n('Last %1$d minute', 'Last %1$d minutes', $value);
}
$value /= 60;
// break; is not missing here.
case 'h':
if ($value < 24 || $value % 24 != 0) {
return _n('Last %1$d hour', 'Last %1$d hours', $value);
}
$value /= 24;
// break; is not missing here.
case 'd':
return _n('Last %1$d day', 'Last %1$d days', $value);
case 'M':
return _n('Last %1$d month', 'Last %1$d months', $value);
case 'y':
return _n('Last %1$d year', 'Last %1$d years', $value);
}
}
}
}
return $from.' '.$to;
}
/**
* Get human readable time period.
*
* @param int $seconds
*
* @return string
*/
function secondsToPeriod(int $seconds): string {
$hours = floor($seconds / 3600);
$seconds -= $hours * 3600;
$minutes = floor($seconds / 60);
$seconds -= $minutes * 60;
$period = ($hours > 0) ? _n('%1$s hour', '%1$s hours', $hours) : '';
if ($minutes > 0) {
if ($period !== '') {
$period .= ', ';
}
$period .= _n('%1$s minute', '%1$s minutes', $minutes);
}
if ($seconds > 0 || $period === '') {
if ($period !== '') {
$period .= ', ';
}
$period .= _n('%1$s second', '%1$s seconds', $seconds);
}
return $period;
}
/**
* Generates UUID version 4.
*
* @param string $seed String to be hashed as md5 and used as UUID body.
*
* @return string
*/
function generateUuidV4($seed = '') {
$data = ($seed === '') ? random_bytes(16) : hex2bin(md5($seed));
// Set head of 7th byte to 0100 (0100xxxx)
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set head of 9th byte to 10 (10xxxxxx)
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return bin2hex($data);
}
/**
* Function returns predefined Leaflet Tile providers with parameters.
*
* @return array
*/
function getTileProviders(): array {
return [
'OpenStreetMap.Mapnik' => [
'name' => 'OpenStreetMap Mapnik',
'geomaps_tile_url' => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'geomaps_max_zoom' => '19',
'geomaps_attribution' => '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
],
'OpenTopoMap' => [
'name' => 'OpenTopoMap',
'geomaps_tile_url' => 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
'geomaps_max_zoom' => '17',
'geomaps_attribution' => 'Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: &copy; <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'
],
'Stamen.TonerLite' => [
'name' => 'Stamen Toner Lite',
'geomaps_tile_url' => 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png',
'geomaps_max_zoom' => '20',
'geomaps_attribution' => 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a> &mdash; Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
],
'Stamen.Terrain' => [
'name' => 'Stamen Terrain',
'geomaps_tile_url' => 'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}{r}.png',
'geomaps_max_zoom' => '18',
'geomaps_attribution' => 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a> &mdash; Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
],
'USGS.USTopo' => [
'name' => 'USGS US Topo',
'geomaps_tile_url' => 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
'geomaps_max_zoom' => '20',
'geomaps_attribution' => 'Tiles courtesy of the <a href="https://usgs.gov/">U.S. Geological Survey</a>'
],
'USGS.USImagery' => [
'name' => 'USGS US Imagery',
'geomaps_tile_url' => 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}',
'geomaps_max_zoom' => '20',
'geomaps_attribution' => 'Tiles courtesy of the <a href="https://usgs.gov/">U.S. Geological Survey</a>'
]
];
}