From d6bbb4d57a086dfaf5f0a1ebf8913577568ae887 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 21 Apr 2020 18:16:36 +0200 Subject: [statistics] Refactor filter creation (Part 1) Filter classes are now instances of their respective classes, to move more logic into those classes. A bind method is used for assigning actual operator and argument values. renderFilterBox() is still a little too messy, maybe a clever class for mapping a (bound) filter to data for mustache will come in handy here. --- .../statistics/inc/statisticsfilter.inc.php | 729 +++++++++++---------- .../statistics/inc/statisticsfilterset.inc.php | 67 +- modules-available/statistics/pages/list.inc.php | 5 +- modules-available/statistics/pages/summary.inc.php | 19 +- .../statistics/templates/filterbox.html | 2 +- 5 files changed, 401 insertions(+), 421 deletions(-) diff --git a/modules-available/statistics/inc/statisticsfilter.inc.php b/modules-available/statistics/inc/statisticsfilter.inc.php index 6999654d..e554a61c 100644 --- a/modules-available/statistics/inc/statisticsfilter.inc.php +++ b/modules-available/statistics/inc/statisticsfilter.inc.php @@ -3,7 +3,7 @@ /* base class with rudimentary SQL generation abilities. * WARNING: argument is escaped, but $column and $operator are passed unfiltered into SQL */ -class StatisticsFilter +abstract class StatisticsFilter { /** * Legacy delimiter for js_selectize filters - used to redirect old URLs @@ -13,11 +13,49 @@ class StatisticsFilter const SIZE_ID44 = array(0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 2000, 4000); const SIZE_RAM = array(1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024); + private static $keyCounter = 0; + + + /* + * Simple filters that map directly to DB columns + */ + + const OP_ORDINAL = ['=', '!=', '<', '>', '<=', '>=']; + const OP_STRCMP = ['~', '!~', '=', '!=']; + const OP_NOMINAL = ['=', '!=']; + + /** + * @var StatisticsFilter[] + */ + public static $columns; + + /** + * @var string|null db-based sort column for this field, null if not sortable + */ public $column; - public $operator; - public $argument; - private static $keyCounter = 0; + /** + * @var string[] valid operators for this filter + */ + public $ops; + /** + * @var string placeholder for input field + */ + public $placeholder; + + public function __construct($column, array $ops, string $placeholder = '') + { + $this->column = $column; + $this->ops = $ops; + $this->placeholder = $placeholder; + } + + public function type() { return $this->ops === self::OP_ORDINAL ? 'int' : 'string'; } + + /* returns a where clause and adds needed operators to the passed arrays */ + public abstract function whereClause(string $operator, $argument, array &$args, array &$joins); + + public function bind(string $op, $argument) { return new DatabaseFilter($this, $op, $argument); } public static function findBestValue($array, $value, $up) { @@ -45,88 +83,12 @@ class StatisticsFilter return $colname . '_' . (self::$keyCounter++); } - public function __construct($column, $operator, $argument = null) - { - $this->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 */ + /** + * @return DatabaseFilter[] + */ 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'), @@ -140,7 +102,21 @@ class StatisticsFilter 'argument' => gmdate('Y-m-d', strtotime('-30 day')), ]; } - return $currentValues; + $filters = []; + foreach ($currentValues as $filterType => $data) { + if (!$data['filter']) + continue; + $operator = $data['op']; + $argument = $data['argument']; + + if (array_key_exists($filterType, self::$columns)) { + $filters[$filterType] = self::$columns[$filterType]->bind($operator, $argument); + } else { + Message::addError('invalid-filter-key', $filterType); + } + } + + return $filters; } /** @@ -149,414 +125,420 @@ class StatisticsFilter 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' : '', - ]; - } + self::$columns['location']->filterLocations($filterSet->getAllowedLocations()); } - // 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') { + $showCount = 0; + foreach (self::$columns as $key => $filter) { + $col = [ + 'key' => $key, + 'name' => Dictionary::translateFile('filters', $key, true), + 'placeholder' => $filter->placeholder, + ]; + $bind = $filterSet->hasFilterKey($key); + if ($filter->type() === 'int') { $col['input'] = 'number'; - } elseif ($col['type'] === 'string') { + } elseif ($filter->type() === 'string') { $col['input'] = 'text'; - } elseif ($col['type'] === 'date') { + } elseif ($filter->type() === 'date') { $col['input'] = 'text'; $col['inputclass'] = 'is-date'; - } elseif ($col['type'] === 'enum') { + } elseif ($filter->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'])) { + $col['values'] = $filter->values; + if ($bind !== false) { // Current value from GET foreach ($col['values'] as &$value) { - if ($value['key'] == $currentValues[$key]['argument']) { + if ($value['key'] == $bind->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'; - } + if ($bind !== false) { + $col['currentvalue'] = $bind->argument; + $col['checked'] = 'checked'; + $showCount++; } elseif (!isset($col['show']) || !$col['show']) { $col['collapse'] = 'collapse'; } - // Current value, arrayize + $col['op'] = $filter->ops; foreach ($col['op'] as &$value) { $value = ['op' => $value]; - if (($currentValues[$key]['op'] ?? '=') === $value['op']) { + if ($bind !== false && $bind->op === $value['op']) { $value['selected'] = 'selected'; } } - $columns[] = $col; + $columns[$key] = $col; + } + if ($showCount < 2) { + unset($columns['clientip']['collapse']); + } + if ($showCount < 1) { + unset($columns['machineuuid']['collapse']); } $data = array( 'show' => $show, - 'columns' => $columns, + 'columns' => array_values($columns), + $show . 'ButtonClass' => 'active', ); - if ($show === 'list') { - $data['listButtonClass'] = 'active'; - $data['statButtonClass'] = ''; + Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']); + Render::addTemplate('filterbox', $data); + } + + public static function initConstants() + { + self::$columns = [ + 'clientip' => new IpStatisticsFilter(), + 'hostname' => new SimpleStatisticsFilter('hostname', self::OP_STRCMP, 'pc.fqdn.example.com'), + 'machineuuid' => new SimpleStatisticsFilter('machineuuid', self::OP_STRCMP, '88888888-4444-4444-121212121212'), + 'macaddr' => new SimpleStatisticsFilter('macaddr', self::OP_STRCMP, '11-22-33-44-55-66'), + 'firstseen' => new DateStatisticsFilter('firstseen', '2020-10-15 14:00'), + 'lastseen' => new DateStatisticsFilter('lastseen', '2020-10-15 14:00'), + 'logintime' => new DateStatisticsFilter('logintime', '2020-10-15 14:00'), + 'lastboot' => new RuntimeStatisticsFilter(), + 'runtime' => new SimpleStatisticsFilter('runtime', self::OP_ORDINAL, ''), + 'realcores' => new SimpleStatisticsFilter('realcores', self::OP_ORDINAL, ''), + 'systemmodel' => new SimpleStatisticsFilter('systemmodel', self::OP_STRCMP, 'PC-365 (IBM)'), + 'cpumodel' => new SimpleStatisticsFilter('cpumodel', self::OP_STRCMP, 'Pentium Pro 200 MHz'), + 'hddgb' => new Id44StatisticsFilter(), + 'gbram' => new RamGbStatisticsFilter(), + 'kvmstate' => new EnumStatisticsFilter('kvmstate', ['ENABLED', 'DISABLED', 'UNSUPPORTED']), + 'badsectors' => new SimpleStatisticsFilter('badsectors', self::OP_ORDINAL, ''), + 'currentuser' => new SimpleStatisticsFilter('currentuser', self::OP_STRCMP, 'login'), + 'state' => new StateStatisticsFilter(), + 'live_swapfree' => new SimpleStatisticsFilter('live_swapfree', self::OP_ORDINAL, 'MiB'), + 'live_memfree' => new SimpleStatisticsFilter('live_memfree', self::OP_ORDINAL, 'MiB'), + 'live_tmpfree' => new SimpleStatisticsFilter('live_tmpfree', self::OP_ORDINAL, 'MiB'), + ]; + if (Module::isAvailable('locations')) { + self::$columns['location'] = new LocationStatisticsFilter(); + } + } + +} + +class SimpleStatisticsFilter extends StatisticsFilter +{ + + public function whereClause(string $operator, $argument, array &$args, array &$joins) + { + $addendum = ''; + $key = self::getNewKey($this->column); + $args[$key] = $argument; + + if (is_array($argument)) { + if ($operator{0} === '!') { + $op = 'NOT IN'; + } else { + $op = 'IN'; + } } else { - $data['listButtonClass'] = ''; - $data['statButtonClass'] = 'active'; + if ($operator === '~' || $operator === '!~') { + $args[$key] = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $args[$key]); + $addendum = " ESCAPE '='"; + } + $op = $operator; + if ($operator === '~') { + $op = 'LIKE'; + } elseif ($operator === '!~') { + $op = 'NOT LIKE'; + } } + return 'm.' . $this->column . ' ' . $op . ' (:' . $key . ') ' . $addendum; + } - Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']); - Render::addTemplate('filterbox', $data); +} + +class EnumStatisticsFilter extends SimpleStatisticsFilter +{ + + public $values; + + public function __construct(string $column, array $values) + { + parent::__construct($column, self::OP_NOMINAL, ''); + if (isset($values[0])) { + if (!is_array($values[0])) { + $values = array_map(function($e) { return [ + 'key' => $e, + 'value' => $e, + ]; }, $values); + } + } else { + $values = array_map(function($v, $k) { return [ + 'key' => $k, + 'value' => $v, + ]; }, $values, array_keys($values)); + } + $this->values = $values; } - /* - * Simple filters that map directly to DB columns - */ + public function type() { return 'enum'; } - const OP_ORDINAL = ['=', '!=', '<', '>', '<=', '>=']; - const OP_STRCMP = [ '=', '!=', '!~', '~']; - const OP_NOMINAL = ['!=', '=']; - public static $columns; + public function whereClause(string $operator, $argument, array &$args, array &$joins) + { + $keys = ArrayUtil::flattenByKey($this->values, 'key'); + if (is_array($argument)) { + $ok = true; + foreach ($argument as $e) { + if (!in_array($e, $keys)) { + $ok = false; + } + } + } else { + $ok = in_array($argument, $keys); + } + if (!$ok) { + Message::addError('invalid-enum-item', $this->column, $argument); + return '0'; + } + return parent::whereClause($operator, $argument, $args, $joins); + } - /** - * Do this here instead of const since we need to check for available modules while building array. - */ - public static function initConstants() +} + +class DateStatisticsFilter extends StatisticsFilter +{ + + public function __construct(string $column, string $placeholder) { + parent::__construct($column, self::OP_ORDINAL, $placeholder); + } - 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 - ]; + public function type() { return 'date'; } + + public function whereClause(string $operator, $argument, array &$args, array &$joins) + { + $key = self::getNewKey($this->column); + $addendum = ''; + + if (!preg_match('/^(?\d{4}-\d{2}-\d{2})(\s+(?\d{1,2})(:(?\d{2})(:\d+)?)?)?$/', $argument, $out)) { + Message::addError('invalid-date-format', $argument); + return '0'; } + + if (isset($out['m'])) { + $span = 'minute'; + } elseif (isset($out['h'])) { + $span = 'hour'; + $argument .= ':00'; + } else { + $span = 'day'; + } + + $args[$key] = strtotime($argument); + if ($operator === '=' || $operator === '!=') { + $key2 = self::getNewKey($this->column); + $args[$key2] = strtotime(' +1 ' . $span, $args[$key]); + return ($operator === '=' ? '' : 'NOT ') . 'm.' . $this->column . " BETWEEN :$key AND :$key2"; + } + if ($operator === '>' || $operator === '<=') { + $args[$key] = strtotime('+1 ' . $span . ' -1 second', $args[$key]); + } + + return 'm.' . $this->column . ' ' . $operator . ' :' . $key . $addendum; } } class RamGbStatisticsFilter extends StatisticsFilter { - public function __construct($operator, $argument) + + public function __construct() { - parent::__construct('mbram', $operator, $argument); + parent::__construct('mbram', self::OP_ORDINAL, 'GiB'); } - public function whereClause(&$args, &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$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 == '=') { + $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$argument, false) * 1024 - 100); + $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$argument, true) * 1024 + 100); + if ($operator == '=') { return " mbram BETWEEN $lower AND $upper"; - } elseif ($this->operator == '<') { + } elseif ($operator == '<') { return " mbram < $lower"; - } elseif ($this->operator == '<=') { + } elseif ($operator == '<=') { return " mbram <= $upper"; - } elseif ($this->operator == '>') { + } elseif ($operator == '>') { return " mbram > $upper"; - } elseif ($this->operator == '>=') { + } elseif ($operator == '>=') { return " mbram >= $lower"; - } elseif ($this->operator == '!=') { - return " (mbram < $lower OR mbram > $upper)"; - } else { - error_log("unimplemented operator in RamGbFilter: $this->operator"); - - return ' 1'; } + // != + return " (mbram < $lower OR mbram > $upper)"; } + } class RuntimeStatisticsFilter extends StatisticsFilter { - public function __construct($operator, $argument) + + public function __construct() { - parent::__construct('lastboot', $operator, $argument); + parent::__construct('lastboot', self::OP_ORDINAL); } - public function whereClause(&$args, &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins) { - $upper = time() - (int)$this->argument * 3600; + $upper = time() - (int)$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'; + if ($operator == '=') { + return "$common (lastboot BETWEEN $lower AND $upper)"; + } elseif ($operator == '<') { + return "$common lastboot > $upper"; + } elseif ($operator == '<=') { + return "$common lastboot > $lower"; + } elseif ($operator == '>') { + return "$common lastboot < $lower"; + } elseif ($operator == '>=') { + return "$common lastboot < $upper"; } + // != + return "$common (lastboot < $lower OR lastboot > $upper)"; } } class Id44StatisticsFilter extends StatisticsFilter { - public function __construct($operator, $argument) + + public function __construct() { - parent::__construct('id44mb', $operator, $argument); + parent::__construct('id44mb', self::OP_ORDINAL,'GiB'); } - public function whereClause(&$args, &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$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); + if ($operator === '=' || $operator === '!=') { + $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $argument, false) * 1024 - 100); + $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $argument, true) * 1024 + 100); } else { - $lower = $upper = round($this->argument * 1024); + $lower = $upper = round($argument * 1024); } - if ($this->operator === '=') { + if ($operator === '=') { return " id44mb BETWEEN $lower AND $upper"; - } elseif ($this->operator === '!=') { - return " id44mb < $lower OR id44mb > $upper"; - } elseif ($this->operator === '<=') { + } elseif ($operator === '<=') { return " id44mb <= $upper"; - } elseif ($this->operator === '>=') { + } elseif ($operator === '>=') { return " id44mb >= $lower"; - } elseif ($this->operator === '<') { + } elseif ($operator === '<') { return " id44mb < $lower"; - } elseif ($this->operator === '>') { + } elseif ($operator === '>') { return " id44mb > $upper"; - } else { - error_log("unimplemented operator in Id44Filter: $this->operator"); - - return ' 1'; } + // != + return " id44mb < $lower OR id44mb > $upper"; } } class StateStatisticsFilter extends StatisticsFilter { - public function __construct($operator, $argument) + + public function __construct() { - parent::__construct(null, $operator, $argument); + parent::__construct('state', self::OP_NOMINAL); } - public function whereClause(&$args, &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins) { $map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ]; - $neg = $this->operator == '!=' ? 'NOT ' : ''; - if (array_key_exists($this->argument, $map)) { + $neg = $operator == '!=' ? 'NOT ' : ''; + if (array_key_exists($argument, $map)) { $key = StatisticsFilter::getNewKey($this->column); - $args[$key] = $map[$this->argument]; + $args[$key] = $map[$argument]; return " m.state $neg IN ( :$key ) "; } else { - Message::addError('invalid-filter-argument', 'state', $this->argument); + Message::addError('invalid-filter-argument', 'state', $argument); return ' 1'; } } } -class LocationStatisticsFilter extends StatisticsFilter +class LocationStatisticsFilter extends EnumStatisticsFilter { - public function __construct($operator, $argument) + + public function __construct() { - parent::__construct('locationid', $operator, $argument); + $locs = []; + foreach (Location::getLocations(-1, 0, true) as $loc) { + $locs[] = [ + 'key' => $loc['locationid'], + 'value' => $loc['locationpad'] . ' ' . $loc['locationname'], + ]; + } + parent::__construct('locationid', $locs); + } - public function whereClause(&$args, &$joins) + public function type() { return 'enum'; } + + public function whereClause(string $operator, $argument, array &$args, array &$joins) { - $recursive = (substr($this->operator, -1) === '~'); - $this->operator = str_replace('~', '=', $this->operator); + $recursive = (substr($operator, -1) === '~'); + $operator = str_replace('~', '=', $operator); - if (is_array($this->argument)) { - if ($recursive) - Util::traceError('Cannot use ~ operator for location with array'); - } else { - settype($this->argument, 'int'); + if ($recursive && is_array($argument)) { + Util::traceError('Cannot use ~ operator for location with array'); } - $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; + if ($recursive) { + $argument = array_keys(Location::getRecursiveFlat($argument)); + } + return parent::whereClause($operator, $argument, $args, $joins); + } + + public function filterLocations($list) + { + if ($list === false || in_array(0, $list)) + return; + foreach ($this->values as &$loc) { + if (!in_array($loc['key'], $list)) { + $loc['disabled'] = 'disabled'; } - return "m.locationid $neg IN (:$key)"; } } } -class SubnetStatisticsFilter extends StatisticsFilter +class IpStatisticsFilter extends StatisticsFilter { - public function __construct($operator, $argument) + + public function __construct() { - parent::__construct(null, $operator, $argument); + parent::__construct('clientip', self::OP_NOMINAL, '1.2.3.4, 1.2.3.*, 1.2.3/24'); } - public function whereClause(&$args, &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins) { - $argument = preg_replace('/[^0-9\.:]/', '', $this->argument); - return " clientip LIKE '$argument%'"; + $argument = preg_replace('#[^0-9.:/*]#', '', $argument); + if (strpos($argument, '/') !== false) { + $range = IpUtil::parseCidr($argument); + if ($range === false) { + Message::addError('invalid-cidr-notion', $argument); + return '0'; + } + return 'INET_ATON(clientip) BETWEEN ' . $range['start'] . ' AND ' . $range['end']; + } + return "clientip LIKE '" . str_replace('*', '%', $argument) . "'"; } } class IsClientStatisticsFilter extends StatisticsFilter { - public function __construct($argument) + + public function __construct() { - parent::__construct(null, null, $argument); + parent::__construct(null, []); } - public function whereClause(&$args, &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins) { - if ($this->argument) { + if ($argument) { $joins[] = ' LEFT JOIN runmode USING (machineuuid)'; return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)"; } @@ -566,4 +548,29 @@ class IsClientStatisticsFilter extends StatisticsFilter } +class DatabaseFilter +{ + /** @var StatisticsFilter + */ + private $inst; + public $op; + public $argument; + public function __construct(StatisticsFilter $inst, string $op, $argument) + { + $this->inst = $inst; + $this->op = $op; + $this->argument = $argument; + } + public function whereClause(array &$args, array &$joins) + { + return $this->inst->whereClause($this->op, $this->argument, $args, $joins); + } + + public function isClass($what) + { + return get_class($this->inst) === $what; + } + +} + StatisticsFilter::initConstants(); diff --git a/modules-available/statistics/inc/statisticsfilterset.inc.php b/modules-available/statistics/inc/statisticsfilterset.inc.php index c2642850..a96102dc 100644 --- a/modules-available/statistics/inc/statisticsfilterset.inc.php +++ b/modules-available/statistics/inc/statisticsfilterset.inc.php @@ -3,11 +3,9 @@ class StatisticsFilterSet { /** - * @var \StatisticsFilter[] + * @var \DatabaseFilter[] */ private $filters; - private $sortDirection; - private $sortColumn; private $cache = false; @@ -16,34 +14,17 @@ class StatisticsFilterSet $this->filters = $filters; } - public function setSort($col, $direction) - { - $direction = ($direction === 'DESC' ? 'DESC' : 'ASC'); - - if (!is_string($col) || !array_key_exists($col, StatisticsFilter::$columns)) { - /* default sorting column is clientip */ - $col = 'clientip'; - } - if ($col === $this->sortColumn && $direction === $this->sortDirection) - return; - $this->cache = false; - $this->sortDirection = $direction; - $this->sortColumn = $col; - } - - public function makeFragments(&$where, &$join, &$sort, &$args) + public function makeFragments(&$where, &$join, &$args) { if ($this->cache !== false) { $where = $this->cache['where']; $join = $this->cache['join']; - $sort = $this->cache['sort']; $args = $this->cache['args']; return; } /* generate where clause & arguments */ $where = ''; $joins = []; - $sort = ""; $args = []; if (empty($this->filters)) { $where = ' 1 '; @@ -54,18 +35,7 @@ class StatisticsFilterSet } } $join = implode(' ', array_unique($joins)); - - $col = $this->sortColumn; - $isMapped = array_key_exists('map_sort', StatisticsFilter::$columns[$col]); - $concreteCol = ($isMapped ? StatisticsFilter::$columns[$col]['map_sort'] : $col) ; - - if ($concreteCol === 'clientip') { - $concreteCol = "INET_ATON(clientip)"; - } - - $sort = " ORDER BY " . $concreteCol . " " . $this->sortDirection - . ", machineuuid ASC"; - $this->cache = compact('where', 'join', 'sort', 'args'); + $this->cache = compact('where', 'join', 'args'); } public function isNoId44Filter() @@ -74,39 +44,40 @@ class StatisticsFilterSet return $filter !== false && $filter->argument == 0; } - public function getSortDirection() - { - return $this->sortDirection; - } - - public function getSortColumn() - { - return $this->sortColumn; - } - public function filterNonClients() { if (Module::get('runmode') === false || $this->hasFilter('IsClientFilter') !== false) return; $this->cache = false; // Runmode module exists, add filter - $this->filters[] = new IsClientStatisticsFilter(true); + $this->filters[] = (new IsClientStatisticsFilter())->bind('=', true); } /** * @param string $type filter type (class name) - * @return false|StatisticsFilter The filter, false if not found + * @return false|DatabaseFilter The filter, false if not found */ public function hasFilter($type) { foreach ($this->filters as $filter) { - if (get_class($filter) === $type) { + if ($filter->isClass($type)) { return $filter; } } return false; } + /** + * @param string $type filter type key/id + * @return false|DatabaseFilter The filter, false if not found + */ + public function hasFilterKey($type) + { + if (isset($this->filters[$type])) + return $this->filters[$type]; + return false; + } + /** * Add a location filter based on the allowed permissions for the given permission. * Returns false if the user doesn't have the given permission for any location. @@ -116,6 +87,8 @@ class StatisticsFilterSet */ public function setAllowedLocationsFromPermission($permission) { + if (!Module::isAvailable('locations')) + return true; $locs = User::getAllowedLocations($permission); if (empty($locs)) return false; @@ -124,7 +97,7 @@ class StatisticsFilterSet return true; unset($this->filters['permissions']); } else { - $this->filters['permissions'] = new LocationStatisticsFilter('=', $locs); + $this->filters['permissions'] = StatisticsFilter::$columns['location']->bind('=', $locs); } $this->cache = false; return true; diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php index a709ab3d..7b8e8f5f 100644 --- a/modules-available/statistics/pages/list.inc.php +++ b/modules-available/statistics/pages/list.inc.php @@ -15,7 +15,6 @@ class SubPage $filters = StatisticsFilter::parseQuery(); $filterSet = new StatisticsFilterSet($filters); - $filterSet->setSort($sortColumn, $sortDirection); if (!$filterSet->setAllowedLocationsFromPermission('view.list')) { Message::addError('main.no-permission'); @@ -32,7 +31,7 @@ class SubPage private static function showMachineList($filterSet) { Module::isAvailable('js_stupidtable'); - $filterSet->makeFragments($where, $join, $sort, $args); + $filterSet->makeFragments($where, $join, $args); $xtra = ''; if ($filterSet->isNoId44Filter()) { $xtra .= ', data'; @@ -47,7 +46,7 @@ class SubPage m.logintime, m.state, m.currentuser, m.realcores, m.mbram, m.kvmstate, m.cpumodel, m.id44mb, m.hostname, m.notes IS NOT NULL AS hasnotes, m.badsectors, Count(s.machineuuid) AS confvars $xtra FROM machine m LEFT JOIN setting_machine s USING (machineuuid) - $join WHERE $where GROUP BY m.machineuuid $sort", $args); + $join WHERE $where GROUP BY m.machineuuid", $args); $rows = array(); $singleMachine = 'none'; // TODO: Cannot disable checkbox for those where user has no permission, since we got multiple actions now diff --git a/modules-available/statistics/pages/summary.inc.php b/modules-available/statistics/pages/summary.inc.php index c2e3ac80..ead62a26 100644 --- a/modules-available/statistics/pages/summary.inc.php +++ b/modules-available/statistics/pages/summary.inc.php @@ -17,7 +17,6 @@ class SubPage $filters = StatisticsFilter::parseQuery(); $filterSet = new StatisticsFilterSet($filters); - $filterSet->setSort($sortColumn, $sortDirection); if (!$filterSet->setAllowedLocationsFromPermission('view.summary')) { Message::addError('main.no-permission'); @@ -47,7 +46,7 @@ class SubPage */ private static function showSummary($filterSet) { - $filterSet->makeFragments($where, $join, $sort, $args); + $filterSet->makeFragments($where, $join, $args); $known = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE $where", $args); $on = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state IN ('IDLE', 'OCCUPIED') AND ($where)", $args); $used = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state = 'OCCUPIED' AND ($where)", $args); @@ -100,7 +99,7 @@ class SubPage */ private static function showSystemModels($filterSet) { - $filterSet->makeFragments($where, $join, $sort, $args); + $filterSet->makeFragments($where, $join, $args); $res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine m' . " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args); $lines = array(); @@ -130,8 +129,9 @@ class SubPage */ private static function showMemory($filterSet) { - $filterSet->makeFragments($where, $join, $sort, $args); - $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY mbram", $args); + $filterSet->makeFragments($where, $join, $args); + $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join + WHERE $where GROUP BY mbram", $args); $lines = array(); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { $gb = (int)ceil($row['mbram'] / 1024); @@ -174,9 +174,10 @@ class SubPage */ private static function showKvmState($filterSet) { - $filterSet->makeFragments($where, $join, $sort, $args); + $filterSet->makeFragments($where, $join, $args); $colors = array('UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6'); - $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args); + $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join + WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args); $lines = array(); $json = array(); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { @@ -195,7 +196,7 @@ class SubPage */ private static function showId44($filterSet) { - $filterSet->makeFragments($where, $join, $sort, $args); + $filterSet->makeFragments($where, $join, $args); $res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY id44mb", $args); $lines = array(); $total = 0; @@ -245,7 +246,7 @@ class SubPage */ private static function showLatestMachines($filterSet) { - $filterSet->makeFragments($where, $join, $sort, $args); + $filterSet->makeFragments($where, $join, $args); $args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10; $res = Database::simpleQuery("SELECT machineuuid, clientip, hostname, firstseen, mbram, kvmstate, id44mb FROM machine m $join" diff --git a/modules-available/statistics/templates/filterbox.html b/modules-available/statistics/templates/filterbox.html index 34f4d3a6..e7c1cd9b 100644 --- a/modules-available/statistics/templates/filterbox.html +++ b/modules-available/statistics/templates/filterbox.html @@ -7,7 +7,7 @@