column = trim($column); $this->operator = trim($operator); $this->argument = is_array($argument) ? $argument : trim($argument); } /* returns a where clause and adds needed operators to the passed array */ public function whereClause(&$args, &$joins) { $key = self::getNewKey($this->column); $addendum = ''; /* check if we have to do some parsing*/ if (self::$columns[$this->column]['type'] === 'date') { $args[$key] = strtotime($this->argument); if ($this->operator === '=' || $this->operator === '!=') { $key2 = self::getNewKey($this->column); $args[$key2] = strtotime(' +1 day', $args[$key]); return ($this->operator === '=' ? '' : 'NOT ') . 'm.' . $this->column . " BETWEEN :$key AND :$key2"; } if ($this->operator === '>') { $args[$key] = strtotime('+1 day', $args[$key]); } elseif ($this->operator === '<') { $args[$key] = strtotime('-1 day', $args[$key]); } } else { $args[$key] = $this->argument; if ($this->operator === '~' || $this->operator === '!~') { $args[$key] = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $args[$key]); $addendum = " ESCAPE '='"; } } $op = $this->operator; if ($this->operator == '~') { $op = 'LIKE'; } elseif ($this->operator == '!~') { $op = 'NOT LIKE'; } return 'm.' . $this->column . ' ' . $op . ' :' . $key . $addendum; } /* parse a query into an array of filters */ public static function parseQuery() { // Get current settings from GET $currentValues = self::loadFilterFromGet(); $filters = []; foreach ($currentValues as $filterType => $data) { if (!$data['filter']) continue; $operator = $data['op']; $argument = $data['argument']; if ($filterType === 'gbram') { $filters[] = new RamGbStatisticsFilter($operator, $argument); } elseif ($filterType === 'runtime') { $filters[] = new RuntimeStatisticsFilter($operator, $argument); } elseif ($filterType === 'state') { $filters[] = new StateStatisticsFilter($operator, $argument); } elseif ($filterType === 'hddgb') { $filters[] = new Id44StatisticsFilter($operator, $argument); } elseif ($filterType === 'location') { $filters[] = new LocationStatisticsFilter($operator, $argument); } elseif ($filterType === 'subnet') { $filters[] = new SubnetStatisticsFilter($operator, $argument); } else { if (array_key_exists($filterType, self::$columns)) { $filters[] = new StatisticsFilter($filterType, $operator, $argument); } else { Message::addError('invalid-filter-key', $filterType); } } } return $filters; } private static function loadFilterFromGet() { $ops = Request::get('op', [], 'array'); $currentValues = ArrayUtil::mergeByKey([ 'filter' => Request::get('filter', [], 'array'), 'op' => $ops, 'argument' => Request::get('arg', [], 'array'), ]); if (Request::get('show') === false && empty($ops)) { $currentValues['lastseen'] = [ 'filter' => true, 'op' => '>', 'argument' => gmdate('Y-m-d', strtotime('-30 day')), ]; } return $currentValues; } /** * @param \StatisticsFilterSet $filterSet */ public static function renderFilterBox($show, $filterSet) { // Build location list, with permissions $locs = []; if (Module::isAvailable('locations')) { $allowed = $filterSet->getAllowedLocations(); foreach (Location::getLocations(-1, 0, true) as $loc) { $locs[] = [ 'key' => $loc['locationid'], 'value' => $loc['locationpad'] . ' ' . $loc['locationname'], 'disabled' => $allowed !== false && !in_array($loc['locationid'], $allowed) ? 'disabled' : '', ]; } } // Get current settings from GET $currentValues = self::loadFilterFromGet(); // Build column array for rendering $columns = []; foreach (self::$columns as $key => $col) { if ($key === 'location') { $col['values'] = $locs; } $col['key'] = $key; $col['name'] = Dictionary::translateFile('filters', $key, true); if ($col['type'] === 'int') { $col['input'] = 'number'; } elseif ($col['type'] === 'string') { $col['input'] = 'text'; } elseif ($col['type'] === 'date') { $col['input'] = 'text'; $col['inputclass'] = 'is-date'; } elseif ($col['type'] === 'enum') { $col['enum'] = true; if (isset($col['values'][0])) { if (!is_array($col['values'][0])) { // Arrayize $col['values'] = array_map(function($e) { return [ 'key' => $e, 'value' => $e, ]; }, $col['values']); } } else { $col['values'] = array_map(function($v, $k) { return [ 'key' => $k, 'value' => $v, ]; }, $col['values'], array_keys($col['values'])); } if (isset($currentValues[$key]['argument'])) { // Current value from GET foreach ($col['values'] as &$value) { if ($value['key'] == $currentValues[$key]['argument']) { $value['selected'] = 'selected'; } } } } // current value from GET if (isset($currentValues[$key])) { $col['currentvalue'] = $currentValues[$key]['argument'] ?? ''; if ($currentValues[$key]['filter']) { $col['checked'] = 'checked'; } elseif (!isset($col['show']) || !$col['show']) { $col['collapse'] = 'collapse'; } } elseif (!isset($col['show']) || !$col['show']) { $col['collapse'] = 'collapse'; } // Current value, arrayize foreach ($col['op'] as &$value) { $value = ['op' => $value]; if (($currentValues[$key]['op'] ?? '=') === $value['op']) { $value['selected'] = 'selected'; } } $columns[] = $col; } $data = array( 'show' => $show, 'columns' => $columns, ); if ($show === 'list') { $data['listButtonClass'] = 'active'; $data['statButtonClass'] = ''; } else { $data['listButtonClass'] = ''; $data['statButtonClass'] = 'active'; } Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']); Render::addTemplate('filterbox', $data); } /* * Simple filters that map directly to DB columns */ const OP_ORDINAL = ['=', '!=', '<', '>', '<=', '>=']; const OP_STRCMP = [ '=', '!=', '!~', '~']; const OP_NOMINAL = ['!=', '=']; public static $columns; /** * Do this here instead of const since we need to check for available modules while building array. */ public static function initConstants() { self::$columns = [ 'clientip' => [ 'op' => self::OP_NOMINAL, 'type' => 'string', 'placeholder' => '1.2.3.4', 'show' => true, ], 'hostname' => [ 'op' => self::OP_STRCMP, 'type' => 'string', 'placeholder' => 'pc.fqdn.example.com', ], 'machineuuid' => [ 'op' => self::OP_NOMINAL, 'type' => 'string', 'placeholder' => '88888888-4444-4444-121212121212', 'show' => true, ], 'macaddr' => [ 'op' => self::OP_NOMINAL, 'type' => 'string', 'placeholder' => '11-22-33-44-55-66', ], 'firstseen' => [ 'op' => self::OP_ORDINAL, 'type' => 'date', 'placeholder' => '2020-10-15', ], 'lastseen' => [ 'op' => self::OP_ORDINAL, 'type' => 'date', 'placeholder' => '2020-10-15', ], 'logintime' => [ 'op' => self::OP_ORDINAL, 'type' => 'date', 'placeholder' => '2020-10-15', ], 'lastboot' => [ 'op' => self::OP_ORDINAL, 'type' => 'date', 'placeholder' => '2020-10-15', ], 'runtime' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', ], 'realcores' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', ], 'systemmodel' => [ 'op' => self::OP_STRCMP, 'type' => 'string', 'placeholder' => 'PC-365 (IBM)', ], 'cpumodel' => [ 'op' => self::OP_STRCMP, 'type' => 'string', 'placeholder' => 'Pentium Pro 200', ], 'hddgb' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', 'placeholder' => 'GiB', 'map_sort' => 'id44mb' ], 'gbram' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', 'map_sort' => 'mbram', 'placeholder' => 'GiB', ], 'kvmstate' => [ 'op' => self::OP_NOMINAL, 'type' => 'enum', 'values' => ['ENABLED', 'DISABLED', 'UNSUPPORTED'] ], 'badsectors' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', ], 'subnet' => [ 'op' => self::OP_NOMINAL, 'type' => 'string', ], 'currentuser' => [ 'op' => self::OP_NOMINAL, 'type' => 'string', 'placeholder' => 'login', ], 'state' => [ 'op' => self::OP_NOMINAL, 'type' => 'enum', 'values' => ['occupied', 'on', 'off', 'idle', 'standby'] ], 'live_swapfree' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', 'placeholder' => 'MiB', ], 'live_memfree' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', 'placeholder' => 'MiB', ], 'live_tmpfree' => [ 'op' => self::OP_ORDINAL, 'type' => 'int', 'placeholder' => 'MiB', ], ]; if (Module::isAvailable('locations')) { self::$columns['location'] = [ 'op' => self::OP_STRCMP, 'type' => 'enum', 'show' => true, // values filled on render NOCOMMIT ]; } } } class RamGbStatisticsFilter extends StatisticsFilter { public function __construct($operator, $argument) { parent::__construct('mbram', $operator, $argument); } public function whereClause(&$args, &$joins) { $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$this->argument, false) * 1024 - 100); $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$this->argument, true) * 1024 + 100); if ($this->operator == '=') { return " mbram BETWEEN $lower AND $upper"; } elseif ($this->operator == '<') { return " mbram < $lower"; } elseif ($this->operator == '<=') { return " mbram <= $upper"; } elseif ($this->operator == '>') { return " mbram > $upper"; } elseif ($this->operator == '>=') { return " mbram >= $lower"; } elseif ($this->operator == '!=') { return " (mbram < $lower OR mbram > $upper)"; } else { error_log("unimplemented operator in RamGbFilter: $this->operator"); return ' 1'; } } } class RuntimeStatisticsFilter extends StatisticsFilter { public function __construct($operator, $argument) { parent::__construct('lastboot', $operator, $argument); } public function whereClause(&$args, &$joins) { $upper = time() - (int)$this->argument * 3600; $lower = $upper - 3600; $common = "state IN ('OCCUPIED', 'IDLE', 'STANDBY') AND"; if ($this->operator == '=') { return "$common ({$this->column} BETWEEN $lower AND $upper)"; } elseif ($this->operator == '<') { return "$common {$this->column} > $upper"; } elseif ($this->operator == '<=') { return "$common {$this->column} > $lower"; } elseif ($this->operator == '>') { return "$common {$this->column} < $lower"; } elseif ($this->operator == '>=') { return "$common {$this->column} < $upper"; } elseif ($this->operator == '!=') { return "$common ({$this->column} < $lower OR {$this->column} > $upper)"; } else { error_log("unimplemented operator in RuntimeFilter: $this->operator"); return ' 1'; } } } class Id44StatisticsFilter extends StatisticsFilter { public function __construct($operator, $argument) { parent::__construct('id44mb', $operator, $argument); } public function whereClause(&$args, &$joins) { if ($this->operator === '=' || $this->operator === '!=') { $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $this->argument, false) * 1024 - 100); $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $this->argument, true) * 1024 + 100); } else { $lower = $upper = round($this->argument * 1024); } if ($this->operator === '=') { return " id44mb BETWEEN $lower AND $upper"; } elseif ($this->operator === '!=') { return " id44mb < $lower OR id44mb > $upper"; } elseif ($this->operator === '<=') { return " id44mb <= $upper"; } elseif ($this->operator === '>=') { return " id44mb >= $lower"; } elseif ($this->operator === '<') { return " id44mb < $lower"; } elseif ($this->operator === '>') { return " id44mb > $upper"; } else { error_log("unimplemented operator in Id44Filter: $this->operator"); return ' 1'; } } } class StateStatisticsFilter extends StatisticsFilter { public function __construct($operator, $argument) { parent::__construct(null, $operator, $argument); } public function whereClause(&$args, &$joins) { $map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ]; $neg = $this->operator == '!=' ? 'NOT ' : ''; if (array_key_exists($this->argument, $map)) { $key = StatisticsFilter::getNewKey($this->column); $args[$key] = $map[$this->argument]; return " m.state $neg IN ( :$key ) "; } else { Message::addError('invalid-filter-argument', 'state', $this->argument); return ' 1'; } } } class LocationStatisticsFilter extends StatisticsFilter { public function __construct($operator, $argument) { parent::__construct('locationid', $operator, $argument); } public function whereClause(&$args, &$joins) { $recursive = (substr($this->operator, -1) === '~'); $this->operator = str_replace('~', '=', $this->operator); if (is_array($this->argument)) { if ($recursive) Util::traceError('Cannot use ~ operator for location with array'); } else { settype($this->argument, 'int'); } $neg = $this->operator === '=' ? '' : 'NOT'; if ($this->argument === 0) { return "m.locationid IS $neg NULL"; } else { $key = StatisticsFilter::getNewKey($this->column); if ($recursive) { $args[$key] = array_keys(Location::getRecursiveFlat($this->argument)); } else { $args[$key] = $this->argument; } return "m.locationid $neg IN (:$key)"; } } } class SubnetStatisticsFilter extends StatisticsFilter { public function __construct($operator, $argument) { parent::__construct(null, $operator, $argument); } public function whereClause(&$args, &$joins) { $argument = preg_replace('/[^0-9\.:]/', '', $this->argument); return " clientip LIKE '$argument%'"; } } class IsClientStatisticsFilter extends StatisticsFilter { public function __construct($argument) { parent::__construct(null, null, $argument); } public function whereClause(&$args, &$joins) { if ($this->argument) { $joins[] = ' LEFT JOIN runmode USING (machineuuid)'; return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)"; } $joins[] = ' INNER JOIN runmode USING (machineuuid)'; return "runmode.isclient = 0"; } } StatisticsFilter::initConstants();