summaryrefslogblamecommitdiffstats
path: root/modules-available/statistics/inc/statisticsfilter.inc.php
blob: 5e6448c77ce3463d091df9ac815bddf7b8f8d51f (plain) (tree)
1
2
3
4
5
6
7
8
9



                                                                                            
 
                               
 
           
                                                                                
           
                                       
 
                                                                                                                                 


                                                                                                                          
 









                                                             
                                              
                                                                              





                                  



                               


                                                                                     
                       
 















                                                                                  
                                      


                                                                                                                     
 
           


















                                                                                                                                   






                                                                                        
                                                                                                               
 

                                                                                                             
                                           
           
                                                                                                                         
 



                                                                                            




                                                                
                                                                                        
                                                                                                                          






                         




















                                                                                     
                                                          



                                                              


                                   
                                                  
         
                                                












                                                                                    














                                                                                                                

         
                                                                                                  
         
                                                        
                                                       
                                                                                                       
                 

                                                   



                                                             
                                                                                     



                                                                      
                                                         
                                                                 
                                                       
                                                               

                                                               
                                                               
                                                    
                                                                        
                                                                 
                                                     

                                                                             
                                                                                       





                                                                                        
                                             


                                                                       


                                                                          
                                                  

                                                          
                                                                                   


                                                                        






                                                                   
                 

                                        

                                                            

                  









                                                                                                                                       
                                                                      

                                                                                                 

                                                                                               
                                                                                                     
                                                                           
                                                                                                                     

                                                                                    







                                                                                                                   
                                                                                                                       
                                                                             
                                                                    
                                                                     
                                                                 
                                                                            










                                                                                    
                                                                                                     





                                                      
                                                   



                                               
                        









                                                                                                                                          

                 

                                                                                          
 

 













                                                                                                     




                                                         
                                                                                                 
         
                                                       













                                                                           

         
                                                         
 
                                                                                                     
         







                                                                              
                                 





                                                                                                 
                         


                                                                                
 

                                                                    

 







                                                                    
                                                                                                     









                                                                                

                                                                     

 



                                                                        
         

                                                                             
 
                                                         
 
                                                                                                     

                                                      



                                                                                                                              
                 



















                                                                                                                      
                                                                            

         

 
                                                      
 

                                     
         
                                                                  

         
                                                                                                     
         




                                                                                  
                                              
                                                           
                                             
                                                           
                                              


                                                                              
                 
                     






                                                                            
                                                                                             



















                                                                                                                             
         
 
 
 
                                                               
 

                                     
         
                                                                            

         
                                                                                                     
         
                                                                                 
         
 

 
                                                                     
 
 
                                                   
         
                                                                            

         
                                                                                                     
         
                                                                                       
         
 
 
                                                        
 

                                     
         
                                                                                           

         
                                                                                                     
         
                                                                                                                                                        
                                                        
                                                        
                                                                          
                                                      
                                                             
                 

                                                                                 
         

 
                                                           
 

                                     
         






                                                                                            
                                                                             

         
                                                         
 
                                                                                                     
         

                                                             
 
                                                        
                                                                                                  
                 

                                                                                      

                                                                                              










                                                                                
                         

                 

 
                                                 
 

                                     
         
                                                                                                

         
                                                                                                      
         




                                                                                        
                                                              
                                              



                                                                                                          
                                                                                     
                                                                   




                                                                                     



                                                                           
                 

                                                                                       
         

 
                                                       
 

                                     
         
                                              

         
                                                                                                     
         
                                
                                                                                                 

                                                                                     
                                                                                          

                                              
 
 
 




                                                        
                                                                       



                                                                                                     

                                                                                                                           


                                                                      


                                          
                                              
                                                                                                  


                                                             
                                                          


                                                                     





                                                                    





                           




                                                       
                                                                                  













                                                                                                     



















                                                                                                     






























                                                                                                     



















                                                                                                                            




                                                              
                                                 









                                                                                                                                          
                                                                                         




                                                                                          






                                 
 


                                              

                                                                                  
                                             



                                            


                                                                                     

                                                                        



                                                                                           
                                                   



                                                        




                                              

 
                                  
<?php

/* base class with rudimentary SQL generation abilities.
 * WARNING: argument is escaped, but $column and $operator are passed unfiltered into SQL */

abstract class StatisticsFilter
{
	/**
	 * Legacy delimiter for js_selectize filters - used to redirect old URLs
	 */
	const LEGACY_DELIMITER = '~,~';

	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;

