diff options
Diffstat (limited to 'library/Icinga/Data/Filter')
19 files changed, 1407 insertions, 0 deletions
diff --git a/library/Icinga/Data/Filter/Filter.php b/library/Icinga/Data/Filter/Filter.php new file mode 100644 index 0000000..f5d8bdf --- /dev/null +++ b/library/Icinga/Data/Filter/Filter.php @@ -0,0 +1,255 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +use Icinga\Web\UrlParams; +use Icinga\Exception\ProgrammingError; + +/** + * Filter + * + * Base class for filters (why?) and factory for the different FilterOperators + */ +abstract class Filter +{ + protected $id = '1'; + + public function setId($id) + { + $this->id = (string) $id; + return $this; + } + + abstract public function isExpression(); + + abstract public function isChain(); + + abstract public function isEmpty(); + + abstract public function toQueryString(); + + abstract public function andFilter(Filter $filter); + + abstract public function orFilter(Filter $filter); + + /** + * Whether the give row matches this Filter + * + * @param mixed $row Preferrably an stdClass instance + * @return bool + */ + abstract public function matches($row); + + public function getUrlParams() + { + return UrlParams::fromQueryString($this->toQueryString()); + } + + public function getById($id) + { + if ((string) $id === $this->getId()) { + return $this; + } + throw new ProgrammingError( + 'Trying to get invalid filter index "%s" from "%s" ("%s")', + $id, + $this, + $this->id + ); + } + + public function getId() + { + return $this->id; + } + + public function isRootNode() + { + return false === strpos($this->id, '-'); + } + + abstract public function listFilteredColumns(); + + public function applyChanges($changes) + { + $filter = $this; + $pairs = array(); + foreach ($changes as $k => $v) { + if (preg_match('/^(column|value|sign|operator)_([\d-]+)$/', $k, $m)) { + $pairs[$m[2]][$m[1]] = $v; + } + } + $operators = array(); + foreach ($pairs as $id => $fs) { + if (array_key_exists('operator', $fs)) { + $operators[$id] = $fs['operator']; + } else { + $f = $filter->getById($id); + $f->setColumn($fs['column']); + if ($f->getSign() !== $fs['sign']) { + if ($f->isRootNode()) { + $filter = $f->setSign($fs['sign']); + } else { + $filter->replaceById($id, $f->setSign($fs['sign'])); + } + } + $f->setExpression($fs['value']); + } + } + + krsort($operators, SORT_NATURAL); + foreach ($operators as $id => $operator) { + $f = $filter->getById($id); + if ($f->getOperatorName() !== $operator) { + if ($f->isRootNode()) { + $filter = $f->setOperatorName($operator); + } else { + $filter->replaceById($id, $f->setOperatorName($operator)); + } + } + } + + return $filter; + } + + public function getParentId() + { + if ($this->isRootNode()) { + throw new ProgrammingError('Filter root nodes have no parent'); + } + return substr($this->id, 0, strrpos($this->id, '-')); + } + + public function getParent() + { + return $this->getById($this->getParentId()); + } + + public function hasId($id) + { + if ($id === $this->getId()) { + return true; + } + return false; + } + + /** + * Where Filter factory + * + * @param string $col Column to be filtered + * @param string $filter Filter expression + * + * @throws FilterException + * @return FilterExpression + */ + public static function where($col, $filter) + { + return new FilterExpression($col, '=', $filter); + } + + public static function expression($col, $op, $expression) + { + switch ($op) { + case '=': + return new FilterMatch($col, $op, $expression); + case '<': + return new FilterLessThan($col, $op, $expression); + case '>': + return new FilterGreaterThan($col, $op, $expression); + case '>=': + return new FilterEqualOrGreaterThan($col, $op, $expression); + case '<=': + return new FilterEqualOrLessThan($col, $op, $expression); + case '!=': + return new FilterMatchNot($col, $op, $expression); + default: + throw new ProgrammingError( + 'There is no such filter sign: %s', + $op + ); + } + } + + /** + * Or FilterOperator factory + * + * @param Filter $filter,... Unlimited optional list of Filters + * + * @return FilterOr + */ + public static function matchAny() + { + $args = func_get_args(); + if (count($args) === 1 && is_array($args[0])) { + $args = $args[0]; + } + return new FilterOr($args); + } + + /** + * Or FilterOperator factory + * + * @param Filter $filter,... Unlimited optional list of Filters + * + * @return FilterAnd + */ + public static function matchAll() + { + $args = func_get_args(); + if (count($args) === 1 && is_array($args[0])) { + $args = $args[0]; + } + return new FilterAnd($args); + } + + /** + * FilterNot factory, negates the given filter + * + * @param Filter $filter Filter to be negated + * + * @return FilterNot + */ + public static function not() + { + $args = func_get_args(); + if (count($args) === 1) { + if (is_array($args[0])) { + $args = $args[0]; + } + } + if (count($args) > 1) { + return new FilterNot(array(new FilterAnd($args))); + } else { + return new FilterNot($args); + } + } + + public static function chain($operator, $filters = array()) + { + switch ($operator) { + case 'AND': + return self::matchAll($filters); + case 'OR': + return self::matchAny($filters); + case 'NOT': + return self::not($filters); + } + throw new ProgrammingError( + '"%s" is not a valid filter chain operator', + $operator + ); + } + + /** + * Create filter from queryString + * + * This is still pretty basic, need improvement + * + * @return static + */ + public static function fromQueryString($query) + { + return FilterQueryString::parse($query); + } +} diff --git a/library/Icinga/Data/Filter/FilterAnd.php b/library/Icinga/Data/Filter/FilterAnd.php new file mode 100644 index 0000000..96b68cc --- /dev/null +++ b/library/Icinga/Data/Filter/FilterAnd.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +/** + * Filter list AND + * + * Binary AND, all contained filters must succeed + */ +class FilterAnd extends FilterChain +{ + protected $operatorName = 'AND'; + + protected $operatorSymbol = '&'; + + /** + * Whether the given row object matches this filter + * + * @object $row + * @return boolean + */ + public function matches($row) + { + foreach ($this->filters as $filter) { + if (! $filter->matches($row)) { + return false; + } + } + return true; + } + + public function andFilter(Filter $filter) + { + return $this->addFilter($filter); + } + + public function orFilter(Filter $filter) + { + return Filter::matchAny($this, $filter); + } +} diff --git a/library/Icinga/Data/Filter/FilterChain.php b/library/Icinga/Data/Filter/FilterChain.php new file mode 100644 index 0000000..0f1e071 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterChain.php @@ -0,0 +1,286 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +use Icinga\Exception\ProgrammingError; +use Icinga\Exception\QueryException; + +/** + * FilterChain + * + * A FilterChain contains a list ... + */ +abstract class FilterChain extends Filter +{ + protected $filters = array(); + + protected $operatorName; + + protected $operatorSymbol; + + protected $allowedColumns; + + /** + * Set the filters + * + * @param array $filters + * + * @return $this + */ + public function setFilters(array $filters) + { + $this->filters = $filters; + + $this->refreshChildIds(); + + return $this; + } + + public function hasId($id) + { + foreach ($this->filters() as $filter) { + if ($filter->hasId($id)) { + return true; + } + } + return parent::hasId($id); + } + + public function getById($id) + { + foreach ($this->filters() as $filter) { + if ($filter->hasId($id)) { + return $filter->getById($id); + } + } + return parent::getById($id); + } + + public function removeId($id) + { + if ($id === $this->getId()) { + $this->filters = array(); + return $this; + } + $remove = null; + foreach ($this->filters as $key => $filter) { + if ($filter->getId() === $id) { + $remove = $key; + } elseif ($filter instanceof FilterChain) { + $filter->removeId($id); + } + } + if ($remove !== null) { + unset($this->filters[$remove]); + $this->filters = array_values($this->filters); + } + $this->refreshChildIds(); + return $this; + } + + public function replaceById($id, $filter) + { + $found = false; + foreach ($this->filters as $k => $child) { + if ($child->getId() == $id) { + $this->filters[$k] = $filter; + $found = true; + break; + } + if ($child->hasId($id)) { + $child->replaceById($id, $filter); + $found = true; + break; + } + } + if (! $found) { + throw new ProgrammingError('You tried to replace an unexistant child filter'); + } + $this->refreshChildIds(); + return $this; + } + + protected function refreshChildIds() + { + $i = 0; + $id = $this->getId(); + foreach ($this->filters as $filter) { + $i++; + $filter->setId($id . '-' . $i); + } + return $this; + } + + public function setId($id) + { + return parent::setId($id)->refreshChildIds(); + } + + public function getOperatorName() + { + return $this->operatorName; + } + + public function setOperatorName($name) + { + if ($name !== $this->operatorName) { + return Filter::chain($name, $this->filters); + } + return $this; + } + + public function getOperatorSymbol() + { + return $this->operatorSymbol; + } + + public function setAllowedFilterColumns(array $columns) + { + $this->allowedColumns = $columns; + return $this; + } + + /** + * List and return all column names referenced in this filter + * + * @param array $columns The columns listed so far + * + * @return array + */ + public function listFilteredColumns(array $columns = array()) + { + foreach ($this->filters as $filter) { + if ($filter instanceof FilterExpression) { + $column= $filter->getColumn(); + if (! in_array($column, $columns, true)) { + $columns[] = $column; + } + } else { + $columns = $filter->listFilteredColumns($columns); + } + } + + return $columns; + } + + public function toQueryString() + { + $parts = array(); + if (empty($this->filters)) { + return ''; + } + foreach ($this->filters() as $filter) { + if (! $filter->isEmpty()) { + $parts[] = $filter->toQueryString(); + } + } + + // TODO: getLevel?? + if (strpos($this->getId(), '-')) { + return '(' . implode($this->getOperatorSymbol(), $parts) . ')'; + } else { + return implode($this->getOperatorSymbol(), $parts); + } + } + + /** + * Get simple string representation + * + * Useful for debugging only + * + * @return string + */ + public function __toString() + { + if (empty($this->filters)) { + return ''; + } + $parts = array(); + foreach ($this->filters as $filter) { + if ($filter instanceof FilterChain) { + $parts[] = '(' . $filter . ')'; + } else { + $parts[] = (string) $filter; + } + } + $op = ' ' . $this->getOperatorSymbol() . ' '; + return implode($op, $parts); + } + + public function __construct($filters = array()) + { + foreach ($filters as $filter) { + $this->addFilter($filter); + } + } + + public function isExpression() + { + return false; + } + + public function isChain() + { + return true; + } + + public function isEmpty() + { + return empty($this->filters); + } + + public function addFilter(Filter $filter) + { + if (! empty($this->allowedColumns)) { + $this->validateFilterColumns($filter); + } + + $this->filters[] = $filter; + $filter->setId($this->getId() . '-' . $this->count()); + return $this; + } + + protected function validateFilterColumns(Filter $filter) + { + if ($filter->isExpression()) { + $valid = false; + foreach ($this->allowedColumns as $column) { + if (is_callable($column)) { + if (call_user_func($column, $filter->getColumn())) { + $valid = true; + break; + } + } elseif ($filter->getColumn() === $column) { + $valid = true; + break; + } + } + + if (! $valid) { + throw new QueryException('Invalid filter column provided: %s', $filter->getColumn()); + } + } else { + foreach ($filter->filters() as $subFilter) { + $this->validateFilterColumns($subFilter); + } + } + } + + public function &filters() + { + return $this->filters; + } + + public function count() + { + return count($this->filters); + } + + public function __clone() + { + foreach ($this->filters as & $filter) { + $filter = clone $filter; + } + } +} diff --git a/library/Icinga/Data/Filter/FilterEqual.php b/library/Icinga/Data/Filter/FilterEqual.php new file mode 100644 index 0000000..da53d3f --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqual.php @@ -0,0 +1,16 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterEqual extends FilterExpression +{ + public function matches($row) + { + if (! isset($row->{$this->column})) { + return false; + } + + return (string) $row->{$this->column} === (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php new file mode 100644 index 0000000..d7bd5b8 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php @@ -0,0 +1,16 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterEqualOrGreaterThan extends FilterExpression +{ + public function matches($row) + { + if (! isset($row->{$this->column})) { + return false; + } + + return (string) $row->{$this->column} >= (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterEqualOrLessThan.php b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php new file mode 100644 index 0000000..8016fc4 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php @@ -0,0 +1,26 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterEqualOrLessThan extends FilterExpression +{ + public function __toString() + { + return $this->column . ' <= ' . $this->expression; + } + + public function toQueryString() + { + return $this->column . '<=' . $this->expression; + } + + public function matches($row) + { + if (! isset($row->{$this->column})) { + return false; + } + + return (string) $row->{$this->column} <= (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterException.php b/library/Icinga/Data/Filter/FilterException.php new file mode 100644 index 0000000..842d7ab --- /dev/null +++ b/library/Icinga/Data/Filter/FilterException.php @@ -0,0 +1,15 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +use Icinga\Exception\IcingaException; + +/** + * Filter Exception Class + * + * Filter Exceptions should be thrown on filter parse errors or similar + */ +class FilterException extends IcingaException +{ +} diff --git a/library/Icinga/Data/Filter/FilterExpression.php b/library/Icinga/Data/Filter/FilterExpression.php new file mode 100644 index 0000000..73fb625 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterExpression.php @@ -0,0 +1,224 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +use Exception; + +class FilterExpression extends Filter +{ + protected $column; + protected $sign; + protected $expression; + + /** + * Does this filter compare case sensitive? + * + * @var bool + */ + protected $caseSensitive; + + public function __construct($column, $sign, $expression) + { + $column = trim($column); + $this->column = $column; + $this->sign = $sign; + $this->expression = $expression; + $this->caseSensitive = true; + } + + public function isExpression() + { + return true; + } + + public function isChain() + { + return false; + } + + public function isEmpty() + { + return false; + } + + public function getColumn() + { + return $this->column; + } + + public function getSign() + { + return $this->sign; + } + + public function setColumn($column) + { + $this->column = $column; + return $this; + } + + public function getExpression() + { + return $this->expression; + } + + /** + * Return whether this filter compares case sensitive + * + * @return bool + */ + public function getCaseSensitive() + { + return $this->caseSensitive; + } + + public function setExpression($expression) + { + $this->expression = $expression; + return $this; + } + + public function setSign($sign) + { + if ($sign !== $this->sign) { + return Filter::expression($this->column, $sign, $this->expression); + } + return $this; + } + + /** + * Set this filter's case sensitivity + * + * @param bool $caseSensitive + * + * @return $this + */ + public function setCaseSensitive($caseSensitive = true) + { + $this->caseSensitive = $caseSensitive; + return $this; + } + + public function listFilteredColumns() + { + return array($this->getColumn()); + } + + public function __toString() + { + if ($this->isBooleanTrue()) { + return $this->column; + } + + $expression = is_array($this->expression) ? + '( ' . implode(' | ', $this->expression) . ' )' : + $this->expression; + + return sprintf( + '%s %s %s', + $this->column, + $this->sign, + $expression + ); + } + + public function toQueryString() + { + if ($this->isBooleanTrue()) { + return $this->column; + } + + $expression = is_array($this->expression) ? + '(' . implode('|', array_map('rawurlencode', $this->expression)) . ')' : + rawurlencode($this->expression); + + return $this->column . $this->sign . $expression; + } + + protected function isBooleanTrue() + { + return $this->sign === '=' && $this->expression === true; + } + + /** + * If $var is a scalar, do the same as strtolower() would do. + * If $var is an array, map $this->strtolowerRecursive() to its elements. + * Otherwise, return $var unchanged. + * + * @param mixed $var + * + * @return mixed + */ + protected function strtolowerRecursive($var) + { + if ($var === null) { + return ''; + } + if (is_scalar($var)) { + return strtolower($var); + } + if (is_array($var)) { + return array_map(array($this, 'strtolowerRecursive'), $var); + } + return $var; + } + + public function matches($row) + { + try { + $rowValue = $row->{$this->column}; + } catch (Exception $e) { + // TODO: REALLY? Exception? + return false; + } + + if ($this->caseSensitive) { + $expression = $this->expression; + } else { + $rowValue = $this->strtolowerRecursive($rowValue); + $expression = $this->strtolowerRecursive($this->expression); + } + + if (is_array($expression)) { + return in_array($rowValue, $expression); + } + + $expression = (string) $expression; + if (strpos($expression, '*') === false) { + if (is_array($rowValue)) { + return in_array($expression, $rowValue); + } + + return (string) $rowValue === $expression; + } + + $parts = array(); + foreach (preg_split('~\*~', $expression) as $part) { + $parts[] = preg_quote($part, '/'); + } + $pattern = '/^' . implode('.*', $parts) . '$/'; + + if (is_array($rowValue)) { + foreach ($rowValue as $candidate) { + if (preg_match($pattern, $candidate)) { + return true; + } + } + + return false; + } + + return $rowValue !== null && preg_match($pattern, $rowValue); + } + + public function andFilter(Filter $filter) + { + return Filter::matchAll($this, $filter); + } + + public function orFilter(Filter $filter) + { + return Filter::matchAny($this, $filter); + } +} diff --git a/library/Icinga/Data/Filter/FilterGreaterThan.php b/library/Icinga/Data/Filter/FilterGreaterThan.php new file mode 100644 index 0000000..92a0e62 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterGreaterThan.php @@ -0,0 +1,16 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterGreaterThan extends FilterExpression +{ + public function matches($row) + { + if (! isset($row->{$this->column})) { + // TODO: REALLY? Exception? + return false; + } + return (string) $row->{$this->column} > (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterLessThan.php b/library/Icinga/Data/Filter/FilterLessThan.php new file mode 100644 index 0000000..c13a1ce --- /dev/null +++ b/library/Icinga/Data/Filter/FilterLessThan.php @@ -0,0 +1,26 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterLessThan extends FilterExpression +{ + public function __toString() + { + return $this->column . ' < ' . $this->expression; + } + + public function toQueryString() + { + return $this->column . '<' . $this->expression; + } + + public function matches($row) + { + if (! isset($row->{$this->column})) { + return false; + } + + return (string) $row->{$this->column} < (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterMatch.php b/library/Icinga/Data/Filter/FilterMatch.php new file mode 100644 index 0000000..a3befad --- /dev/null +++ b/library/Icinga/Data/Filter/FilterMatch.php @@ -0,0 +1,8 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterMatch extends FilterExpression +{ +} diff --git a/library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php b/library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php new file mode 100644 index 0000000..9eca173 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php @@ -0,0 +1,13 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterMatchCaseInsensitive extends FilterMatch +{ + public function __construct($column, $sign, $expression) + { + parent::__construct($column, $sign, $expression); + $this->caseSensitive = false; + } +} diff --git a/library/Icinga/Data/Filter/FilterMatchNot.php b/library/Icinga/Data/Filter/FilterMatchNot.php new file mode 100644 index 0000000..1e5050e --- /dev/null +++ b/library/Icinga/Data/Filter/FilterMatchNot.php @@ -0,0 +1,12 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterMatchNot extends FilterExpression +{ + public function matches($row) + { + return !parent::matches($row); + } +} diff --git a/library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php b/library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php new file mode 100644 index 0000000..3838fa2 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php @@ -0,0 +1,13 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterMatchNotCaseInsensitive extends FilterMatchNot +{ + public function __construct($column, $sign, $expression) + { + parent::__construct($column, $sign, $expression); + $this->caseSensitive = false; + } +} diff --git a/library/Icinga/Data/Filter/FilterNot.php b/library/Icinga/Data/Filter/FilterNot.php new file mode 100644 index 0000000..b61f497 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterNot.php @@ -0,0 +1,58 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterNot extends FilterChain +{ + protected $operatorName = 'NOT'; + + protected $operatorSymbol = '!'; // BULLSHIT + +// TODO: Max count 1 or autocreate sub-and? + + public function matches($row) + { + foreach ($this->filters() as $filter) { + if ($filter->matches($row)) { + return false; + } + } + return true; + } + + public function andFilter(Filter $filter) + { + return Filter::matchAll($this, $filter); + } + + public function orFilter(Filter $filter) + { + return Filter::matchAny($filter); + } + + public function toQueryString() + { + $parts = array(); + if (empty($this->filters)) { + return ''; + } + + foreach ($this->filters() as $filter) { + $parts[] = $filter->toQueryString(); + } + if (count($parts) === 1) { + return '!' . $parts[0]; + } else { + return '!(' . implode('&', $parts) . ')'; + } + } + + public function __toString() + { + if (count($this->filters) === 1) { + return '! ' . $this->filters[0]; + } + return '! (' . implode('&', $this->filters) . ')'; + } +} diff --git a/library/Icinga/Data/Filter/FilterNotEqual.php b/library/Icinga/Data/Filter/FilterNotEqual.php new file mode 100644 index 0000000..8915a3d --- /dev/null +++ b/library/Icinga/Data/Filter/FilterNotEqual.php @@ -0,0 +1,12 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterNotEqual extends FilterExpression +{ + public function matches($row) + { + return (string) $row->{$this->column} !== (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterOr.php b/library/Icinga/Data/Filter/FilterOr.php new file mode 100644 index 0000000..aca91f3 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterOr.php @@ -0,0 +1,39 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterOr extends FilterChain +{ + protected $operatorName = 'OR'; + + protected $operatorSymbol = '|'; + + public function matches($row) + { + foreach ($this->filters as $filter) { + if ($filter->matches($row)) { + return true; + } + } + return false; + } + + public function setOperatorName($name) + { + if ($this->count() > 1 && $name === 'NOT') { + return Filter::not(clone $this); + } + return parent::setOperatorName($name); + } + + public function andFilter(Filter $filter) + { + return Filter::matchAll($this, $filter); + } + + public function orFilter(Filter $filter) + { + return $this->addFilter($filter); + } +} diff --git a/library/Icinga/Data/Filter/FilterParseException.php b/library/Icinga/Data/Filter/FilterParseException.php new file mode 100644 index 0000000..f2b732b --- /dev/null +++ b/library/Icinga/Data/Filter/FilterParseException.php @@ -0,0 +1,10 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +use Icinga\Exception\IcingaException; + +class FilterParseException extends IcingaException +{ +} diff --git a/library/Icinga/Data/Filter/FilterQueryString.php b/library/Icinga/Data/Filter/FilterQueryString.php new file mode 100644 index 0000000..8535df5 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterQueryString.php @@ -0,0 +1,320 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Data\Filter; + +class FilterQueryString +{ + protected $string; + + protected $pos; + + protected $debug = array(); + + protected $reportDebug = false; + + protected $length; + + protected function __construct() + { + } + + protected function debug($msg, $level = 0, $op = null) + { + if ($op === null) { + $op = 'NULL'; + } + $this->debug[] = sprintf( + '%s[%d=%s] (%s): %s', + str_repeat('* ', $level), + $this->pos, + $this->string[$this->pos - 1], + $op, + $msg + ); + } + + public static function parse($string) + { + $parser = new static(); + return $parser->parseQueryString($string); + } + + protected function readNextKey() + { + $str = $this->readUnlessSpecialChar(); + + if ($str === false) { + return $str; + } + return rawurldecode($str); + } + + protected function readNextValue() + { + if ($this->nextChar() === '(') { + $this->readChar(); + $var = preg_split('~\|~', $this->readUnless(')')); + if ($this->readChar() !== ')') { + $this->parseError(null, 'Expected ")"'); + } + } else { + $var = rawurldecode($this->readUnless(array(')', '&', '|', '>', '<'))); + } + return $var; + } + + protected function readNextExpression() + { + if ('' === ($key = $this->readNextKey())) { + return false; + } + + foreach (array('<', '>') as $sign) { + if (false !== ($pos = strpos($key, $sign))) { + if ($this->nextChar() === '=') { + break; + } + $var = substr($key, $pos + 1); + $key = substr($key, 0, $pos); + + if (ctype_digit($var)) { + $var = (float) $var; + } + + return Filter::expression($key, $sign, $var); + } + } + if (in_array($this->nextChar(), array('=', '>', '<', '!'))) { + $sign = $this->readChar(); + } else { + $sign = false; + } + if ($sign === false) { + return Filter::expression($key, '=', true); + } + + $toFloat = false; + if ($sign === '=') { + $last = substr($key, -1); + if ($last === '>' || $last === '<') { + $sign = $last . $sign; + $key = substr($key, 0, -1); + $toFloat = true; + } + // TODO: Same as above for unescaped <> - do we really need this? + } elseif ($sign === '>' || $sign === '<' || $sign === '!') { + $toFloat = $sign === '>' || $sign === '<'; + if ($this->nextChar() === '=') { + $sign .= $this->readChar(); + } + } + + $var = $this->readNextValue(); + if ($toFloat && ctype_digit($var)) { + $var = (float) $var; + } + + return Filter::expression($key, $sign, $var); + } + + protected function parseError($char = null, $extraMsg = null) + { + if ($extraMsg === null) { + $extra = ''; + } else { + $extra = ': ' . $extraMsg; + } + if ($char === null) { + $char = $this->string[$this->pos]; + } + if ($this->reportDebug) { + $extra .= "\n" . implode("\n", $this->debug); + } + + throw new FilterParseException( + 'Invalid filter "%s", unexpected %s at pos %d%s', + $this->string, + $char, + $this->pos, + $extra + ); + } + + protected function readFilters($nestingLevel = 0, $op = null) + { + $filters = array(); + while ($this->pos < $this->length) { + if ($op === '!' && count($filters) === 1) { + break; + } + $filter = $this->readNextExpression(); + $next = $this->readChar(); + + + if ($filter === false) { + $this->debug('Got no next expression, next is ' . $next, $nestingLevel, $op); + if ($next === '!') { + $not = $this->readFilters($nestingLevel + 1, '!'); + $filters[] = $not; + if (in_array($this->nextChar(), array('|', '&', ')'))) { + $next = $this->readChar(); + $this->debug('Got NOT, next is now: ' . $next, $nestingLevel, $op); + } else { + $this->debug('Breaking after NOT: ' . $not, $nestingLevel, $op); + break; + } + } + + if ($op === null && count($filters) > 0 && ($next === '&' || $next === '|')) { + $op = $next; + continue; + } + + if ($next === false) { + // Nothing more to read + break; + } + + if ($next === ')') { + if ($nestingLevel > 0) { + $this->debug('Closing without filter: ' . $next, $nestingLevel, $op); + break; + } + $this->parseError($next); + } + if ($next === '(') { + $filters[] = $this->readFilters($nestingLevel + 1, null); + continue; + } + if ($next === $op) { + continue; + } + $this->parseError($next, "$op level $nestingLevel"); + } else { + $this->debug('Got new expression: ' . $filter, $nestingLevel, $op); + + $filters[] = $filter; + + if ($next === false) { + $this->debug('Next is false, nothing to read but got filter', $nestingLevel, $op); + // Got filter, nothing more to read + break; + } + + if ($op === '!') { + $this->pos--; + break; + } + if ($next === $op) { + $this->debug('Next matches operator', $nestingLevel, $op); + continue; // Break?? + } + + if ($next === ')') { + if ($nestingLevel > 0) { + $this->debug('Closing with filter: ' . $next, $nestingLevel, $op); + break; + } + $this->parseError($next); + } + if ($op === null && in_array($next, array('&', '|'))) { + $this->debug('Setting op to ' . $next, $nestingLevel, $op); + $op = $next; + continue; + } + $this->parseError($next); + } + } + + if ($nestingLevel === 0 && $this->pos < $this->length) { + $this->parseError($op, 'Did not read full filter'); + } + + if ($nestingLevel === 0 && count($filters) === 1 && $op !== '!') { + // There is only one filter expression, no chain + $this->debug('Returning first filter only: ' . $filters[0], $nestingLevel, $op); + return $filters[0]; + } + + if ($op === null && count($filters) === 1) { + $this->debug('No op, single filter, setting AND', $nestingLevel, $op); + $op = '&'; + } + $this->debug(sprintf('Got %d filters, returning', count($filters)), $nestingLevel, $op); + + switch ($op) { + case '&': + return Filter::matchAll($filters); + case '|': + return Filter::matchAny($filters); + case '!': + return Filter::not($filters); + case null: + return Filter::matchAll(); + default: + $this->parseError($op); + } + } + + protected function parseQueryString($string) + { + $this->pos = 0; + + $this->string = $string; + + $this->length = $string ? strlen($string) : 0; + + if ($this->length === 0) { + return Filter::matchAll(); + } + return $this->readFilters(); + } + + protected function readUnless($char) + { + $buffer = ''; + while (false !== ($c = $this->readChar())) { + if (is_array($char)) { + if (in_array($c, $char)) { + $this->pos--; + break; + } + } else { + if ($c === $char) { + $this->pos--; + break; + } + } + $buffer .= $c; + } + + return $buffer; + } + + protected function readUnlessSpecialChar() + { + return $this->readUnless(array('=', '(', ')', '&', '|', '>', '<', '!')); + } + + protected function readExpressionOperator() + { + return $this->readUnless(array('=', '>', '<', '!')); + } + + protected function readChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos++]; + } + return false; + } + + protected function nextChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos]; + } + return false; + } +} |