summaryrefslogtreecommitdiffstats
path: root/modules-available/statistics/inc/statisticsfilter.inc.php
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/statistics/inc/statisticsfilter.inc.php')
-rw-r--r--modules-available/statistics/inc/statisticsfilter.inc.php1064
1 files changed, 701 insertions, 363 deletions
diff --git a/modules-available/statistics/inc/statisticsfilter.inc.php b/modules-available/statistics/inc/statisticsfilter.inc.php
index 934e01da..5e6448c7 100644
--- a/modules-available/statistics/inc/statisticsfilter.inc.php
+++ b/modules-available/statistics/inc/statisticsfilter.inc.php
@@ -3,21 +3,118 @@
/* 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
{
/**
- * Delimiter for js_selectize filters
+ * Legacy delimiter for js_selectize filters - used to redirect old URLs
*/
- const DELIMITER = '~,~';
+ const LEGACY_DELIMITER = '~,~';
- 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);
+ const SIZE_PARTITION = [0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 1500, 2000, 3000,
+ 4000, 6000, 8000, 10000];
+ const SIZE_RAM = [1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024, 1536,
+ 2048];
+ private static $keyCounter = 0;
+
+
+ /*
+ * Simple filters that map directly to DB columns
+ */
+
+ const OP_ORDINAL = ['=', '!=', '<', '>', '<=', '>='];
+ const OP_STRCMP = ['~', '!~', '=', '!='];
+ const OP_NOMINAL = ['=', '!='];
+ const OP_LOCATIONS = ['~', '=', '!='];
+ const OP_FUZZY_ORDINAL = ['=', '!=', '~', '!~', '<', '>', '<=', '>='];
+
+ /**
+ * @var StatisticsFilter[]
+ */
+ public static $columns;
+
+ /*
+ * Class instance stuff
+ */
+
+ /**
+ * @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(): string
+ {
+ return ($this->ops === self::OP_ORDINAL || $this->ops === self::OP_FUZZY_ORDINAL) ? 'int' : 'string';
+ }
+
+ /**
+ * Needed for joins with the hardware tables, to use the HardwareQueryColumn afterwards.
+ * The HardwareQuery class should probably be extended/rewritten to be more versatile in
+ * this regard.
+ */
+ public static function addHardwareJoin(array &$args, array &$joins, string $hwtype = null): string
+ {
+ $joins['mxhw'] = ' INNER JOIN machine_x_hw mxhw ON (mxhw.disconnecttime = 0 AND mxhw.machineuuid = m.machineuuid)';
+ $key = self::getNewKey('foo');
+ $shw = self::getNewKey('shw');
+ if ($hwtype === null) {
+ $joins[] = " INNER JOIN statistic_hw $shw ON (mxhw.hwid = {$shw}.hwid)";
+ } else {
+ $joins[] = " INNER JOIN statistic_hw $shw ON (mxhw.hwid = {$shw}.hwid AND {$shw}.hwtype = :$key)";
+ $args[$key] = $hwtype;
+ }
+ return $shw;
+ }
+
+ /**
+ * To be called by DatabaseFilter::whereClause() when building actual query.
+ * @param string $operator operator to use
+ * @param string[]|string $argument argument to compare against
+ * @param string[] $args assoc array to add parametrized version of $argument to
+ * @param string[] $joins any optional joins can be added to this array
+ * @return string where clause
+ */
+ public abstract function whereClause(string $operator, $argument, array &$args, array &$joins): string;
+
+ /**
+ * Called to get an instance of DatabaseFilter that binds the given $op and $argument to this filter.
+ * @param string[]|string $argument
+ */
+ public function bind(string $op, $argument): DatabaseFilter { return new DatabaseFilter($this, $op, $argument); }
+
+ /**
+ * Check if given $operator is valid for this filter. Throws error and halts if not.
+ * @return void
+ */
+ public final function validateOperator(string $operator)
+ {
+ if (empty($this->ops))
+ return;
+ if (!in_array($operator, $this->ops)) {
+ // Yes keep $this in this call, get_class() !== get_class($this)
+ ErrorHandler::traceError("Invalid op '$operator' for " . get_class($this) . '::' . $this->column);
+ }
+ }
+
+ /*
+ * Static/Helpers
+ */
public static function findBestValue($array, $value, $up)
{
@@ -40,478 +137,719 @@ class StatisticsFilter
return ($array[$best] + $array[$best - 1]) / 2;
}
- public static function getNewKey($colname)
+ public static function getNewKey($colname): string
{
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)
+ /**
+ * @return DatabaseFilter[]
+ */
+ public static function parseQuery(): array
{
- $key = StatisticsFilter::getNewKey($this->column);
- $addendum = '';
-
- /* check if we have to do some parsing*/
- if (self::$columns[$this->column]['type'] === 'date') {
- $args[$key] = strtotime($this->argument);
- } else {
- $args[$key] = $this->argument;
- if ($this->operator === '~' || $this->operator === '!~') {
- $args[$key] = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $args[$key]);
- $addendum = " ESCAPE '='";
- }
+ // Get current settings from GET
+ $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')),
+ ];
}
+ $filters = [];
+ foreach ($currentValues as $filterType => $data) {
+ if (!$data['filter'])
+ continue;
+ $operator = $data['op'];
+ $argument = $data['argument'];
- $op = $this->operator;
- if ($this->operator == '~') {
- $op = 'LIKE';
- } elseif ($this->operator == '!~') {
- $op = 'NOT LIKE';
+ if (array_key_exists($filterType, self::$columns)) {
+ $filters[$filterType] = self::$columns[$filterType]->bind($operator, $argument);
+ } else {
+ Message::addError('invalid-filter-key', $filterType);
+ }
}
- return 'm.' . $this->column . ' ' . $op . ' :' . $key . $addendum;
+ return $filters;
}
- /* parse a query into an array of filters */
- public static function parseQuery($query)
+ public static function renderFilterBox(string $show, StatisticsFilterSet $filterSet): void
{
- $operators = ['<=', '>=', '!=', '!~', '=', '~', '<', '>'];
- $filters = [];
- if (empty($query))
- return $filters;
- foreach (explode(self::DELIMITER, $query) as $q) {
- $q = trim($q);
- if (empty($q))
- continue;
- // Special case: User pasted UUID, turn into filter
- if (preg_match('/^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/', $q)) {
- $filters[] = new StatisticsFilter('machineuuid', '=', $q);
- continue;
- }
- // Special case: User pasted IP, turn into filter
- if (preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $q)) {
- $filters[] = new StatisticsFilter('clientip', '=', $q);
- continue;
- }
- /* find position of first operator */
- $pos = 10000;
- $operator = false;
- foreach ($operators as $op) {
- $newpos = strpos($q, $op);
- if ($newpos > -1 && ($newpos < $pos)) {
- $pos = $newpos;
- $operator = $op;
+ // Build location list, with permissions
+ if (Module::isAvailable('locations')) {
+ self::$columns['location']->filterLocations($filterSet->getAllowedLocations());
+ }
+ // Build column array for rendering
+ $columns = [];
+ $showCount = 0;
+ foreach (self::$columns as $key => $filter) {
+ $col = [
+ 'key' => $key,
+ 'name' => Dictionary::translateFile('filters', $key),
+ 'placeholder' => $filter->placeholder,
+ ];
+ $bind = $filterSet->hasFilterKey($key);
+ if ($filter->type() === 'int') {
+ $col['input'] = 'number';
+ } elseif ($filter->type() === 'string') {
+ $col['input'] = 'text';
+ } elseif ($filter->type() === 'date') {
+ $col['input'] = 'text';
+ $col['inputclass'] = 'is-date';
+ } elseif ($filter->type() === 'enum') {
+ $col['enum'] = true;
+ /** @var EnumStatisticsFilter $filter */
+ $col['values'] = $filter->values;
+ if ($bind !== null) {
+ // Current value from GET
+ foreach ($col['values'] as &$value) {
+ if ($value['key'] == $bind->argument) {
+ $value['selected'] = 'selected';
+ }
+ }
}
}
- if ($pos == 10000) {
- error_log("couldn't find operator in segment " . $q);
- /* TODO */
- continue;
+ // current value from GET
+ if ($bind !== null) {
+ $col['currentvalue'] = $bind->argument;
+ $col['checked'] = 'checked';
+ $showCount++;
+ } elseif (!isset($col['show']) || !$col['show']) {
+ $col['collapse'] = 'collapse';
}
- $lhs = trim(substr($q, 0, $pos));
- $rhs = trim(substr($q, $pos + strlen($operator)));
-
- if ($lhs === 'gbram') {
- $filters[] = new RamGbStatisticsFilter($operator, $rhs);
- } elseif ($lhs === 'runtime') {
- $filters[] = new RuntimeStatisticsFilter($operator, $rhs);
- } elseif ($lhs === 'state') {
- $filters[] = new StateStatisticsFilter($operator, $rhs);
- } elseif ($lhs === 'hddgb') {
- $filters[] = new Id44StatisticsFilter($operator, $rhs);
- } elseif ($lhs === 'location') {
- $filters[] = new LocationStatisticsFilter($operator, $rhs);
- } elseif ($lhs === 'subnet') {
- $filters[] = new SubnetStatisticsFilter($operator, $rhs);
- } else {
- if (array_key_exists($lhs, self::$columns) && self::$columns[$lhs]['column']) {
- $filters[] = new StatisticsFilter($lhs, $operator, $rhs);
- } else {
- Message::addError('invalid-filter-key', $lhs);
+ $col['op'] = $filter->ops;
+ foreach ($col['op'] as &$value) {
+ $value = ['op' => $value];
+ if ($bind !== null && $bind->op === $value['op']) {
+ $value['selected'] = 'selected';
}
}
+ $columns[$key] = $col;
+ }
+ if ($showCount < 2) {
+ unset($columns['clientip']['collapse']);
+ }
+ if ($showCount < 1) {
+ unset($columns['machineuuid']['collapse']);
}
+ $data = array(
+ 'show' => $show,
+ 'columns' => array_values($columns),
+ $show . 'ButtonClass' => 'active',
+ );
- return $filters;
+ Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']);
+ Render::addTemplate('filterbox', $data);
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- public static function renderFilterBox($show, $filterSet, $query)
+ public static function initConstants()
{
- $data = array(
- 'show' => $show,
- 'query' => $query,
- 'delimiter' => StatisticsFilter::DELIMITER,
- 'sortDirection' => $filterSet->getSortDirection(),
- 'sortColumn' => $filterSet->getSortColumn(),
- 'columns' => json_encode(StatisticsFilter::$columns),
- );
+ 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 MacAddressStatisticsFilter(),
+ 'firstseen' => new DateStatisticsFilter('firstseen', '2020-10-15 14:00'),
+ 'lastseen' => new DateStatisticsFilter('lastseen', '2020-10-15 14:00'),
+ 'lastboot' => new DateStatisticsFilter('lastboot', '2020-10-15 14:00'),
+ 'runtime' => new RuntimeStatisticsFilter(),
+ 'realcores' => new SimpleStatisticsFilter('realcores', self::OP_ORDINAL, ''),
+ 'systemmodel' => new SystemModelStatisticsFilter(),
+ 'cpumodel' => new SimpleStatisticsFilter('cpumodel', self::OP_STRCMP, 'Pentium Pro 200 MHz'),
+ 'hddgb' => new PartitionGbStatisticsFilter('id44mb'),
+ 'persistentgb' => new PartitionGbStatisticsFilter('id45mb'),
+ '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'),
+ 'live_id45free' => new SimpleNotZeroStatisticsFilter('live_id45free', self::OP_ORDINAL, 'MiB'),
+ 'standbycrash' => new StandbyCrashStatisticsFilter(),
+ 'pcidev' => new PciDeviceStatisticsFilter(),
+ 'nicspeed' => new NicSpeedStatisticsFilter(),
+ 'hddrpm' => new HddRpmStatisticsFilter(),
+ //'anydev' => new AnyHardwarePropStatisticsFilter(),
+ ];
+ if (Module::isAvailable('locations')) {
+ self::$columns['location'] = new LocationStatisticsFilter();
+ }
+ }
+
+}
+
+class SimpleStatisticsFilter extends StatisticsFilter
+{
- if ($show === 'list') {
- $data['listButtonClass'] = 'active';
- $data['statButtonClass'] = '';
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $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;
+ }
- $locsFlat = array();
- if (Module::isAvailable('locations')) {
- $allowed = $filterSet->getAllowedLocations();
- foreach (Location::getLocations() as $loc) {
- $locsFlat['L' . $loc['locationid']] = array(
- 'pad' => $loc['locationpad'],
- 'name' => $loc['locationname'],
- 'disabled' => $allowed !== false && !in_array($loc['locationid'], $allowed),
- );
- }
+}
+
+class SimpleNotZeroStatisticsFilter extends SimpleStatisticsFilter
+{
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $str = parent::whereClause($operator, $argument, $args, $joins);
+ if ((int)$argument !== 0 || $operator !== '=') {
+ $str = "($str AND {$this->column} != 0)";
}
+ return $str;
+ }
- Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']);
- $data['locations'] = json_encode($locsFlat);
- Render::addTemplate('filterbox', $data);
+}
+
+class EnumStatisticsFilter extends SimpleStatisticsFilter
+{
+
+ public $values;
+
+ public function __construct(string $column, array $values, array $ops = self::OP_NOMINAL)
+ {
+ parent::__construct($column, $ops, '');
+ 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;
}
- private static $query = false;
+ public function type(): string { return 'enum'; }
- public static function getQuery()
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
- if (self::$query === false) {
- self::$query = Request::any('filters', false, 'string');
- if (self::$query === false) {
- self::$query = 'lastseen > ' . gmdate('Y-m-d', strtotime('-30 day'));
+ if ($this->validateArgument()) {
+ $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 self::$query;
+ return parent::whereClause($operator, $argument, $args, $joins);
}
- /*
- * Simple filters that map directly to DB columns
- */
+ protected function validateArgument(): bool { return true; }
- 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()
+class StandbyCrashStatisticsFilter extends EnumStatisticsFilter
+{
+
+ public function __construct()
{
+ parent::__construct('standbysem', ['NONE', 'MANY']);
+ }
- self::$columns = [
- 'machineuuid' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'column' => true,
- ],
- 'macaddr' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'column' => true,
- ],
- 'firstseen' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'date',
- 'column' => true,
- ],
- 'lastseen' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'date',
- 'column' => true,
- ],
- 'logintime' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'date',
- 'column' => true,
- ],
- 'realcores' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'column' => true,
- ],
- 'systemmodel' => [
- 'op' => self::OP_STRCMP,
- 'type' => 'string',
- 'column' => true,
- ],
- 'cpumodel' => [
- 'op' => self::OP_STRCMP,
- 'type' => 'string',
- 'column' => true,
- ],
- 'hddgb' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'column' => false,
- 'map_sort' => 'id44mb'
- ],
- 'gbram' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'map_sort' => 'mbram',
- 'column' => false,
- ],
- 'kvmstate' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'enum',
- 'column' => true,
- 'values' => ['ENABLED', 'DISABLED', 'UNSUPPORTED']
- ],
- 'badsectors' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'column' => true
- ],
- 'clientip' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'column' => true
- ],
- 'hostname' => [
- 'op' => self::OP_STRCMP,
- 'type' => 'string',
- 'column' => true
- ],
- 'subnet' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'column' => false
- ],
- 'currentuser' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'column' => true
- ],
- 'state' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'enum',
- 'column' => true,
- 'values' => ['occupied', 'on', 'off', 'idle', 'standby']
- ],
- 'live_swapfree' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'column' => true
- ],
- 'live_memfree' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'column' => true
- ],
- 'live_tmpfree' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'column' => true
- ],
- ];
- if (Module::isAvailable('locations')) {
- self::$columns['location'] = [
- 'op' => self::OP_STRCMP,
- 'type' => 'enum',
- 'column' => false,
- 'values' => array_keys(Location::getLocationsAssoc()),
- ];
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ if ($argument === 'NONE') {
+ $argument = 0;
+ } else { // MANY
+ $argument = 3;
+ $operator = $operator === '=' ? '>' : '<=';
}
+ return parent::whereClause($operator, $argument, $args, $joins);
}
+ protected function validateArgument(): bool { return false; }
+
}
-class RamGbStatisticsFilter extends StatisticsFilter
+class DateStatisticsFilter 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)";
+
+ public function __construct(string $column, string $placeholder)
+ {
+ parent::__construct($column, self::OP_ORDINAL, $placeholder);
+ }
+
+ public function type(): string { return 'date'; }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $key = self::getNewKey($this->column);
+
+ if (!preg_match('/^(?<date>\d{4}-\d{2}-\d{2})(\s+(?<h>\d{1,2})(:(?<m>\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 {
- error_log("unimplemented operator in RamGbFilter: $this->operator");
+ $span = 'day';
+ }
- return ' 1';
+ $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;
}
+
}
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): string
{
- $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)";
+ if ($operator == '<') { // These are inverted (uptime vs lastboot)
+ return "$common lastboot > $upper";
+ } elseif ($operator == '<=') {
+ return "$common lastboot > $lower";
+ } elseif ($operator == '>') {
+ return "$common lastboot < $lower";
+ } elseif ($operator == '>=') {
+ return "$common lastboot < $upper";
+ } elseif ($operator == '=') {
+ return "$common (lastboot BETWEEN $lower AND $upper)";
+ }
+ // !=
+ return "$common (lastboot NOT BETWEEN $lower AND > $upper)";
+ }
+}
+
+abstract class GbToMbRangeStatisticsFilter extends StatisticsFilter
+{
+
+ protected function rangeClause(string $operator, $argument, array $fuzzyVals): string
+ {
+ if ($operator === '~' || $operator === '!~') {
+ $lower = (int)floor(StatisticsFilter::findBestValue($fuzzyVals, (int)$argument, false) * 1024 - 500);
+ $upper = (int)ceil(StatisticsFilter::findBestValue($fuzzyVals, (int)$argument, true) * 1024 + 100);
+ $operator = str_replace('~', '=', $operator);
} else {
- error_log("unimplemented operator in RuntimeFilter: $this->operator");
- return ' 1';
+ $lower = round($argument * 1024 - 500);
+ $upper = round($argument * 1024 + 1023);
}
+ if ($operator === '=')
+ return " {$this->column} BETWEEN $lower AND $upper";
+ if ($operator === '!=')
+ return " {$this->column} NOT BETWEEN $lower AND $upper";
+ if ($operator === '<')
+ return " {$this->column} < $lower";
+ if ($operator === '<=')
+ return " {$this->column} <= $upper";
+ if ($operator === '>')
+ return " {$this->column} > $upper";
+ return " {$this->column} >= $lower"; // >=
}
+
}
-class Id44StatisticsFilter extends StatisticsFilter
+class RamGbStatisticsFilter extends GbToMbRangeStatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct('id44mb', $operator, $argument);
+ parent::__construct('mbram', self::OP_FUZZY_ORDINAL, 'GiB');
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
- 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 parent::rangeClause($operator, $argument, self::SIZE_RAM);
+ }
- return ' 1';
- }
+}
+
+class PartitionGbStatisticsFilter extends GbToMbRangeStatisticsFilter
+{
+
+ public function __construct(string $column)
+ {
+ parent::__construct($column, self::OP_FUZZY_ORDINAL, 'GiB');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ return parent::rangeClause($operator, $argument, self::SIZE_PARTITION);
}
}
-class StateStatisticsFilter extends StatisticsFilter
+class StateStatisticsFilter extends EnumStatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct(null, $operator, $argument);
+ parent::__construct('state', ['on', 'off', 'idle', 'occupied', 'standby']);
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
$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);
- return ' 1';
}
+ 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, self::OP_LOCATIONS);
}
- public function whereClause(&$args, &$joins)
+ public function type(): string { return 'enum'; }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
- $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)) {
+ ErrorHandler::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));
+ } elseif ($argument == 0) {
+ return 'locationid IS ' . ($operator === '!=' ? 'NOT' : '') . ' NULL';
+ }
+ 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) : string
{
- $argument = preg_replace('/[^0-9\.:]/', '', $this->argument);
- return " clientip LIKE '$argument%'";
+ $argument = strtolower(preg_replace('#[^0-9a-f.:/*]#i', '', $argument));
+ if (filter_var($argument, FILTER_VALIDATE_IP) !== false) {
+ // Valid \o/ - do nothing to $argument
+ } elseif (strpos($argument, '/') !== false) {
+ // TODO: IPv6 CIDR
+ $range = IpUtil::parseCidr($argument);
+ if ($range === null) {
+ Message::addError('invalid-cidr-notion', $argument);
+ return '0';
+ }
+ return 'INET_ATON(clientip) BETWEEN ' . $range['start'] . ' AND ' . $range['end'];
+ } elseif (($num = substr_count($argument, ':')) !== 0 && $num <= 7) {
+ // TODO: Probably valid IPv6, not yet in DB
+ } elseif (($num = substr_count($argument, '.')) !== 0 && $num <= 3) {
+ if (substr($argument, -1) === '.') {
+ $argument .= '*';
+ } elseif ($num < 3) {
+ $argument .= '.*';
+ }
+ } else {
+ Message::addError('invalid-ip-address', $argument);
+ return '0';
+ }
+ $operator = $operator[0] === '!' ? 'NOT LIKE' : 'LIKE';
+ return "clientip $operator '" . 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): string
{
- if ($this->argument) {
- $joins[] = ' LEFT JOIN runmode USING (machineuuid)';
+ if ($argument) {
+ $joins[] = ' LEFT JOIN runmode ON (m.machineuuid = runmode.machineuuid)';
return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)";
}
- $joins[] = ' INNER JOIN runmode USING (machineuuid)';
+ $joins[] = ' INNER JOIN runmode ON (m.machineuuid = runmode.machineuuid)';
return "runmode.isclient = 0";
}
}
-StatisticsFilter::initConstants(); \ No newline at end of file
+class PciDeviceStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, ['='], 'vvvv[:dddd][,cccc]');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ // vendor[:device][,class]
+ if (!preg_match('/^(?<v>[0-9a-f]{4})(?::(?<d>[0-9a-f]{4}))?(?:,(?<c>[0-9a-f]{4}))?$/i', $argument, $out)) {
+ Message::addError('invalid-pciid', $argument);
+ return '0';
+ }
+ $vendor = $out['v'];
+ $device = $out['d'] ?? '';
+ $class = $out['c'] ?? '';
+ // basic join for hw_x_machine
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::PCI_DEVICE);
+ $_ = [];
+ $c = new HardwareQueryColumn(true, 'vendor');
+ $c->addCondition($operator, $vendor);
+ $c->generate($joins, $_, $args, [], $shw);
+ if (!empty($device)) {
+ $c = new HardwareQueryColumn(true, 'device');
+ $c->addCondition($operator, $device);
+ $c->generate($joins, $_, $args, [], $shw);
+ }
+ if (!empty($class)) {
+ $c = new HardwareQueryColumn(true, 'class');
+ $c->addCondition($operator, $class);
+ $c->generate($joins, $_, $args, [], $shw);
+ }
+ return '1';
+ }
+
+}
+
+class NicSpeedStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, StatisticsFilter::OP_ORDINAL, 'MBit/s');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::MAINBOARD);
+ $_ = [];
+ $c = new HardwareQueryColumn(false, 'nic-speed');
+ $c->addCondition($operator, $argument);
+ $c->generate($joins, $_, $args, [], $shw);
+ return '1';
+ }
+
+}
+
+class HddRpmStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, StatisticsFilter::OP_ORDINAL, '7200');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::HDD);
+ $_ = [];
+ $c = new HardwareQueryColumn(true, 'rotation_rate');
+ $c->addCondition($operator, $argument);
+ $c->generate($joins, $_, $args, [], $shw);
+ return '1';
+ }
+
+}
+
+class SystemModelStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, StatisticsFilter::OP_STRCMP, 'PC-365 (IBM)');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::DMI_SYSTEM);
+ $_ = [];
+ $manufacturer = null;
+ $model = $argument;
+ if (preg_match('/^(.*)\((.*)\)\s*$/', $model, $out)) {
+ $manufacturer = trim($out[2]);
+ $model = trim($out[1]);
+ }
+ $c = new HardwareQueryColumn(true, 'Product Name');
+ $c->addCondition($operator, $model);
+ $c->generate($joins, $_, $args, [], $shw);
+ if ($manufacturer !== null) {
+ $c = new HardwareQueryColumn(true, 'Manufacturer');
+ $c->addCondition($operator, $manufacturer);
+ $c->generate($joins, $_, $args, [], $shw);
+ }
+ return '1';
+ }
+
+}
+
+class MacAddressStatisticsFilter extends SimpleStatisticsFilter
+{
+ public function __construct()
+ {
+ parent::__construct('macaddr', self::OP_STRCMP, '11-22-33-44-55-66');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ // Allow just 12 hex digits, and convert ':' to '-', which we unfortunately settled on for the DB format
+ if (preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i',
+ $argument, $out)) {
+ $argument = $out[1] . '-' . $out[2] . '-' . $out[3] . '-' . $out[4] . '-' . $out[5] . '-' . $out[6];
+ } elseif (strpos($argument, ':') !== false) {
+ $argument = str_replace(':', '-', $argument);
+ }
+ return parent::whereClause($operator, $argument, $args, $joins);
+ }
+}
+
+class AnyHardwarePropStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, ['~']);
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins);
+ $val = self::getNewKey('val');
+ $key1 = self::getNewKey('hw');
+ $joins[] = "LEFT JOIN statistic_hw_prop $key1 ON (`$key1`.`value` LIKE :$val AND `$key1`.hwid = `$shw`.hwid)";
+ $key2 = self::getNewKey('hw');
+ $joins[] = "LEFT JOIN machine_x_hw_prop $key2 ON (`$key2`.`value` LIKE :$val AND `$key2`.machinehwid = mxhw.machinehwid)";
+ $args[$val] = '%' . str_replace(['%', '*'], ['_', '%'], $argument) . '%';
+ return "((`$key1`.`value` IS NOT NULL) OR (`$key2`.`value` IS NOT NULL))";
+ }
+
+}
+
+class DatabaseFilter
+{
+ /** @var StatisticsFilter
+ */
+ private $inst;
+ public $op;
+ public $argument;
+
+ /**
+ * Called by StatisticsFilter::bind().
+ */
+ public function __construct(StatisticsFilter $inst, string $op, $argument)
+ {
+ $inst->validateOperator($op);
+ $this->inst = $inst;
+ $this->op = $op;
+ $this->argument = $argument;
+ }
+
+ /**
+ * Called from StatisticsFilterSet::makeFragments() to build the final query.
+ */
+ public function whereClause(array &$args, array &$joins): string
+ {
+ return $this->inst->whereClause($this->op, $this->argument, $args, $joins);
+ }
+
+ public function isClass(string $what): bool
+ {
+ return get_class($this->inst) === $what;
+ }
+
+ public function getClass(): string
+ {
+ return get_class($this->inst);
+ }
+
+}
+
+StatisticsFilter::initConstants();