$item) { $path = $config['html_dir'] . '/includes/entities/' . $entity_type . '.inc.php'; if (is_file($path)) { include_once($path); } } /** * Callback function for replacing strings in the HTML buffer at the end of the script execution. * * @param string $buffer HTML buffer obtained from ob_start() * * @return string Modified buffer */ function html_callback($buffer) { global $config; global $cache_html; // Do not disclose version to unauthorized requests $version_param = $_SESSION['authenticated'] ? '?v=' . OBSERVIUM_VERSION : ''; // Define template strings for registered CSS/JS links and other elements $templates = [ 'css' => ' ' . PHP_EOL, 'style' => ' ' . PHP_EOL, 'js' => ' ' . PHP_EOL, 'script' => ' ' . PHP_EOL, // key-value 'meta-equiv' => ' ' . PHP_EOL, 'meta' => ' ' . PHP_EOL, ]; // Process and replace resources in the buffer foreach ($templates as $type => $template) { $uppercase_type = strtoupper($type); if (isset($GLOBALS['cache_html']['resources'][$type])) { $resource_string = '' . PHP_EOL; if ($type === 'meta-equiv' || $type === 'meta') { foreach ($GLOBALS['cache_html']['resources'][$type] as $name => $content) { //bdump($content); $resource_string .= str_replace([ '%%STRING_name%%', '%%STRING_content%%' ], [ $name, $content ], $template); } } else { foreach (array_unique($GLOBALS['cache_html']['resources'][$type]) as $content) { $resource_string .= str_replace('%%STRING%%', $content, $template); } } $resource_string .= ' ' . PHP_EOL; $buffer = str_replace('' . PHP_EOL, $resource_string, $buffer); } else { // Clean template string $buffer = str_replace('', '', $buffer); } } // Replace placeholders in the buffer with actual values $replacements = [ '##TITLE##' => html_callback_build_title(), '##PAGE_PANEL##' => $GLOBALS['cache_html']['page_panel'], '##UI_ALERTS##' => implode(PHP_EOL, (array)$GLOBALS['cache_html']['ui_alerts']), ]; // Return the modified HTML page source return array_str_replace($replacements, $buffer, TRUE); } /** * Set the title of the page based on various criteria. */ function html_callback_build_title() { global $config; global $vars; global $cache_html; if (!is_array($cache_html['title'])) { // Title not set by any page, fall back to nicecase'd page name: if ($vars['page'] && $_SESSION['authenticated']) { $cache_html['title'] = [nicecase($vars['page'])]; } else { // Main page or no page specified, leave the title empty $cache_html['title'] = []; } } // If a suffix is set, append it to the title if ($config['page_title_suffix']) { $cache_html['title'][] = $config['page_title_suffix']; } // If a prefix is set, prepend it to the title if ($config['page_title_prefix']) { array_unshift($cache_html['title'], $config['page_title_prefix']); } // Build the title with separators return escape_html(implode($config['page_title_separator'], $cache_html['title'])); } /** * Register an HTML resource * * Registers resource for use later (will be re-inserted via output buffer handler) * CSS and JS files default to the css/ and js/ directories respectively. * Scripts are inserted literally as passed in $name. * * @param string $type Type of resource (css/js/script) * @param string $content Filename or script content or array (for meta) */ // TESTME needs unit testing function register_html_resource($type, $content) { // If no path specified, default to subdirectory of resource type (for CSS and JS only) $type = strtolower($type); if (in_array($type, [ 'css', 'js' ]) && !str_contains($content, '/')) { $content = $type . '/' . $content; } // Insert into global variable, used in html callback function $GLOBALS['cache_html']['resources'][$type][] = $content; } /** * Register an HTML title section * * Registers title section for use in the html tag. * Calls can be stacked, and will be concatenated later by the HTML callback function. * * @param string $title Section title content */ // TESTME needs unit testing function register_html_title($title) { $GLOBALS['cache_html']['title'][] = $title; } function register_html_meta($name, $content, $tag = 'name') { if (safe_empty($content) || !is_alpha($name)) { return; } if ($tag !== 'name') { // http-equiv is multiplied $GLOBALS['cache_html']['resources']['meta-equiv'][$name] = escape_html($content); } else { $GLOBALS['cache_html']['resources']['meta'][$name] = escape_html($content); } } /** * Register an HTML alert block displayed in top of page. * * @param string $text Alert message * @param string $title Alert title if passed * @param string $severity Severity in list: info, danger, warning, success, recovery, suppressed, delay, disabled */ function register_html_alert($text, $title = NULL, $severity = 'info') { if (!$GLOBALS['config']['web_show_notifications']) { // suppress web ui alerts return; } // FIXME handle severity parameter with colour or icon? $ui_alerts = '<div width="100%" class="alert alert-' . $severity . '">'; if (!safe_empty($title)) { $ui_alerts .= '<h4>' . $title . '</h4>'; } $ui_alerts .= $text . '</div>'; $GLOBALS['cache_html']['ui_alerts'][] = $ui_alerts; } /** * Register an HTML panel section * * Registers left panel section. * Calls can be stacked, and will be concatenated later by the HTML callback function. * * @param string $html Section panel content */ // TESTME needs unit testing function register_html_panel($html = '') { if (!isset($GLOBALS['cache_html']['page_panel']) && (empty($html) || $html === 'default')) { // register default (ajax) panel // Load a default panel after whole page (only when visible) // $config['html_dir'] . "/includes/panels/default.inc.php" register_html_resource('script', "$(document).ready(function (e) { $('#myAffix:visible[data-panel=default]').load('/ajax/panel.php').attr('data-panel', 'loaded'); });"); // load when become visible register_html_resource('script', "$(window).resize(function (e) { $('#myAffix:visible[data-panel=default]').load('/ajax/panel.php').attr('data-panel', 'loaded'); });"); return; } // Just rename data attrib for panel placeholder from default register_html_resource('script', "$(document).ready(function (e) { $('#myAffix[data-panel=default]').attr('data-panel', 'register'); });"); $GLOBALS['cache_html']['page_panel'] = $html; } function http_match_referer($pattern) { if ($_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin') { return FALSE; } if (is_array_list($pattern)) { $match = FALSE; foreach ($pattern as $patt) { if ($match = preg_match($patt, $_SERVER['HTTP_REFERER'])) { break; } } } else { $match = preg_match($pattern, $_SERVER['HTTP_REFERER']); } return (bool)$match; } /** * Parse $_GET, $_POST and REQUEST_URI into $vars array * * @param array|string $vars_order Request variables order (POST, URI, GET) * @param boolean $auth this var or ($_SESSION['authenticated']) used for allow to use var_decode() * * @return array array of vars */ function get_vars($vars_order = [], $auth = FALSE) { if (is_string($vars_order)) { $vars_order = explode(' ', $vars_order); } elseif (empty($vars_order) || !is_array($vars_order)) { $vars_order = [ 'POST', 'URI', 'GET' ]; // Default order } // Content-Type=>application/x-www-form-urlencoded $content_type = $_SERVER['HTTP_CONTENT_TYPE'] ?? $_SERVER['CONTENT_TYPE']; // Allow using var_decode(), this prevents to use potentially unsafe serialize functions $auth = $auth || $_SESSION['authenticated']; $vars = []; foreach ($vars_order as $order) { $order = strtoupper($order); switch ($order) { case 'JSON': //r(getallheaders()); //exit; // https://stackoverflow.com/questions/8893574/php-php-input-vs-post if (!in_array($content_type, ['application/x-www-form-urlencoded', 'multipart/form-data-encoded'])) { //$json = @json_decode(trim(file_get_contents("php://input")), TRUE, 512, OBS_JSON_DECODE); $json = safe_json_decode(trim(file_get_contents("php://input"))); if (is_array_assoc($json)) { //$vars = array_merge_indexed($vars, $json); $vars = $json; // Currently just override $vars, see ajax actions //$vars_got['JSON'] = 1; } } break; case 'POST': // Parse POST variables into $vars foreach ($_POST as $name => $value) { // Var names sanitize if (!preg_match(OBS_PATTERN_VAR_NAME, $name)) { continue; } if (!isset($vars[$name])) { $vars[$name] = $auth ? var_decode($value) : $value; if (is_string($vars[$name]) && preg_match(OBS_PATTERN_XSS, $vars[$name])) { // Prevent any <script> html tag inside vars, exclude any possible XSS with scripts unset($vars[$name]); } //$vars_got['POST'] = 1; } } break; case 'URI': case 'URL': // Parse URI into $vars $segments = explode('/', trim($_SERVER['REQUEST_URI'], '/')); $compressed = $auth && in_array('compressed=1', $segments, TRUE); // vars can be compressed by str_compress() //r($segments); //r($_SERVER['REQUEST_URI']); $last_var = ''; foreach ($segments as $pos => $segment) { $name_value = str_contains($segment, '='); if ($pos === 0 && !$name_value) { if (!preg_match(OBS_PATTERN_XSS, $segment)) { // Prevent any <script> html tag inside vars, exclude any possible XSS with scripts $segment = urldecode($segment); $vars['page'] = $segment; } $last_var = 'page'; //$vars_got['URI'] = 1; } elseif ($last_var === 'port' && !$name_value) { // Append segment to previous var // Correctly parse /port=GigabitEthernet0/1/0.2571 if (!preg_match(OBS_PATTERN_XSS, $segment)) { // Prevent any <script> html tag inside vars, exclude any possible XSS with scripts $segment = urldecode($segment); $vars[$last_var] .= '/' . $segment; } } else { [ $name, $value ] = explode('=', $segment, 2); $last_var = $name; // Var names sanitize if (!preg_match(OBS_PATTERN_VAR_NAME, $name)) { continue; } if (!isset($vars[$name])) { if (!$name_value || $value === '') { $vars[$name] = 'yes'; } else { if ($compressed && $value_uncompress = str_decompress($value)) { $value = $value_uncompress; unset($value_uncompress); } else { // rawurldecode() instead of urldecode() to translate %n and not mangle + // %7F (DEL, delete) - control char, not defined in HTML 4 standard // %05 (ENQ, enquiry) - control char, not defined in HTML 4 standard $value = str_replace(['%7F', '%05'], ['/', '%'], rawurldecode($value)); } if (preg_match(OBS_PATTERN_XSS, $value)) { // Prevent any <script> html tag inside vars, exclude any possible XSS with scripts continue; } // Better to understand quoted vars $vars[$name] = get_var_csv($value, $auth); if (is_string($vars[$name]) && preg_match(OBS_PATTERN_XSS, $vars[$name])) { // Prevent any <script> html tag inside vars, exclude any possible XSS with scripts unset($vars[$name]); } } //$vars_got['URI'] = 1; } } } break; case 'GET': // Parse GET variable into $vars $compressed = $auth && get_var_true($_GET['compressed']); // vars can be compressed by str_compress() foreach ($_GET as $name => $value) { // Var names sanitize if (!preg_match(OBS_PATTERN_VAR_NAME, $name)) { continue; } if (!isset($vars[$name])) { if ($compressed && $value_uncompress = str_decompress($value)) { $value = $value_uncompress; unset($value_uncompress); } else { // rawurldecode() instead of urldecode() to translate %n and not mangle + // %7F (DEL, delete) - control char, not defined in HTML 4 standard // %05 (ENQ, enquiry) - control char, not defined in HTML 4 standard $value = str_replace(['%7F', '%05'], ['/', '%'], rawurldecode($value)); } if (preg_match(OBS_PATTERN_XSS, $value)) { // Prevent any <script> html tag inside vars, exclude any possible XSS with scripts continue; } // Better to understand quoted vars $vars[$name] = get_var_csv($value, $auth); if (is_string($vars[$name]) && preg_match(OBS_PATTERN_XSS, $vars[$name])) { // Prevent any <script> html tag inside vars, exclude any possible XSS with scripts unset($vars[$name]); } //$vars_got['GET'] = 1; } } break; } } //print_success("Got [".implode(', ', array_keys($vars_got))."] vars ($content_type)."); // Always convert location to array if (isset($vars['location'])) { if ($vars['location'] === '') { // Unset location if is empty string unset($vars['location']); } elseif (is_array($vars['location'])) { // Additionally, decode locations if array entries encoded foreach ($vars['location'] as $k => $location) { $vars['location'][$k] = $auth ? var_decode($location) : $location; } } else { // All other location strings covert to array $vars['location'] = [$vars['location']]; } } return ($vars); } function form_action(&$vars) { global $config; if (!isset($vars['action']) || is_ajax() || !is_alpha($vars['action'])) { //bdump($vars); return FALSE; } if ($vars['page'] === 'wmap') { // Weathermap return FALSE; } if ($_SESSION['userlevel'] < 7) { print_error_permission('Action not allowed.'); return FALSE; } if (!request_token_valid($vars)) { //r($vars); return FALSE; } if ($config['devel']) { bdump($vars); } $limitwrite = $_SESSION['userlevel'] >= 8; $securewrite = $_SESSION['userlevel'] >= 9; $readwrite = $_SESSION['userlevel'] >= 10; $vars['action'] = strtolower($vars['action']); // force action to lowercase if (is_file($config['html_dir'] . "/includes/actions/" . $vars['action'] . ".inc.php")) { return include($config['html_dir'] . "/includes/actions/" . $vars['action'] . ".inc.php"); } $target = explode('_', $vars['action'], 2)[0]; if ($target !== $vars['action'] && is_file($config['html_dir'] . "/includes/actions/" . $target . ".inc.php")) { return include($config['html_dir'] . "/includes/actions/" . $target . ".inc.php"); } // Unknown action return FALSE; } /** * Validate requests by compare session and request tokens. * This prevents a CSRF attacks * * @param string|array $token Passed from request token or array with 'requesttoken' param inside. * @param string $json Store status of token validation as json for ajax requests * * @return boolean TRUE if session requesttoken same as passed from request */ function request_token_valid($token = NULL, &$json = '') { if (is_array($token)) { // If $vars array passed, fetch our default 'requesttoken' param $token = $token['requesttoken']; } // Set output silent if json status requested $silent = func_num_args() >= 2; //print_vars($_SESSION['requesttoken']); //print_vars($token); // See: https://stackoverflow.com/questions/6287903/how-to-properly-add-csrf-token-using-php // Session token generated after valid user auth in html/includes/authenticate.inc.php if (empty($_SESSION['requesttoken'])) { // User didn't authenticate //print_warning("Request passed by unauthorized user."); if ($silent) { $json = safe_json_encode(['status' => 'failed', 'message' => 'Request passed by unauthorized user.', 'class' => 'danger']); } return FALSE; } if (empty($token)) { // Token not passed, WARNING seems as CSRF attack if (!$silent) { print_error("WARNING. Possible CSRF attack with EMPTY request token."); } else { $json = safe_json_encode(['status' => 'failed', 'message' => 'WARNING. Possible CSRF attack with EMPTY request token.', 'class' => 'danger']); } return FALSE; } if (hash_equals($_SESSION['requesttoken'], $token)) { // Correct session and request tokens, all good if ($silent) { $json = safe_json_encode(['status' => 'ok']); } return TRUE; } // Passed incorrect request token, // WARNING seems as CSRF attack if (!$silent) { print_error("WARNING. Possible CSRF attack with INCORRECT request token."); } else { $json = safe_json_encode([ 'status' => 'failed', 'message' => 'WARNING. Possible CSRF attack with INCORRECT request token.', 'class' => 'danger', /* 'session_t' => $_SESSION['requesttoken'], 'request_t' => $token */]); } ///FIXME. need an user actions log return FALSE; } // TESTME needs unit testing /** * Generates base64 data uri with alert graph * * @return string */ function generate_alert_graph($graph_array) { global $config, $vars; $vars = $graph_array; $auth = is_cli() ? TRUE : $GLOBALS['auth']; // Always set $auth to true for cli $vars['image_data_uri'] = TRUE; $vars['height'] = '150'; $vars['width'] = '400'; $vars['legend'] = 'no'; $vars['from'] = get_time('twoday'); $vars['to'] = get_time(); include($config['html_dir'] . '/includes/graphs/graph.inc.php'); return $image_data_uri; } // TESTME needs unit testing // DOCME needs phpdoc block function datetime_preset($preset) { $begin_fmt = 'Y-m-d 00:00:00'; $end_fmt = 'Y-m-d 23:59:59'; switch ($preset) { case 'sixhours': $from = date('Y-m-d H:i:00', strtotime('-6 hours')); $to = date('Y-m-d H:i:59'); break; case 'today': $from = date($begin_fmt); $to = date($end_fmt); break; case 'yesterday': $from = date($begin_fmt, strtotime('-1 day')); $to = date($end_fmt, strtotime('-1 day')); break; case 'tweek': $from = (date('l') === 'Monday') ? date($begin_fmt) : date($begin_fmt, strtotime('last Monday')); $to = (date('l') === 'Sunday') ? date($end_fmt) : date($end_fmt, strtotime('next Sunday')); break; case 'lweek': $from = date($begin_fmt, strtotime('-6 days')); $to = date($end_fmt); break; case 'tmonth': $from = date('Y-m-01 00:00:00'); $to = date('Y-m-t 23:59:59'); break; case 'lmonth': $timestamp = strtotime('previous month'); $from = date('Y-m-01 00:00:00', $timestamp); $to = date('Y-m-t 23:59:59', $timestamp); break; case 'tquarter': case 'lquarter': $quarter = ceil(date('m') / 3); // Current quarter if ($preset === 'lquarter') { $quarter = $quarter - 1; // Previous quarter } $year = date('Y'); if ($quarter < 1) { $year -= 1; $quarter = 4; } $tmonth = $quarter * 3; $fmonth = $tmonth - 2; $from = $year . '-' . zeropad($fmonth) . '-01 00:00:00'; $to = date('Y-m-t 23:59:59', strtotime($year . '-' . $tmonth . '-01')); break; case 'tyear': $from = date('Y-01-01 00:00:00'); $to = date('Y-12-31 23:59:59'); break; case 'lyear': $from = date($begin_fmt, strtotime('previous year + 1 day')); $to = date($end_fmt); break; } return ['from' => $from, 'to' => $to]; } // TESTME needs unit testing // DOCME needs phpdoc block function bug() { echo('<div class="alert alert-error"> <button type="button" class="close" data-dismiss="alert">×</button> <strong>Bug!</strong> Please report this to the Observium development team. </div>'); } /** * This function determines detailed info of web browser by User-Agent agent string. * If User-Agent not passed, used current from $_SERVER['HTTP_USER_AGENT'] * * @param string $user_agent Custom User-Agent string, by default, the value of HTTP User-Agent header is used * * @return array Return detected browser info: user_agent, type, icon, platform, browser, version, * browser_full - full browser name (ie: Chrome 43.0) * svg - supported or not svg images (TRUE|FALSE), * screen_ratio - for HiDPI screens it more that 1, * screen_resolution - full resolution of client screen (if exist), * screen_size - initial size of browser window (if exist) */ // TESTME! needs unit testing function detect_browser($user_agent = NULL) { $ua_custom = !is_null($user_agent); // Used custom user agent? if (!$ua_custom && isset($GLOBALS['cache']['detect_browser'])) { // Return cached info return $GLOBALS['cache']['detect_browser']; } if (!$ua_custom) { $user_agent = $_SERVER['HTTP_USER_AGENT']; } // Detect Browser name, version and platform $ua_info = []; if (!empty($user_agent)) { $parser = new \donatj\UserAgent\UserAgentParser(); $ua = $parser->parse($user_agent); //r($ua); $ua_info['browser'] = $ua->browser(); $ua_info['version'] = $ua->browserVersion(); $ua_info['platform'] = str_replace('Macintosh', 'MacOS', $ua->platform()); $ua_info['browser_full'] = $ua_info['browser'] . ' ' . preg_replace('/^([^\.]+(?:\.[^\.]+)?).*$/', '\1', $ua_info['version']); //r($ua_info); } // Detect Browser type & icon $type = get_browser_type($user_agent); $icon = get_browser_icon($type, $ua_info['platform']); $detect_browser = [ 'user_agent' => $user_agent, 'type' => $type, 'icon' => $icon, 'browser_full' => $ua_info['browser_full'], 'browser' => $ua_info['browser'], 'version' => $ua_info['version'], 'platform' => $ua_info['platform'] ]; // For custom UA, do not cache and return only base User-Agent info if ($ua_custom) { return $detect_browser; } // Load screen and DPI detector. This set cookies with: get_browser_screen($detect_browser); $GLOBALS['cache']['detect_browser'] = $detect_browser; // Store to cache if (isset($GLOBALS['config']['devel']) && $GLOBALS['config']['devel']) { bdump($GLOBALS['cache']['detect_browser']); } return $GLOBALS['cache']['detect_browser']; } /** * This function determines type of web browser for current User-Agent (mobile/tablet/tv/generic). * For more detailed browser info and custom User-Agent use detect_browser() * * @return string Return type of browser (generic/mobile/tablet) */ function get_browser_type($user_agent = NULL) { $detect = new Mobile_Detect; if (!is_null($user_agent)) { // Set custom User-Agent $detect->setUserAgent($user_agent); } // Detect Browser type if ($detect->isMobile()) { if ($detect->isTablet()) { // Any tablet device. return 'tablet'; } // Any phone device (exclude tablets). return 'mobile'; } if ($detect->isTV()) { return 'tv'; } return 'generic'; } function get_browser_icon($type, $platform) { if ($type === 'mobile') { return 'icon-mobile-phone'; } if ($type === 'tablet') { return 'icon-tablet'; } if ($type === 'tv') { return 'icon-film'; } switch (strtolower($platform)) { case 'macintosh': case 'macos': return 'icon-apple'; case 'windows': return 'icon-windows'; case 'linux': return 'icon-linux'; } return 'icon-laptop'; } function get_browser_screen(&$detect_browser) { if (is_graph() || is_api()) { return; } // Load screen and DPI detector. This set cookies with: // $_COOKIE['observium_screen_ratio'] - if ratio >= 2, then HiDPI screen is used // $_COOKIE['observium_screen_resolution'] - screen resolution 'width x height', ie: 1920x1080 // $_COOKIE['observium_screen_size'] - current window size (less than resolution) 'width x height', ie: 1097x456 register_html_resource('js', 'observium-screen.js'); // Additional browser info (screen_ratio, screen_size) $detect_browser['screen_ratio'] = $_COOKIE['observium_screen_ratio'] ?? 2; if (isset($_COOKIE['observium_screen_resolution'])) { $detect_browser['screen_resolution'] = $_COOKIE['observium_screen_resolution']; $detect_browser['screen_size'] = $_COOKIE['observium_screen_size']; } return $detect_browser['screen_ratio']; } // TESTME needs unit testing // DOCME needs phpdoc block function data_uri($file, $mime) { $contents = file_get_contents($file); $base64 = base64_encode($contents); return ('data:' . $mime . ';base64,' . $base64); } // TESTME needs unit testing // DOCME needs phpdoc block function toner_map($descr, $colour) { return str_istarts($descr, $GLOBALS['config']['toner'][$colour]); } // TESTME needs unit testing // DOCME needs phpdoc block function toner_to_colour($descr, $percent) { if (str_starts($descr, 'C') || toner_map($descr, "cyan")) { $colour['left'] = "B6F6F6"; $colour['right'] = "33B4B1"; } elseif (str_starts($descr, 'M') || toner_map($descr, "magenta")) { $colour['left'] = "FBA8E6"; $colour['right'] = "D028A6"; } elseif (str_starts($descr, 'Y') || toner_map($descr, "yellow")) { $colour['left'] = "FFF764"; $colour['right'] = "DDD000"; } elseif (str_starts($descr, 'K') || toner_map($descr, "black")) { $colour['left'] = "888787"; $colour['right'] = "555555"; } elseif (str_starts($descr, 'R') || toner_map($descr, "red")) { $colour['left'] = "FB6A4A"; $colour['right'] = "CB181D"; } if (!isset($colour['left'])) { $colour = get_percentage_colours(100 - $percent); $colour['found'] = FALSE; } else { $colour['found'] = TRUE; } return $colour; } // TESTME needs unit testing // DOCME needs phpdoc block function generate_link($text, $vars, $new_vars = [], $escape = TRUE) { if ($escape) { $text = escape_html($text); } return '<a href="' . generate_url($vars, $new_vars) . '">' . $text . '</a>'; } // TESTME needs unit testing // DOCME needs phpdoc block function pagination(&$vars, $total, $options = []) { // Compatibility with pre-options if (!is_array($options)) { $options = [ 'return_vars' => (bool)$options ]; } $pagesizes = [ 10, 20, 50, 100, 500, 1000, 10000, 50000 ]; // Permitted pagesizes if (is_numeric($vars['pagesize'])) { $per_page = (int)$vars['pagesize']; } elseif (isset($_SESSION['pagesize'])) { $per_page = $_SESSION['pagesize']; } else { $per_page = $GLOBALS['config']['web_pagesize']; } // Widget/ajax do not set session var for pagesize if (!$vars['short'] && !is_ajax()) { // Permit fixed pagesizes only (except $vars['short'] == TRUE) foreach ($pagesizes as $pagesize) { if ($per_page <= $pagesize) { $per_page = $pagesize; break; } } if (isset($vars['pagesize']) && $vars['pagesize'] != $_SESSION['pagesize']) { if ($vars['pagesize'] != $GLOBALS['config']['web_pagesize']) { session_set_var('pagesize', $per_page); // Store pagesize in session only if changed default } elseif (isset($_SESSION['pagesize'])) { session_unset_var('pagesize'); // Reset pagesize from session } } } $vars['pagesize'] = $per_page; // Return back current pagesize $page = (int)$vars['pageno']; $lastpage = ceil($total / $per_page); if ($page < 1) { $page = 1; } elseif (!$options['return_vars'] && $lastpage < $page) { $page = (int)$lastpage; } $vars['pageno'] = $page; // Return back current pageno if ($options['return_vars']) { return ''; } // Silent exit (needed for detect default pagesize/pageno) $start = ($page - 1) * $per_page; $prev = $page - 1; $next = $page + 1; $lpm1 = $lastpage - 1; $adjacents = 3; $pagination = ''; // Show pagination if total > 99, total > page size, or web_always_paginate is set. if ($total > 99 || $total > $per_page || (isset($GLOBALS['config']['web_always_paginate']) && $GLOBALS['config']['web_always_paginate'] === 1)) { if ($total > 9999) { $total_text = format_si($total); } else { $total_text = $total; } $pagination .= '<div class="row">' . PHP_EOL . ' <div class="col-lg-1 col-md-2 col-sm-2" style="display: inline-block;">' . PHP_EOL . //' <span class="btn disabled" style="line-height: 20px;">'.$total.' Items</span>' . PHP_EOL . ' <div class="box box-solid" style="padding: 4px 12px;">' . $total_text . ' Items</div>' . PHP_EOL . ' </div>' . PHP_EOL . ' <div class="col-lg-10 col-md-8 col-sm-8">' . PHP_EOL . ' <div class="pagination pagination-centered"><ul>' . PHP_EOL; if ($prev) { //$pagination .= ' <li><a href="'.generate_url($vars, array('pageno' => 1)).'">First</a></li>' . PHP_EOL; $pagination .= ' <li><a href="' . generate_url($vars, ['pageno' => $prev]) . '">Prev</a></li>' . PHP_EOL; } if ($lastpage < 7 + ($adjacents * 2)) { for ($counter = 1; $counter <= $lastpage; $counter++) { if ($counter == $page) { $pagination .= "<li class='active'><a>$counter</a></li>"; } else { $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => $counter]) . "'>$counter</a></li>"; } } } elseif ($lastpage > 5 + ($adjacents * 2)) { if ($page < 1 + ($adjacents * 2)) { for ($counter = 1; $counter < 4 + ($adjacents * 2); $counter++) { if ($counter == $page) { $pagination .= "<li class='active'><a>$counter</a></li>"; } else { $class = ''; //if ($counter > 9) //{ // $class = ' class="hidden-md hidden-sm hidden-xs"'; //} //else if ($counter > 6) //{ // $class = ' class="hidden-sm hidden-xs"'; //} $pagination .= "<li$class><a href='" . generate_url($vars, ['pageno' => $counter]) . "'>$counter</a></li>"; } } $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => $lpm1]) . "'>$lpm1</a></li>"; $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => $lastpage]) . "'>$lastpage</a></li>"; } elseif ($lastpage - ($adjacents * 2) > $page && $page > ($adjacents * 2)) { $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => '1']) . "'>1</a></li>"; $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => '2']) . "'>2</a></li>"; for ($counter = $page - $adjacents; $counter <= $page + $adjacents; $counter++) { if ($counter == $page) { $pagination .= "<li class='active'><a>$counter</a></li>"; } else { $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => $counter]) . "'>$counter</a></li>"; } } $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => $lpm1]) . "'>$lpm1</a></li>"; $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => $lastpage]) . "'>$lastpage</a></li>"; } else { $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => '1']) . "'>1</a></li>"; $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => '2']) . "'>2</a></li>"; for ($counter = $lastpage - (2 + ($adjacents * 2)); $counter <= $lastpage; $counter++) { if ($counter == $page) { $pagination .= "<li class='active'><a>$counter</a></li>"; } else { $class = ''; //if ($lastpage - $counter > 9) //{ // $class = ' class="hidden-md hidden-sm hidden-xs"'; //} //else if ($lastpage - $counter > 6) //{ // $class = ' class="hidden-sm hidden-xs"'; //} $pagination .= "<li$class><a href='" . generate_url($vars, ['pageno' => $counter]) . "'>$counter</a></li>"; } } } } if ($page < $counter - 1) { $pagination .= "<li><a href='" . generate_url($vars, ['pageno' => $next]) . "'>Next</a></li>"; # No need for "Last" as we don't have "First", 1, 2 and the 2 last pages are always in the list. #$pagination.= "<li><a href='".generate_url($vars, array('pageno' => $lastpage))."'>Last</a></li>"; } elseif ($lastpage > 1) { $pagination .= "<li class='active'><a>Next</a></li>"; #$pagination.= "<li class='active'><a>Last</a></li>"; } $pagination .= "</ul></div></div>"; //$values = array('' => array('name')) foreach ($pagesizes as $pagesize) { $value = generate_url($vars, ['pagesize' => $pagesize, 'pageno' => floor(float_div($start, $pagesize))]); $name = $pagesize == $GLOBALS['config']['web_pagesize'] ? "[ $pagesize ]" : $pagesize; $values[$value] = ['name' => $name, 'class' => 'text-center']; } $element = [ 'type' => 'select', 'class' => 'pagination', 'id' => 'pagesize', 'name' => '# ' . $per_page, 'width' => '90px', 'onchange' => "window.open(this.options[this.selectedIndex].value,'_top')", 'value' => $per_page, 'data-style' => 'box', 'values' => $values ]; $pagination .= ' <div class="col-lg-1 col-md-2 col-sm-2"> <form class="pull-right pagination" action="#">'; $pagination .= generate_form_element($element); $pagination .= '</form></div></div>'; } return $pagination; } // TESTME needs unit testing // DOCME needs phpdoc block function generate_url($vars, $new_vars = []) { $vars = !safe_empty($vars) ? array_merge($vars, (array)$new_vars) : (array)$new_vars; $url = !safe_empty($vars['page']) ? urlencode($vars['page']) : ''; unset($vars['page']); if (!str_ends($url, '/')) { $url .= '/'; } foreach ($vars as $var => $value) { if ($var === "username" || $var === "password") { // Ignore these vars. They shouldn't end up in URLs. continue; } if (is_array($value)) { // Keep a numeric array list as a comma list $value_implode = is_array_numeric($value) ? implode(',', $value) : var_encode($value); $url .= urlencode($var) . '=' . $value_implode . '/'; } elseif (!is_numeric($var) && !safe_empty($value) && !str_contains($var, "opt")) { // rawurlencode() according change in r12351 $url .= urlencode($var) . '=' . rawurlencode(str_replace([ '%', '/' ], [ '%05', '%7F' ], $value)) . '/'; // %7F converted back to / in get_vars() } } // If we're being generated outside the web interface, prefix the generated URL to make it work properly. if (is_cli()) { if ($GLOBALS['config']['web_url'] === 'http://localhost:80/') { // default // override default web_url by http://localhost/ $url = 'http://' . get_localhost() . '/' . $url; } else { $url = $GLOBALS['config']['web_url'] . $url; } } return $url; } function generate_html_attribs($attribs) { if (!is_array($attribs)) { return ''; } // Make some common operations with attribs, i.e. confirmation process_html_attribs($attribs); // Filter attributes (data-*, aria-*, role, style, class) //$attrib_pattern = '/^(data\-[_\w\-]+|aria\-[_\w\-]+|role|class|style|onclick)$/'; // More free form HTML attributes, ie: https://www.w3schools.com/tags/ref_attributes.asp $attrib_pattern = '/^((data|aria|http|accept)\-[a-z_\-]+|[a-z]+)$/'; $elements = []; foreach ($attribs as $attr => $value) { if (is_array($value)) { $value = implode(' ', $value); } elseif (safe_empty($value)) { continue; } if (preg_match($attrib_pattern, $attr)) { $elements[] = escape_html($attr) . '="' . escape_html($value) . '"'; } } if (!empty($elements)) { return implode(' ', $elements); } return ''; } function process_html_attribs(&$attribs) { if (isset($attribs['data-toggle'])) { // Enable item specific JS/CSS/Script switch ($attribs['data-toggle']) { case 'confirm': case 'confirmation': if ($attribs['data-toggle'] === 'confirmation') { $attribs['data-toggle'] = 'confirm'; } // Bootstrap-Confirmation register_html_resource('js', 'bootstrap-confirmation.min.js'); //register_html_resource('script', '$("[data-toggle=\'' . $attribs['data-toggle'] . // '\']").confirmation({rootSelector: \'[data-toggle=' . $attribs['data-toggle'] . ']\',});'); //$script_options = [ 'rootSelector: \'[data-toggle=' . $attribs['data-toggle'] . ']\'' ]; if (!isset($attribs['data-btn-ok-label'])) { // default "Yes" //$attribs['data-btn-ok-label'] = 'Yes'; } if (!isset($attribs['data-btn-ok-class'])) { // default "btn btn-xs btn-primary" } if (!isset($attribs['data-btn-ok-icon'])) { // default "glyphicon glyphicon-ok" //$attribs['data-btn-ok-icon'] = 'Yes'; } if (!isset($attribs['data-btn-cancel-label'])) { // default "No" //$attribs['data-btn-cancel-label'] = 'Cheese'; } if (!isset($attribs['data-btn-cancel-class'])) { // default "btn btn-xs btn-default" //$attribs['data-btn-cancel-class'] = 'btn-small btn-warning'; } if (!isset($attribs['data-btn-cancel-icon'])) { // default "glyphicon glyphicon-remove" //$attribs['data-btn-cancel-icon'] = 'icon-sort'; } // migrate from popConfirm if (!isset($attribs['data-title'])) { $attribs['data-title'] = 'Confirmation'; } if (!isset($attribs['data-html'])) { $attribs['data-html'] = 'true'; } if (!isset($attribs['data-singleton'])) { $attribs['data-singleton'] = 'true'; } if (!isset($attribs['data-popout'])) { $attribs['data-popout'] = 'true'; } if (isset($attribs['data-confirm-placement'])) { $attribs['data-placement'] = $attribs['data-confirm-placement']; unset($attribs['data-confirm-placement']); } if (isset($attribs['data-confirm-content'])) { $attribs['data-content'] = $attribs['data-confirm-content']; unset($attribs['data-confirm-content']); } //register_html_resource('script', '$("[data-toggle=\'' . $attribs['data-toggle'] . '\']").confirmation({' . implode(', ', $script_options) . '});'); break; } } } // TESTME needs unit testing // DOCME needs phpdoc block function generate_feed_url($vars) { if (!class_exists('SimpleXMLElement')) { return FALSE; } // Break if class SimpleXMLElement is not available. if (!is_numeric($_SESSION['user_id']) || !is_numeric($_SESSION['userlevel'])) { return FALSE; } if ($key = get_user_pref($_SESSION['user_id'], 'atom_key')) { $param = []; $param[] = rtrim($GLOBALS['config']['base_url'], '/') . '/feed.php?id=' . $_SESSION['user_id']; $param[] = 'hash=' . encrypt($_SESSION['user_id'] . '|' . $_SESSION['userlevel'] . '|' . $_SESSION['auth_mechanism'], $key); $feed_type = 'atom'; foreach ($vars as $var => $value) { if ($value != '') { switch ($var) { case 'v': if ($value == 'rss') { $param[] = "$var=rss"; $feed_type = 'rss'; } break; case 'feed': $title = "Observium :: " . ucfirst($value) . " Feed"; $param[] = 'size=' . $GLOBALS['config']['frontpage']['eventlog']['items']; // no break here case 'size': $param[] = "size=" . (int)$value; break; } } } $baseurl = implode('&', $param); return '<link href="' . $baseurl . '" rel="alternate" title="' . escape_html($title) . '" type="application/' . $feed_type . '+xml" />'; } return FALSE; } // TESTME needs unit testing // DOCME needs phpdoc block function generate_location_url($location, $vars = []) { if ($location === '') { $location = OBS_VAR_UNSET; } $value = var_encode($location); return generate_url(['page' => 'devices', 'location' => $value], $vars); } // TESTME needs unit testing // DOCME needs phpdoc block function generate_overlib_content($graph_array, $text = NULL, $escape = TRUE) { $graph_array['height'] = "100"; $graph_array['width'] = "220"; if ($escape) { $text = escape_html($text); } $content = '<div style="width: 590px;"><span style="font-weight: bold; font-size: 16px;">' . $text . '</span><br />'; /* $box_args = array('body-style' => 'width: 590px;'); if (strlen($text)) { $box_args['title'] = $text; } $content = generate_box_open($box_args); */ foreach ([ 'day', 'week', 'month', 'year' ] as $period) { $graph_array['from'] = get_time($period); $content .= generate_graph_tag($graph_array); } $content .= "</div>"; //$content .= generate_box_close(); return $content; } // TESTME needs unit testing // DOCME needs phpdoc block function get_percentage_colours($percentage) { if ($percentage > '90') { $background['left'] = 'cb181d'; $background['right'] = 'fb6a4a'; $background['class'] = 'error'; } elseif ($percentage > '80') { $background['left'] = 'cc4c02'; $background['right'] = 'fe9929'; $background['class'] = 'warning'; } elseif ($percentage > '60') { $background['left'] = '6a51a3'; $background['right'] = '9e9ac8'; $background['class'] = 'information'; } elseif ($percentage > '30') { $background['left'] = '045a8d'; $background['right'] = '74a9cf'; $background['class'] = 'information'; } else { $background['left'] = '4d9221'; $background['right'] = '7fbc41'; $background['class'] = 'information'; } return ($background); } /** * Generate common popup links which uses ajax/entitypopup.php * * @param string $type Popup type, see possible types in html/ajax/entitypopup.php * @param string $text Text used as link name and ajax data * @param array $vars Array for generate url * @param string $class Additional css classes for link * @param boolean $escape Escape or not text in url * * @return string Returns string with link, when hover on this link show popup message based on type */ function generate_popup_link($type, $text = NULL, $vars = [], $class = NULL, $escape = TRUE) { if (!is_string($type) || !is_string($text)) { return ''; } if ($type === 'ip') { $addresses = []; // Find networks if (preg_match_all(OBS_PATTERN_IP_NET_FULL, $text, $matches)) { $addresses += $matches['ip_network']; } // Find single addresses if (preg_match_all(OBS_PATTERN_IP_FULL, $text, $matches)) { $addresses += $matches['ip']; } //r($addresses); if (!safe_empty($addresses)) { $return = $escape ? escape_html($text) : $text; // escape before replace (ip nets not escaped anyway) foreach ($addresses as $address) { [ $ip, $net ] = explode('/', $address); $ip_version = get_ip_version($ip); if ($ip_version === 6) { // Compress IPv6 $address_compressed = ip_compress($ip); if (!safe_empty($net)) { $address_compressed .= '/' . $net; } } else { // IPv4 always compressed :) $address_compressed = $address; } $ip_type = get_ip_type($ip); //print_warning("$address : $ip_type"); // Do not linkify some types of ip addresses if (in_array($ip_type, [ 'loopback', 'unspecified', 'broadcast', 'private', 'cgnat', 'link-local', 'reserved' ])) { if (!safe_empty($class)) { $link = '<span class="' . $class . '">' . $address_compressed . '</span>'; $return = str_replace($address, $link, $return); } elseif ($ip_version === 6) { $return = str_replace($address, $address_compressed, $return); } continue; } $url = !safe_empty($vars) ? generate_url($vars) : 'javascript:void(0)'; // If vars empty, set link not clickable $link = '<a href="' . $url . '" class="entity-popup' . ($class ? " $class" : '') . '" data-eid="' . $ip . '" data-etype="' . $type . '">' . $address_compressed . '</a>'; $return = str_replace($address, $link, $return); } return $return; } } elseif ($type === 'autodiscovery') { $data = $text; $text = get_icon('info'); $escape = FALSE; } $url = safe_count($vars) ? generate_url($vars) : 'javascript:void(0)'; // If vars empty, set link not clickable $data = $data ?? $text; if ($escape) { $text = escape_html($text); } return '<a href="' . $url . '" class="entity-popup' . ($class ? " $class" : '') . '" data-eid="' . $data . '" data-etype="' . $type . '">' . $text . '</a>'; } function generate_tooltip_time($timestamp, $text = '') { if (is_numeric($timestamp) && $timestamp > OBS_MIN_UNIXTIME) { // Unixtime $timediff = get_time() - $timestamp; $timetext = format_uptime($timediff, "short-3"); if (!safe_empty($text)) { $timetext .= " $text"; } return generate_tooltip_link('', $timetext, format_unixtime($timestamp), NULL); } // Timestamp $timediff = get_time() - strtotime($timestamp); $timetext = format_uptime($timediff, "short-3"); if (!safe_empty($text)) { $timetext .= " $text"; } return generate_tooltip_link('', $timetext, format_timestamp($timestamp), NULL); } /** * Generate mouseover links with static tooltip from URL, link text, contents and a class. * * Tooltips with static position and linked to current object. * Note, mostly same as overlib_link(), except tooltip position. * Always display tooltip if content not empty * * @param string $url URL string * @param string $text Text displayed as link * @param string $contents Text content displayed in mouseover tooltip (only for non-mobile devices) * @param string $class Css class name used for link * @param array $attribs Url/link extended attributes (ie data-*, class, style) * @param boolean $escape Escape or not link text * * @return string */ // TESTME needs unit testing function generate_tooltip_link($url, $text, $contents = '', $class = NULL, $attribs = [], $escape = FALSE) { global $config, $link_iter; $link_iter++; $href = !safe_empty($url) ? 'href="' . $url . '"' : ''; if ($escape) { $text = escape_html($text); } $attribs['class'] = array_merge((array)$class, (array)$attribs['class']); // Allow the Grinch to disable popups and destroy Christmas. $allow_mobile = !in_array(detect_browser()['type'], [ 'mobile', 'tablet' ]) || $config['web_mouseover_mobile']; if ($config['web_mouseover'] && $allow_mobile && !safe_empty($contents)) { $attribs['style'] = 'cursor: pointer;'; $attribs['data-rel'] = 'tooltip'; $attribs['data-tooltip'] = $contents; //$output = '<a '.$href.' class="'.$class.'" style="cursor: pointer;" data-rel="tooltip" data-tooltip="'.escape_html($contents).'">'.$text.'</a>'; } return '<a ' . $href . ' ' . generate_html_attribs($attribs) . '>' . $text . '</a>'; } /** * Generate mouseover links from URL, link text, contents and a class. * * Tooltips followed by mouse cursor. * Note, by default text NOT escaped for compatability with many old magic code usage. * * @param string $url URL string * @param string $text Text displayed as link * @param string $contents Text content displayed in mouseover tooltip (only for non-mobile devices) * @param string $class Css class name used for link * @param array $attribs Url/link extended attributes (ie data-*, class, style) * @param boolean $escape Escape or not link text */ // TESTME needs unit testing function generate_mouseover_link($url, $text, $contents, $class = NULL, $attribs = [], $escape = FALSE) { global $config, $link_iter; $link_iter++; $href = (strlen($url) ? 'href="' . $url . '"' : ''); if ($escape) { $text = escape_html($text); } if ($class) { $attribs['class'] = array_merge((array)$class, (array)$attribs['class']); } // Allow the Grinch to disable popups and destroy Christmas. $allow_mobile = (in_array(detect_browser()['type'], [ 'mobile', 'tablet' ]) ? $config['web_mouseover_mobile'] : TRUE); if ($config['web_mouseover'] && strlen($contents) && $allow_mobile) { $attribs['style'] = 'cursor: pointer;'; $attribs['class'] = array_merge(['tooltip-from-data'], (array)$attribs['class']); $attribs['data-tooltip'] = $contents; //$output = '<a '.$href.' class="tooltip-from-data '.$class.'" style="cursor: pointer;" data-tooltip="'.escape_html($contents).'">'.$text.'</a>'; } return '<a ' . $href . ' ' . generate_html_attribs($attribs) . '>' . $text . '</a>'; } function overlib_link($url, $text, $contents, $class = NULL, $attribs = [], $escape = FALSE) { return generate_mouseover_link($url, $text, $contents, $class, $attribs, $escape); } /** * DERPECATED * Generate menu links with item counts from URL, link text, contents and a class. * * Tooltips with static position and linked to current object. * Note, mostly same as overlib_link(), except tooltip position. * Always display tooltip if content not empty * * @param string $url URL string * @param string $text Text displayed as link * @param string $count Counts displayed at right * @param string $class Css class name used for count (default is 'label') * @param boolean $escape Escape or not link text */ function generate_menu_link($url, $text, $count = NULL, $class = 'label', $escape = FALSE, $alert_count = NULL) { return generate_menu_link_ng([ 'url' => $url, 'count' => $count, 'alert_count' => $alert_count, 'count_class' => $class ], $text, $escape); } /** * Generate menu links with item counts from URL, link text, contents and a class. * * Replaces previous function with multiple arguments. Should be used for all navbar menus * * @param array $array Array of options * @param string $text * @param bool $escape */ // TESTME needs unit testing function generate_menu_link_ng($array, $text = NULL, $escape = TRUE) { $array = array_merge([ 'role' => 'menuitem', 'count' => NULL, 'event' => '' ], (array)$array); $attribs = []; if (isset($array['role'])) { $attribs['role'] = $array['role']; } if (isset($array['tooltip'])) { $attribs['alt'] = $array['tooltip']; } if (isset($array['alt'])) { $attribs['data-rel'] = 'tooltip'; $attribs['data-tooltip'] = $array['alt']; } if (isset($array['id'])) { $attribs['id'] = $array['id']; } if (isset($array['attribs']) && is_array($array['attribs'])) { $attribs = array_merge($attribs, $array['attribs']); unset($array['link_opts']); // do not use unescaped link_opts, convert to attribs! } $link_opts = generate_html_attribs($attribs); if (isset($array['link_opts'])) { $link_opts .= ' ' . $array['link_opts']; } if (empty($array['url']) || $array['url'] === '#' || isset($array['action'])) { $array['url'] = 'javascript:void(0)'; } $output = '<a href="' . $array['url'] . '" ' . $link_opts . '>'; $output .= '<span>'; if (isset($array['icon'])) { $output .= get_icon($array['icon']) . ' '; } if (isset($entry['image'])) { // Detect an allowed screen ratio for current browser, cached! $ua_info = detect_browser(); if (isset($entry['image_2x']) && $ua_info['screen_ratio'] > 1) { // Add hidpi image set $srcset = ' srcset="' . $entry['image_2x'] . ' 2x"'; } else { $srcset = ''; } $output .= '<img src="' . $entry['image'] . '"' . $srcset . ' alt="" />' . ' '; } if (safe_empty($text)) { if (isset($array['title'])) { $text = $array['title']; } elseif (isset($array['text'])) { $text = $array['text']; } } if ($array['escape'] ?? $escape) { $text = escape_html($text); } $output .= $text . '</span>'; // Counter label(s) in navbar menu $extra = ''; if (isset($array['count_array']) && safe_count($array['count_array'])) { // Multiple counts as a group $count_items = []; // Ok/Up if ($array['count_array']['ok']) { $count_items[] = [ 'event' => 'success', 'text' => $array['count_array']['ok'] ]; } elseif ($array['count_array']['up']) { $count_items[] = [ 'event' => 'success', 'text' => $array['count_array']['up'] ]; } // Warning if ($array['count_array']['warning']) { $count_items[] = [ 'event' => 'warning', 'text' => $array['count_array']['warning'] ]; } // Alert/Down if ($array['count_array']['alert']) { $count_items[] = [ 'event' => 'danger', 'text' => $array['count_array']['alert'] ]; } elseif ($array['count_array']['down']) { $count_items[] = [ 'event' => 'danger', 'text' => $array['count_array']['down'] ]; } // Ignored if ($array['count_array']['ignored']) { $count_items[] = [ 'event' => 'default', 'text' => $array['count_array']['ignored'] ]; } // Disabled if ($array['count_array']['disabled']) { $count_items[] = [ 'event' => 'inverse', 'text' => $array['count_array']['disabled'] ]; } // Fallback to just count if (empty($count_items) && !safe_empty($array['count_array']['count'])) { $count_items[] = [ 'event' => 'default', 'text' => $array['count_array']['count'] ]; } //r(get_label_group($count_items)); $extra .= get_label_group($count_items); } else { // single counts if (isset($array['alert_count']) && is_numeric($array['alert_count'])) { //$extra .= ' <span class="label label-danger">' . $array['alert_count'] . '</span> '; $extra .= ' ' . get_label_span($array['alert_count'], 'danger') . ' '; } if (isset($array['count']) && is_numeric($array['count'])) { //$extra .= ' <span class="' . $array['count_class'] . '">' . $array['count'] . '</span>'; $extra .= ' ' . get_label_span($array['count'], $array['events']); } } if (isset($array['extra'])) { // used in refresh menu $extra .= ($array['escape'] ?? $escape) ? escape_html($array['extra']) : $array['extra']; } $output .= $extra . '</a>'; return $output; } // Generate a typical 4-graph popup using $graph_array // TESTME needs unit testing // DOCME needs phpdoc block function generate_graph_popup($graph_array) { global $config; // Todo - this should have entity headers where appropriate, too. // Take $graph_array and print day,week,month,year graps in overlib, hovered over graph $original_from = $graph_array['from']; $graph = generate_graph_tag($graph_array); /* $box_args = array('body-style' => 'width: 850px;'); if (strlen($graph_array['popup_title'])) { $box_args['title'] = $graph_array['popup_title']; } $content = generate_box_open($box_args); */ unset($graph_array['style']); $content = '<div class=entity-title><h4>' . $graph_array['popup_title'] . '</h4></div>'; $content .= '<div style="width: 850px">'; $graph_array['legend'] = "yes"; $graph_array['height'] = "100"; $graph_array['width'] = "340"; $graph_array['from'] = get_time('day'); $content .= generate_graph_tag($graph_array); $graph_array['from'] = get_time('week'); $content .= generate_graph_tag($graph_array); $graph_array['from'] = get_time('month'); $content .= generate_graph_tag($graph_array); $graph_array['from'] = get_time('year'); $content .= generate_graph_tag($graph_array); $content .= "</div>"; //$content .= generate_box_close(); $graph_array['from'] = $original_from; $graph_array['link'] = generate_url($graph_array, ['page' => 'graphs', 'height' => NULL, 'width' => NULL, 'bg' => NULL]); return overlib_link($graph_array['link'], $graph, $content, NULL); } // output the popup generated in generate_graph_popup(); // TESTME needs unit testing // DOCME needs phpdoc block function print_graph_popup($graph_array) { echo(generate_graph_popup($graph_array)); } // TESTME needs unit testing // DOCME needs phpdoc block function permissions_cache($user_id) { $cache_key = 'permissions_' . $GLOBALS['config']['auth_mechanism'] . $user_id; $cache_item = get_cache_item($cache_key); if (ishit_cache_item($cache_item)) { return get_cache_data($cache_item); } $permissions = []; // Get permissions from user-specific and role tables. $permission_where = '`user_id` = ? AND `auth_mechanism` = ?'; $permission_params = [ $user_id, $GLOBALS['config']['auth_mechanism'] ]; $entity_permissions = dbFetchRows("SELECT * FROM `entity_permissions` WHERE " . $permission_where, $permission_params); $roles_entity_permissions = dbFetchRows("SELECT * FROM `roles_entity_permissions` LEFT JOIN `roles_users` USING (`role_id`) WHERE " . $permission_where, $permission_params); foreach (array_merge((array)$entity_permissions, (array)$roles_entity_permissions) as $entity) { // Set access to ro if it's not in the defined list. $access = (in_array($entity['access'], [ 'ro', 'rw' ]) ? $entity['access'] : 'ro'); if ($entity['entity_type'] === 'group') { // this is a group, so expand its members into an array $group = get_group_by_id($entity['entity_id']); foreach (get_group_entities($entity['entity_id']) as $group_entity_id) { $permissions[$group['entity_type']][(int)$group_entity_id] = $access; } } $permissions[$entity['entity_type']][(int)$entity['entity_id']] = $access; } // Cache platform permissions foreach (dbFetchRows("SELECT * FROM `roles_permissions` LEFT JOIN `roles_users` USING (`role_id`) WHERE " . $permission_where, $permission_params) as $perm) { $permissions['permission'][$perm['permission']] = TRUE; } // Alerts // FIXME - this seems like it would be slow on very large installs $alert = []; foreach (dbFetchRows('SELECT `alert_table_id`, `device_id`, `entity_id`, `entity_type` FROM `alert_table`') as $alert_table_entry) { //r($alert_table_entry); if (is_entity_permitted($alert_table_entry['entity_id'], $alert_table_entry['entity_type'], $alert_table_entry['device_id'], $permissions)) { $alert[$alert_table_entry['alert_table_id']] = TRUE; } } if (!safe_empty($alert)) { $permissions['alert'] = $alert; } set_cache_item($cache_item, $permissions); // Clear expired cache del_cache_expired(); return $permissions; } /** * Return WEB client remote IP address. * In mostly cases (also by default) this is just $_SERVER['REMOTE_ADDR'], * but if config options ($config['web_remote_addr_header']) set, this can use specified HTTP headers * * @param bool $use_http_header Use or not HTTP header specified in $config['web_remote_addr_header'] * * @return string IP address of remote client */ function get_remote_addr($use_http_header = FALSE) { global $config; if ($use_http_header) { // Note, this headers is very dangerous for use as auth! $addr_headers = ['HTTP_CF_CONNECTING_IP', // CF-Connecting-IP (CloudFlare network) 'HTTP_X_REAL_IP', // X-Real-IP 'HTTP_X_FORWARDED_FOR', // X-Forwarded-For 'HTTP_CLIENT_IP']; // Client-IP if (!in_array($config['web_remote_addr_header'], ['default', 'detect', 'auto'])) { $remote_addr_header = 'HTTP_' . str_replace('-', '_', strtoupper($config['web_remote_addr_header'])); if (in_array($remote_addr_header, $addr_headers)) { // Use only exact single header $addr_headers = [$remote_addr_header]; } else { // Unknown config value passed, do not check any header $addr_headers = []; } } foreach ($addr_headers as $header) { if (!empty($_SERVER[$header]) && preg_match(OBS_PATTERN_IP_FULL, $_SERVER[$header], $matches)) { // HTTP header found and it contains valid IP address return $matches[1]; } } } // By default just use server remote address return $_SERVER['REMOTE_ADDR']; } // TESTME needs unit testing // DOCME needs phpdoc block function bill_permitted($bill_id) { global $permissions; if ($_SESSION['userlevel'] >= "5") { $allowed = TRUE; } elseif ($permissions['bill'][$bill_id]) { $allowed = TRUE; } else { $allowed = FALSE; } return $allowed; } function entity_permitted_array(&$entities, $entity_type) { $entity_type_data = entity_type_translate_array($entity_type); // Strip out the entities the user isn't allowed to see, if they don't have global view rights if (!isset($_SESSION['user_limited']) || $_SESSION['user_limited']) { foreach ($entities as $key => $entity) { if (!is_entity_permitted($entity[$entity_type_data['id_field']], $entity_type, $entity['device_id'])) { unset($entities[$key]); } } } } // TESTME needs unit testing // DOCME needs phpdoc block function application_permitted($app_id, $device_id = NULL) { global $permissions; if (is_numeric($app_id)) { if (!$device_id) { $device_id = get_device_id_by_app_id($app_id); } if ($_SESSION['userlevel'] >= "5") { $allowed = TRUE; } elseif (device_permitted($device_id)) { $allowed = TRUE; } elseif ($permissions['application'][$app_id]) { $allowed = TRUE; } else { $allowed = FALSE; } } else { $allowed = FALSE; } return $allowed; } // TESTME needs unit testing // DOCME needs phpdoc block function print_graph_tag($args) { echo(generate_graph_tag($args)); } // TESTME needs unit testing // DOCME needs phpdoc block function generate_graph_tag($args, $return_array = FALSE) { if (empty($args)) { // Quickly return if passed an empty array return ''; } $i = []; $style = 'max-width: 100%; width: auto; vertical-align: top;'; if (isset($args['style'])) { if (is_array($args['style'])) { $style .= implode("; ", $args['style']) . ';'; } else { $style .= $args['style'] . ';'; } unset($args['style']); } $i['img_id'] = $args['img_id'] ?? random_string(8); // Detect an allowed screen ratio for the current browser $ua_info = detect_browser(); $zoom = $ua_info['screen_ratio']; if ($zoom > 1) { // Add img srcset for HiDPI screens $args_x = $args; $args_x['zoom'] = $zoom; $srcset = ' srcset="' . generate_graph_url($args_x) . ' ' . $args_x['zoom'] . 'x"'; $i['srcset'] = $srcset; } else { $srcset = ''; } $attribs = generate_html_attribs(array_filter_key($args, [ 'class', 'loading', 'fetchpriority' ])); $img_url = generate_graph_url($args); $i['img_url'] = $img_url; $i['img_tag'] = '<img id="' . $i['img_id'] . '" ' . $attribs . ' src="' . $img_url . '"' . $srcset . ' style="' . $style . '" alt="" />'; //$i['img_tag'] = '<img id="' . $i['img_id'] . '" src="' . $img_url . '"' . $srcset . $attribs.' style="' . $style . '" alt="" loading="lazy" />'; if ($return_array) { return $i; } return $i['img_tag']; } function generate_graph_url($args, $escape = TRUE) { // Encode arrays in fields before we build the URL foreach ($args as $key => $arg) { if (is_array($arg)) { // Encode arrays $args[$key] = var_encode($arg); } } $url = 'graph.php?' . http_build_query($args); if (is_cli()) { if ($GLOBALS['config']['web_url'] == 'http://localhost:80/') { // override default web_url by http://localhost/ $url = 'http://' . get_localhost() . '/' . $url; } else { $url = $GLOBALS['config']['web_url'] . $url; } } return $url; } // TESTME needs unit testing // DOCME needs phpdoc block function generate_graph_js_state($args) { // we are going to assume we know roughly what the graph url looks like here. // TODO: Add sensible defaults $from = (is_numeric($args['from']) ? $args['from'] : 0); $to = (is_numeric($args['to']) ? $args['to'] : 0); $width = (is_numeric($args['width']) ? $args['width'] : 0); $height = (is_numeric($args['height']) ? $args['height'] : 0); $legend = str_replace("'", "", $args['legend']); $state = <<<STATE <script type="text/javascript"> document.graphFrom = $from; document.graphTo = $to; document.graphWidth = $width; document.graphHeight = $height; document.graphLegend = '$legend'; </script> STATE; return $state; } /** * Generate Percentage Bar * * This function generates an Observium percentage bar from a supplied array of arguments. * It is possible to draw a bar that does not work at all, * So care should be taken to make sure values are valid. * * @param array $args * * @return string */ // TESTME needs unit testing function percentage_bar($args) { if (strlen($args['bg'])) { $style .= 'background-color:' . $args['bg'] . ';'; } if (strlen($args['border'])) { $style .= 'border-color:' . $args['border'] . ';'; } if (strlen($args['width'])) { $style .= 'width:' . $args['width'] . ';'; } if (strlen($args['text_c'])) { $style_b .= 'color:' . $args['text_c'] . ';'; } $total = '0'; $output = '<div class="percbar" style="' . $style . '">'; foreach ($args['bars'] as $bar) { $output .= '<div class="bar" style="width:' . $bar['percent'] . '%; background-color:' . $bar['colour'] . ';"></div>'; $total += $bar['percent']; } $left = '100' - $total; if ($left > 0) { $output .= '<div class="bar" style="width:' . $left . '%;"></div>'; } if ($left >= 0) { $output .= '<div class="bar-text" style="margin-left: -100px; margin-top: 0px; float: right; text-align: right; ' . $style_b . '">' . $args['text'] . '</div>'; } foreach ($args['bars'] as $bar) { $output .= '<div class="bar-text" style="width:' . $bar['percent'] . '%; max-width:' . $bar['percent'] . '%; padding-left: 4px;">' . $bar['text'] . '</div>'; } # if ($left > '0') { $output .= '<div class="bar-text" style="margin-left: -100px; margin-top: -16px; float: right; text-align: right; '.$style_b.'">'.$args['text'].'</div>'; } $output .= '</div>'; return $output; } // Legacy function // DO NOT USE THIS. Please replace instances of it with percentage_bar from above. // TESTME needs unit testing // DOCME needs phpdoc block function print_percentage_bar($width, $height, $percent, $left_text, $left_colour, $left_background, $right_text, $right_colour, $right_background) { if ($percent > "100") { $size_percent = "100"; } else { $size_percent = $percent; } $percentage_bar['border'] = "#" . $left_background; $percentage_bar['bg'] = "#" . $right_background; $percentage_bar['width'] = $width; $percentage_bar['text'] = $right_text; $percentage_bar['bars'][0] = ['percent' => $size_percent, 'colour' => '#' . $left_background, 'text' => $left_text]; $output = percentage_bar($percentage_bar); return $output; } /** * Return cached locations list * * If filter used, return locations available only for specified params. * Without filter return all available locations (cached) * * @param array $filter * * @return array */ // TESTME needs unit testing function get_locations($filter = []) { $where_array = []; foreach ($filter as $var => $value) { switch ($var) { case 'location_lat': case 'location_lon': case 'location_country': case 'location_state': case 'location_county': case 'location_city': // Check geo params only when GEO enabled globally if ($GLOBALS['config']['geocoding']['enable']) { $where_array[$var] = generate_query_values($value, $var); } break; case 'location': $where_array[$var] = generate_query_values($value, $var); break; } } if (!safe_empty($where_array)) { // Return only founded locations $locations = dbFetchColumn("SELECT DISTINCT `location` FROM `devices_locations`" . generate_where_clause($GLOBALS['cache']['where']['devices_permitted'], $where_array)); } else { $locations = empty($GLOBALS['cache']['device_locations']) ? [] : array_keys($GLOBALS['cache']['device_locations']); } sort($locations); return $locations; } // Note, by default text NOT escaped. // TESTME needs unit testing // DOCME needs phpdoc block function generate_ap_link($args, $text = NULL, $type = NULL, $escape = FALSE) { humanize_port($args); if (!$text) { $text = escape_html($args['port_label']); } if ($type) { $args['graph_type'] = $type; } if (!isset($args['graph_type'])) { $args['graph_type'] = 'port_bits'; } if (!isset($args['hostname']) && $args['device_id']) { $args = array_merge($args, device_by_id_cache($args['device_id'])); } $content = "<div class=entity-title>" . $args['text'] . " - " . escape_html($args['port_label']) . "</div>"; if ($args['ifAlias']) { $content .= escape_html($args['ifAlias']) . "<br />"; } $content .= "<div style=\'width: 850px\'>"; $graph_array['type'] = $args['graph_type']; $graph_array['legend'] = "yes"; $graph_array['height'] = "100"; $graph_array['width'] = "340"; $graph_array['to'] = get_time(); $graph_array['from'] = get_time('day'); $graph_array['id'] = $args['accesspoint_id']; $content .= generate_graph_tag($graph_array); $graph_array['from'] = get_time('week'); $content .= generate_graph_tag($graph_array); $graph_array['from'] = get_time('month'); $content .= generate_graph_tag($graph_array); $graph_array['from'] = get_time('year'); $content .= generate_graph_tag($graph_array); $content .= "</div>"; $url = generate_ap_url($args); if (port_permitted($args['interface_id'], $args['device_id'])) { return overlib_link($url, $text, $content, $class, $escape); } return $text; } // TESTME needs unit testing // DOCME needs phpdoc block function generate_ap_url($ap, $vars = []) { return generate_url(['page' => 'device', 'device' => $ap['device_id'], 'tab' => 'accesspoint', 'ap' => $ap['accesspoint_id']], $vars); } /** * Generate SQL WHERE string with check permissions and ignores for device_id, port_id and other * * Note, this function uses comparison operator IN. Max number of values in the IN list * is limited by the 'max_allowed_packet' option (default: 1048576) * * Usage examples: * generate_query_permitted() * ' AND `device_id` IN (1,4,8,33) AND `device_id` NOT IN (66) AND (`device_id` != '' AND `device_id` IS NOT NULL) ' * generate_query_permitted(array('device'), array('device_table' => 'D')) * ' AND `D`.`device_id` IN (1,4,8,33) AND `D`.`device_id` NOT IN (66) AND (`D`.`device_id` != '' AND `D`.`device_id` IS NOT NULL) ' * generate_query_permitted(array('device', 'port'), array('port_table' => 'I')) == * ' AND `device_id` IN (1,4,8,33) AND `device_id` NOT IN (66) AND (`device_id` != '' AND `device_id` IS NOT NULL) * AND `I`.`port_id` IN (1,4,8,33) AND `I`.`port_id` NOT IN (66) AND (`I`.`port_id` != '' AND `I`.`port_id` IS NOT NULL) ' * generate_query_permitted(array('device', 'port'), array('port_table' => 'I', 'hide_ignored' => TRUE)) * This additionaly exclude all ignored devices and ports * * @param array|string $type_array Array with permission types, currently allowed 'devices', 'ports' * @param array $options Options for each permission type: device_table, port_table, hide_ignored, hide_disabled * * @return string * @uses html/includes/cache-data.inc.php * @global integer $_SESSION ['userlevel'] * @global boolean $GLOBALS ['config']['web_show_disabled'] * @global array $GLOBALS ['permissions'] * @global array $GLOBALS ['cache']['devices'] * @global array $GLOBALS ['cache']['ports'] * @global string $GLOBALS ['vars']['page'] */ // TESTME needs unit testing function generate_query_permitted_ng($type_array = [ 'device' ], $options = []) { if (!is_array($type_array)) { $type_array = [ $type_array ]; } $user_limited = $_SESSION['userlevel'] < 5; $page = $GLOBALS['vars']['page']; // If device IDs stored in SESSION use it (used in ajax) //if (!isset($GLOBALS['cache']['devices']) && isset($_SESSION['cache']['devices'])) //{ // $GLOBALS['cache']['devices'] = $_SESSION['cache']['devices']; //} if (!isset($GLOBALS['permissions'])) { if (is_graph() || is_api()) { // Do not broke graph/api output print_debug("Function " . __FUNCTION__ . "() on page '$page' called before include cache-data.inc.php or something wrong with caching permissions."); } else { // Note, this function must used after load permissions list! print_error("Function " . __FUNCTION__ . "() on page '$page' called before include cache-data.inc.php or something wrong with caching permissions."); } } // Use option hide_disabled if passed or use config $options['hide_disabled'] = $options['hide_disabled'] ?? !$GLOBALS['config']['web_show_disabled']; //$query_permitted = ''; $query_part = []; foreach ($type_array as $type) { switch ($type) { // Devices permission query case 'device': case 'devices': $column = '`device_id`'; $query_permitted = []; if (isset($options['device_table'])) { $column = '`' . $options['device_table'] . '`.' . $column; } // Show only permitted devices if ($user_limited) { if (!safe_empty($GLOBALS['permissions']['device'])) { $query_permitted[] = generate_query_values(array_keys($GLOBALS['permissions']['device']), $column); } else { // Exclude all entries, because there are no permitted devices $query_permitted[] = ' 0'; } } // Also don't show ignored and disabled devices (except on 'device' and 'devices' pages) $devices_excluded = []; if (!str_starts_with($page, 'device')) { if ($options['hide_ignored'] && !safe_empty($GLOBALS['cache']['devices']['ignored'])) { $devices_excluded = array_merge($devices_excluded, $GLOBALS['cache']['devices']['ignored']); } if ($options['hide_disabled'] && !safe_empty($GLOBALS['cache']['devices']['disabled'])) { $devices_excluded = array_merge($devices_excluded, $GLOBALS['cache']['devices']['disabled']); } } if (!safe_empty($devices_excluded)) { //sort($devices_excluded, SORT_NUMERIC); //r($devices_excluded); // Set query with excluded devices $query_permitted[] = generate_query_values($devices_excluded, $column, '!='); } // At the end excluded entries with empty/null device_id (wrong entries) //$query_permitted[] = " ($column != '' AND $column IS NOT NULL)"; $query_permitted[] = " $column IS NOT NULL"; // Note: SELECT '' = 0; is TRUE $query_part[] = implode(" AND ", $query_permitted); unset($query_permitted); break; // Ports permission query case 'port': case 'ports': $table = isset($options['port_table']) ? '`' . $options['port_table'] . '`.' : ''; if (isset($options['entity'])) { $query_permitted[] = generate_query_values('port', $table.'entity_type'); $column = $table.'`entity_id`'; } else { $column = $table.'`port_id`'; } // If port IDs stored in SESSION use it (used in ajax) //if (!isset($GLOBALS['cache']['ports']) && isset($_SESSION['cache']['ports'])) //{ // $GLOBALS['cache']['ports'] = $_SESSION['cache']['ports']; //} // Show only permitted ports if ($user_limited) { if (!safe_empty($GLOBALS['permissions']['port'])) { $query_permitted[] = generate_query_values(array_keys($GLOBALS['permissions']['port']), $column); // $query_permitted[] = " $column IN (" . // implode(',', array_keys($GLOBALS['permissions']['port'])) . // ')'; } else { // Exclude all entries, because there is no permitted ports $query_permitted[] = '0'; } } $ports_excluded = []; // Don't show ports with disabled polling. if (!safe_empty($GLOBALS['cache']['ports']['poll_disabled'])) { $ports_excluded = array_merge($ports_excluded, $GLOBALS['cache']['ports']['poll_disabled']); //foreach ($GLOBALS['cache']['ports']['poll_disabled'] as $entry) //{ // $ports_excluded[] = $entry; //} //$ports_excluded = array_unique($ports_excluded); } // Don't show deleted ports (except on 'deleted-ports' page) if ($page !== 'deleted-ports' && !safe_empty($GLOBALS['cache']['ports']['deleted'])) { $ports_excluded = array_merge($ports_excluded, $GLOBALS['cache']['ports']['deleted']); //foreach ($GLOBALS['cache']['ports']['deleted'] as $entry) //{ // $ports_excluded[] = $entry; //} //$ports_excluded = array_unique($ports_excluded); } if ($page !== 'device' && !in_array('device', $type_array)) { // Don't show ports for disabled devices (except on 'device' page or if 'device' permissions already queried) if ($options['hide_disabled'] && !$user_limited && !safe_empty($GLOBALS['cache']['ports']['device_disabled'])) { $ports_excluded = array_merge($ports_excluded, $GLOBALS['cache']['ports']['device_disabled']); //foreach ($GLOBALS['cache']['ports']['device_disabled'] as $entry) //{ // $ports_excluded[] = $entry; //} //$ports_excluded = array_unique($ports_excluded); } // Don't show ports for ignored devices (except on 'device' page) if ($options['hide_ignored'] && !safe_empty($GLOBALS['cache']['ports']['device_ignored'])) { $ports_excluded = array_merge($ports_excluded, $GLOBALS['cache']['ports']['device_ignored']); //foreach ($GLOBALS['cache']['ports']['device_ignored'] as $entry) //{ // $ports_excluded[] = $entry; //} //$ports_excluded = array_unique($ports_excluded); } } // Don't show ignored ports (only on some pages!) if (($page === 'overview' || $options['hide_ignored']) && !safe_empty($GLOBALS['cache']['ports']['ignored'])) { $ports_excluded = array_merge($ports_excluded, $GLOBALS['cache']['ports']['ignored']); //foreach ($GLOBALS['cache']['ports']['ignored'] as $entry) //{ // $ports_excluded[] = $entry; //} //$ports_excluded = array_unique($ports_excluded); } unset($entry); if (!safe_empty($ports_excluded)) { // Set query with excluded ports $query_permitted[] = generate_query_values($ports_excluded, $column, '!='); } // At the end excluded entries with empty/null port_id (wrong entries) if (!isset($options['port_null']) || !$options['port_null']) { //$query_permitted[] = "($column != '' AND $column IS NOT NULL)"; $query_permitted[] = "$column IS NOT NULL"; } elseif (!$user_limited && safe_count($query_permitted)) { // FIXME. derp code, need rewrite //$query_permitted[] = safe_count($query_permitted) ? "OR $column IS NULL" : "$column IS NULL"; $query_permitted[] = "OR $column IS NULL"; } $query_permitted = implode(" AND ", (array)$query_permitted); if (!safe_empty($query_permitted)) { $query_part[] = str_replace(" AND OR ", ' OR ', $query_permitted); } unset($query_permitted); break; case 'sensor': case 'sensors': // For sensors // FIXME -- this is easily generifyable, just use translate_table_array() $table = isset($options['sensor_table']) ? '`' . $options['sensor_table'] . '`.' : ''; if (isset($options['entity'])) { $query_permitted[] = generate_query_values('sensor', $table.'entity_type'); $column = $table.'`entity_id`'; } else { $column = $table.'`sensor_id`'; } // If IDs stored in SESSION use it (used in ajax) //if (!isset($GLOBALS['cache']['sensors']) && isset($_SESSION['cache']['sensors'])) //{ // $GLOBALS['cache']['sensors'] = $_SESSION['cache']['sensors']; //} // Show only permitted entities if ($user_limited) { if (!safe_empty($GLOBALS['permissions']['sensor'])) { $query_permitted[] = generate_query_values(array_keys((array)$GLOBALS['permissions']['sensor']), $column); } else { // Exclude all entries, because there are no permitted entities $query_permitted[] = '0'; } $query_permitted = implode(" AND ", (array)$query_permitted); if (!safe_empty($query_permitted)) { $query_part[] = str_replace(" AND OR ", ' OR ', $query_permitted); } unset($query_permitted); } break; case 'alert': case 'alerts': // For generic alert $column = '`alert_table_id`'; // Show only permitted entities if ($user_limited) { if (!safe_empty($GLOBALS['permissions']['alert'])) { $query_permitted = generate_query_values(array_keys((array)$GLOBALS['permissions']['alert']), $column); } else { // Exclude all entries, because there are no permitted entities $query_permitted = '0'; } $query_part[] = $query_permitted; unset($query_permitted); } break; case 'bill': case 'bills': // For bills break; } } if (!safe_empty($query_part)) { //r($query_part); if ($user_limited) { // Limited user must use OR for include multiple entities $query_permitted = "((" . implode(") OR (", $query_part) . "))"; } else { // Unlimited used must use AND for exclude multiple hidden entities $query_permitted = "((" . implode(") AND (", $query_part) . "))"; } // Append leading AND if option requested if ($options['leading_and']) { $query_permitted = ' AND ' . $query_permitted; } } //r($query_permitted); return !safe_empty($query_permitted) ? $query_permitted . ' ' : ''; } /** * Compat function for old usages, call to generate_query_permitted_ng() * * @param $type_array * @param $options * * @return string */ function generate_query_permitted($type_array = ['device'], $options = []) { $options['leading_and'] = TRUE; return generate_query_permitted_ng($type_array, $options); } // TESTME needs unit testing // DOCME needs phpdoc block function dashboard_exists($dash_id) { return dbExist('dashboards', '`dash_id` = ?', [$dash_id]); //return count(dbFetchRow("SELECT * FROM `dashboards` WHERE `dash_id` = ?", array($dash_id))); } // TESTME needs unit testing // DOCME needs phpdoc block function get_user_prefs($user_id) { $prefs = []; foreach (dbFetchRows("SELECT * FROM `users_prefs` WHERE `user_id` = ?", [$user_id]) as $entry) { $prefs[$entry['pref']] = $entry; } return $prefs; } // TESTME needs unit testing // DOCME needs phpdoc block function get_user_pref($user_id, $pref) { if ($entry = dbFetchRow("SELECT `value` FROM `users_prefs` WHERE `user_id` = ? AND `pref` = ?", [$user_id, $pref])) { return $entry['value']; } return NULL; } // TESTME needs unit testing // DOCME needs phpdoc block function set_user_pref($user_id, $pref, $value) { //if (dbFetchCell("SELECT COUNT(*) FROM `users_prefs` WHERE `user_id` = ? AND `pref` = ?", array($user_id, $pref))) if (dbExist('users_prefs', '`user_id` = ? AND `pref` = ?', [$user_id, $pref])) { $id = dbUpdate(['value' => $value], 'users_prefs', '`user_id` = ? AND `pref` = ?', [$user_id, $pref]); } else { $id = dbInsert(['user_id' => $user_id, 'pref' => $pref, 'value' => $value], 'users_prefs'); } return $id; } // TESTME needs unit testing // DOCME needs phpdoc block function del_user_pref($user_id, $pref) { return dbDelete('users_prefs', "`user_id` = ? AND `pref` = ?", [$user_id, $pref]); } /** * Load user-specific configuration settings and merge them with global configuration. * * This function fetches user-specific preferences from the database and merges * them with the global configuration. Only settings allowed by the configuration * definitions are considered. The resulting configuration is stored in the * $load_config parameter. * * @param array $load_config A reference to the array where the merged configuration should be stored. * @param int $user_id The ID of the user whose preferences should be loaded. * * @return void */ function load_user_config(&$load_config, $user_id) { global $config; if (!$prefs = dbFetchRows("SELECT * FROM `users_prefs` WHERE `user_id` = ? AND `pref` NOT IN (?, ?)", [$user_id, 'atom_key', 'api_key'])) { // No user prefs set return FALSE; } // Always use global config here! include($config['install_dir'] . '/includes/config-variables.inc.php'); foreach ($prefs as $item) { if (!isset($config_variable[$item['pref']]['useredit']) || !$config_variable[$item['pref']]['useredit']) { // Load only permitted settings print_debug("User [$user_id] setting '{$item['pref']}' not permitted by definitions."); continue; } // Convert boo|bee|baa config value into $config['boo']['bee']['baa'] $tree = explode('|', $item['pref']); set_nested_value($load_config, $tree, safe_unserialize($item['value'])); } } /** * Set a value in a nested array, given a list of keys. * * This function sets a value in a nested array, creating intermediate arrays as * necessary. The keys are specified as an array, where each element represents a * level in the nested array. The value is set at the position specified by the * last key. * * @param array $array A reference to the array in which the value should be set. * @param array $keys An array of keys specifying the position in the nested array. * @param mixed $value The value to set in the nested array. * * @return void */ function set_nested_value(&$array, $keys, $value) { $last_key = array_pop($keys); foreach ($keys as $key) { if (!isset($array[$key]) || !is_array($array[$key])) { $array[$key] = []; } $array = &$array[$key]; } $array[$last_key] = $value; } function process_sql_vars($vars) { global $config; // Always use global config here! include($config['install_dir'] . '/includes/config-variables.inc.php'); $deletes = []; $sets = []; $errors = []; $set_attribs = []; // set obs_attribs // Submit button pressed foreach ($vars as $varname => $value) { if (str_starts_with($varname, 'varset_')) { $varname = substr($varname, 7); $sqlname = str_replace('__', '|', $varname); $sqlset = get_var_true($value); // value sets in sql $content = $vars[$varname]; $confname = '$config[\'' . implode("']['", explode('|', $sqlname)) . '\']'; $section = $config_variable[$sqlname]['section']; if ($vars[$varname . '_custom']) { $ok = FALSE; if (isset($config_variable[$sqlname]['edition']) && $config_variable[$sqlname]['edition'] !== OBSERVIUM_EDITION) { // Skip variables not allowed for current Observium edition continue; } if (isset($config_sections[$section]['edition']) && $config_sections[$section]['edition'] !== OBSERVIUM_EDITION) { // Skip sections not allowed for current Observium edition continue; } // Split enum|foo|bar into enum foo|bar [$vartype, $varparams] = explode('|', $config_variable[$sqlname]['type'], 2); $params = []; // If a callback function is defined, use this to fill params. if ($config_variable[$sqlname]['params_call'] && function_exists($config_variable[$sqlname]['params_call'])) { $params = call_user_func($config_variable[$sqlname]['params_call']); // Else if the params are defined directly, use these. } elseif (is_array($config_variable[$sqlname]['params'])) { $params = $config_variable[$sqlname]['params']; } elseif (!empty($varparams)) { // Else use parameters specified in variable type (e.g. enum|1|2|5|10) foreach (explode('|', $varparams) as $param) { $params[$param] = [ 'name' => nicecase($param) ]; } } switch ($vartype) { case 'int': case 'integer': case 'float': if (is_numeric($content)) { $ok = TRUE; } else { $errors[] = $config_variable[$sqlname]['name'] . " ($confname) should be of <strong>numeric</strong> type. Setting '" . escape_html($content) . "' ignored."; } break; case 'bool': case 'boolean': switch ($content) { case 'on': case '1': $content = 1; $ok = TRUE; break; case 'off': // Won't actually happen. When "unchecked" the field is simply not transmitted... case '0': case '': // ... which we catch here. $content = 0; $ok = TRUE; break; default: $ok = FALSE; $errors[] = $config_variable[$sqlname]['name'] . " ($confname) should be of type <strong>bool</strong>. Setting '" . escape_html($content) . "' ignored."; } break; case 'enum': if (!array_key_exists($content, $params)) { $ok = FALSE; $errors[] = $config_variable[$sqlname]['name'] . " ($confname) should be one of <strong>" . implode(', ', $params) . "</strong>. Setting '" . escape_html($content) . "' ignored."; } else { $ok = TRUE; } break; case 'enum-array': //r($content); //r($params); foreach ($content as $value) { // Check all values if (!array_key_exists($value, $params)) { $ok = FALSE; $errors[] = $config_variable[$sqlname]['name'] . " ($confname) all values should be one of this list <strong>" . implode(', ', $params) . "</strong>. Settings '" . implode(', ', $content) . "' ignored."; break; } $ok = TRUE; } break; case 'enum-list': //r($content); //r($params); if (isset($content['value'])) { $content = array_filter(array_unique($content['value']), static function ($value) { return !safe_empty($value); }); $ok = !safe_empty($content); //r($content); } break; case 'enum-key-value': //r($content); //r($params); if (isset($content['key'], $content['value'])) { $tmp = $content; $content = []; foreach ($tmp['key'] as $i => $key) { if (safe_empty($key) && safe_empty($tmp['value'][$i])) { // skip an empty key-value pair continue; } $content[$key] = $tmp['value'][$i]; } $ok = !safe_empty($content); //r($content); } break; case 'enum-freeinput': //r($content); //r($params); // FIXME, need validate values if (is_null($content)) { // Empty array allowed, for override defaults $content = []; $ok = TRUE; } foreach ($content as $value) { $ok = TRUE; } break; case 'password': case 'string': $ok = TRUE; break; default: $ok = FALSE; $errors[] = $config_variable[$sqlname]['name'] . " ($confname) is of unknown type (" . $config_variable[$sqlname]['type'] . ")"; break; } if ($ok) { $sets[$sqlname] = $content; // Set an obs_attrib, example for syslog trigger //r($config_variable[$sqlname]); if (isset($config_variable[$sqlname]['set_attrib']) && !safe_empty($config_variable[$sqlname]['set_attrib'])) { $set_attribs[$config_variable[$sqlname]['set_attrib']] = get_time(); } } } elseif ($sqlset) { $deletes[] = $sqlname; // Set an obs_attrib, example for syslog trigger //r($config_variable[$sqlname]); if (isset($config_variable[$sqlname]['set_attrib']) && !safe_empty($config_variable[$sqlname]['set_attrib'])) { $set_attribs[$config_variable[$sqlname]['set_attrib']] = get_time(); } } } } return [ 'sets' => $sets, 'set_attribs' => $set_attribs, 'deletes' => $deletes, 'errors' => $errors ]; } /** * Convert amqp|conn|host into returning value of $arrayvar['amqp']['conn']['host'] * * @param string $sqlname Variable name * @param array $arrayvar Array where to see param * @param Boolean $try_isset If True, return isset($sqlname) check, else return variable content * * @return mixed */ function sql_to_array($sqlname, $arrayvar, $try_isset = TRUE) { [$key, $pop_sqlname] = explode('|', $sqlname, 2); if (!is_array($arrayvar)) { return FALSE; } $isset = array_key_exists($key, $arrayvar); if (safe_empty($pop_sqlname)) { // Reached the variable, return its content, or FALSE if it's not set if ($try_isset) { return $isset; } return $isset ? $arrayvar[$key] : NULL; } if ($isset) { // Recurse to lower level return sql_to_array($pop_sqlname, $arrayvar[$key], $try_isset); } return FALSE; } /** * Darkens or lightens a colour * Found via http://codepad.org/MTGLWVd0 * * First argument is the colour in hex, second argument is how dark it should be 1=same, 2=50% * * @param string $rgb * @param int $darker * * @return string */ function darken_color($rgb, $darker = 2) { if (strpos($rgb, '#') !== FALSE) { $hash = '#'; $rgb = str_replace('#', '', $rgb); } else { $hash = ''; } $len = strlen($rgb); if ($len == 6) { } // Passed RGB elseif ($len == 8) { // Passed RGBA, remove alpha channel $rgb = substr($rgb, 0, 6); } else { $rgb = FALSE; } if ($rgb === FALSE) { return $hash . '000000'; } $darker = ($darker > 1) ? $darker : 1; [$R16, $G16, $B16] = str_split($rgb, 2); $R = sprintf("%02X", floor(hexdec($R16) / $darker)); $G = sprintf("%02X", floor(hexdec($G16) / $darker)); $B = sprintf("%02X", floor(hexdec($B16) / $darker)); return $hash . $R . $G . $B; } function json_output($status, $message) { header("Content-type: application/json; charset=utf-8"); echo safe_json_encode(["status" => $status, "message" => $message]); exit(); } /** * Redirect to specified URL * * @param string $url Redirecting URL */ function redirect_to_url($url) { if (safe_empty($url) || $url === '#') { return; } // Empty url, do not redirect $parse = parse_url($url); //r($url); if (!isset($parse['scheme']) && !str_starts($url, '/')) { // When this is not full url or not started with / $url = '/' . $url; } if (headers_sent()) { // HTML headers already sent, use JS than register_html_resource('script', "location.href='$url'"); } else { // Just use headers header('Location: ' . $url); } } function generate_colour_gradient($start_colour, $end_colour, $steps) { if ($steps < 4) { $steps = 4; } $FromRGB['r'] = hexdec(substr($start_colour, 0, 2)); $FromRGB['g'] = hexdec(substr($start_colour, 2, 2)); $FromRGB['b'] = hexdec(substr($start_colour, 4, 2)); $ToRGB['r'] = hexdec(substr($end_colour, 0, 2)); $ToRGB['g'] = hexdec(substr($end_colour, 2, 2)); $ToRGB['b'] = hexdec(substr($end_colour, 4, 2)); $StepRGB['r'] = ($FromRGB['r'] - $ToRGB['r']) / ($steps - 1); $StepRGB['g'] = ($FromRGB['g'] - $ToRGB['g']) / ($steps - 1); $StepRGB['b'] = ($FromRGB['b'] - $ToRGB['b']) / ($steps - 1); $GradientColors = []; $GradientColors[] = $start_colour; // Hack because array starts at 0, but we count from 1. for ($i = 0; $i < $steps; $i++) { $RGB['r'] = floor($FromRGB['r'] - ($StepRGB['r'] * $i)); $RGB['g'] = floor($FromRGB['g'] - ($StepRGB['g'] * $i)); $RGB['b'] = floor($FromRGB['b'] - ($StepRGB['b'] * $i)); $HexRGB['r'] = sprintf('%02x', ($RGB['r'])); $HexRGB['g'] = sprintf('%02x', ($RGB['g'])); $HexRGB['b'] = sprintf('%02x', ($RGB['b'])); $GradientColors[] = implode(NULL, $HexRGB); } $GradientColors = array_filter($GradientColors, "c_len"); unset($GradientColors[0]); // Remove the placeholder array position 0 because it's not used. //r($GradientColors); return $GradientColors; } function c_len($val) { return strlen($val) === 6; } function adjust_colour_brightness($hex, $steps) { // Steps should be between -255 and 255. Negative = darker, positive = lighter $steps = max(-255, min(255, $steps)); // Normalize into a six character long hex string $hex = str_replace('#', '', $hex); if (strlen($hex) === 3) { $hex = str_repeat($hex[0], 2) . str_repeat($hex[1], 2) . str_repeat($hex[2], 2); } // Split into three parts: R, G and B $color_parts = str_split($hex, 2); $return = ''; foreach ($color_parts as $color) { $color = hexdec($color); // Convert to decimal $color = max(0, min(255, $color + $steps)); // Adjust color $return .= str_pad(dechex($color), 2, '0', STR_PAD_LEFT); // Make two char hex code } return $return; } /** * Highlight (or replace with specific strings) part of string. * Optionally can search full words of search strings. * * @param string $text Text where need highlight search string * @param array $search Search array. Can be string, simple array or array with 'search', 'replace' pairs * @param string $replace Default is just * @param bool $words If True search full words * * @return string */ function html_highlight($text, $search = [], $replace = '', $words = FALSE) { if (empty($replace)) { // Default is highlight as danger class $replace = $words ? '<em class="text-danger">$1</em>' : '<em class="text-danger">$0</em>'; } $entries = []; foreach ((array)$search as $entry) { if (isset($entry['search'])) { if (!isset($entry['replace'])) { $entry['replace'] = $replace; } $text = html_highlight($text, $entry['search'], $entry['replace'], $words); continue; } if (strlen($entry) == 0) { continue; } $entry = preg_quote($entry, '%'); // allow limited regex patterns in search strings (currently only for interfaces links) //$patterns = [ '\\\\d\+' => '\d+', ]; $entries[] = str_replace('\\\\d\+', '\d+', $entry); } if (!count($entries)) { return $text; } $search_pattern = '(' . implode('|', $entries) . ')'; if ($words) { // Search full words $search_pattern = str_replace('(?:', '(', OBS_PATTERN_START) . $search_pattern . str_replace('(?:', '(', OBS_PATTERN_END); // append start and end in pattern search $replace = '$1' . $replace . '$3'; } else { // Search any search string $search_pattern = '%' . $search_pattern . '%i'; } return preg_replace($search_pattern, $replace, $text); } /** * Silly class to assign and remember a unique class for a type. * * @param string $type * @param string $group * * @return string */ function get_type_class($type, $group = "unknown") { global $cache; // Short-circuit if hardcoded classes exist for this if (isset($GLOBALS['config']['type_class'][$group][$type]['class'])) { return $GLOBALS['config']['type_class'][$group][$type]['class']; } if (isset($cache['type_class'][$group][$type])) { return $cache['type_class'][$group][$type]['class']; } // all known label classes // $classes = [ 'default', 'primary', 'success', 'info', 'warning', 'important', // 'error', 'danger', 'suppressed', 'delayed', 'inverse', 'rainbow' ]; // available label classes for cycling $classes = [ 'primary', 'success', 'info', 'warning', 'important', 'suppressed', 'default' ]; $next = $cache['type_class'][$group]['_NEXT_'] ?? 0; $cache['type_class'][$group][$type]['class'] = $classes[$next]; if (isset($classes[$next + 1])) { $next++; } else { $next = 0; } $cache['type_class'][$group]['_NEXT_'] = $next; return $cache['type_class'][$group][$type]['class']; } /** * Silly class to return a label using persistent class for a certain string/type within a given group * * @param string $type * @param string $group * * @return string */ function get_type_class_label($type, $group = "unknown") { return '<span class="label label-' . get_type_class($type, $group) . '">' . $type . '</span>'; } /** * Get a value from the array by traversing the keys. * * @param array $array The array to get the value from. * @param array $keys An array of keys to traverse. * * @return mixed|null The value at the specified keys or null if not found. */ function get_value_by_keys(&$array, $keys) { $key = array_shift($keys); if (empty($keys)) { return $array[$key] ?? NULL; } return isset($array[$key]) ? get_value_by_keys($array[$key], $keys) : NULL; } /** * Set a value in the array by traversing the keys. * * @param array $array The array to set the value in. * @param array $keys An array of keys to traverse. * @param mixed $value The value to set. */ function set_value_by_keys(&$array, $keys, $value) { $key = array_shift($keys); if (empty($keys)) { $array[$key] = $value; } else { if (!isset($array[$key])) { $array[$key] = []; } set_value_by_keys($array[$key], $keys, $value); } } /** * Unset a value in the array by traversing the keys. * * @param array $array The array to unset the value in. * @param array $keys An array of keys to traverse. */ function unset_value_by_keys(&$array, $keys) { $key = array_shift($keys); if (empty($keys)) { unset($array[$key]); } else { if (isset($array[$key])) { unset_value_by_keys($array[$key], $keys); } } } // EOF