	/**
	 * @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)
	{
		$best = 0;
		for ($i = 0; $i < count($array); ++$i) {
			if (abs($array[$i] - $value) < abs($array[$best] - $value)) {
				$best = $i;
			}
		}
		if (!$up && $best === 0) {
			return $array[0];
		}
		if ($up && $best + 1 === count($array)) {
			return $array[$best];
		}
		if ($up) {
			return ($array[$best] + $array[$best + 1]) / 2;
		}

		return ($array[$best] + $array[$best - 1]) / 2;
	}

	public static function getNewKey($colname): string
	{
		return $colname . '_' . (self::$keyCounter++);
	}

	/**
	 * @return DatabaseFilter[]
	 */
	public static function parseQuery(): array
	{
		// 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'];

			if (array_key_exists($filterType, self::$columns)) {
				$filters[$filterType] = self::$columns[$filterType]->bind($operator, $argument);
			} else {
				Message::addError('invalid-filter-key', $filterType);
			}
		}

		return $filters;
	}

	public static function renderFilterBox(string $show, StatisticsFilterSet $filterSet): void
	{
		// 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';
						}
					}
				}
			}
			// current value from GET
			if ($bind !== null) {
				$col['currentvalue'] = $bind->argument;
				$col['checked'] = 'checked';
				$showCount++;
			} elseif (!isset($col['show']) || !$col['show']) {
				$col['collapse'] = 'collapse';
			}
			$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',
		);

		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 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
{

	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 {
			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;
	}

}

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;
	}

}

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;
	}

	public function type(): string { return 'enum'; }

	public function whereClause(string $operator, $argument, array &$args, array &$joins): string
	{
		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 parent::whereClause($operator, $argument, $args, $joins);
	}

	protected function validateArgument(): bool { return true; }

}

class StandbyCrashStatisticsFilter extends EnumStatisticsFilter
{

	public function __construct()
	{
		parent::__construct('standbysem', ['NONE', 'MANY']);
	}

	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 DateStatisticsFilter extends StatisticsFilter
{

	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 {
			$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;
	}

}

class RuntimeStatisticsFilter extends StatisticsFilter
{

	public function __construct()
	{
		parent::__construct('lastboot', self::OP_ORDINAL);
	}

	public function whereClause(string $operator, $argument, array &$args, array &$joins): string
	{
		$upper = time() - (int)$argument * 3600;
		$lower = $upper - 3600;
		$common = "state IN ('OCCUPIED', 'IDLE', 'STANDBY') AND";
		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 {
			$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 RamGbStatisticsFilter extends GbToMbRangeStatisticsFilter
{

	public function __construct()
	{
		parent::__construct('mbram', self::OP_FUZZY_ORDINAL, 'GiB');
	}

	public function whereClause(string $operator, $argument, array &$args, array &$joins): string
	{
		return parent::rangeClause($operator, $argument, self::SIZE_RAM);
	}

}

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 EnumStatisticsFilter
{

	public function __construct()
	{
		parent::__construct('state', ['on', 'off', 'idle', 'occupied', 'standby']);
	}

	public function whereClause(string $operator, $argument, array &$args, array &$joins): string
	{
		$map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ];
		$neg = $operator === '!=' ? 'NOT ' : '';
		if (array_key_exists($argument, $map)) {
			$key = StatisticsFilter::getNewKey($this->column);
			$args[$key] = $map[$argument];
			return " m.state $neg IN ( :$key ) ";
		}
		Message::addError('invalid-filter-argument', 'state', $argument);
		return ' 1';
	}
}

class LocationStatisticsFilter extends EnumStatisticsFilter
{

	public function __construct()
	{
		$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 type(): string { return 'enum'; }

	public function whereClause(string $operator, $argument, array &$args, array &$joins): string
	{
		$recursive = (substr($operator, -1) === '~');
		$operator = str_replace('~', '=', $operator);

		if ($recursive && is_array($argument)) {
			ErrorHandler::traceError('Cannot use ~ operator for location with array');
		}
		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';
			}
		}
	}
}

class IpStatisticsFilter extends StatisticsFilter
{

	public function __construct()
	{
		parent::__construct('clientip', self::OP_NOMINAL, '1.2.3.4, 1.2.3.*, 1.2.3/24');
	}

	public function whereClause(string $operator, $argument, array &$args, array &$joins) : string
	{
		$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()
	{
		parent::__construct(null, []);
	}

	public function whereClause(string $operator, $argument, array &$args, array &$joins): string
	{
		if ($argument) {
			$joins[] = ' LEFT JOIN runmode ON (m.machineuuid = runmode.machineuuid)';
			return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)";
		}
		$joins[] = ' INNER JOIN runmode ON (m.machineuuid = runmode.machineuuid)';
		return "runmode.isclient = 0";
	}

}

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();