summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Data/Filter
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
commit8ca6cc32b2c789a3149861159ad258f2cb9491e3 (patch)
tree2492de6f1528dd44eaa169a5c1555026d9cb75ec /library/Icinga/Data/Filter
parentInitial commit. (diff)
downloadicingaweb2-upstream/2.11.4.tar.xz
icingaweb2-upstream/2.11.4.zip
Adding upstream version 2.11.4.upstream/2.11.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--library/Icinga/Data/Filter/Filter.php255
-rw-r--r--library/Icinga/Data/Filter/FilterAnd.php42
-rw-r--r--library/Icinga/Data/Filter/FilterChain.php286
-rw-r--r--library/Icinga/Data/Filter/FilterEqual.php16
-rw-r--r--library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php16
-rw-r--r--library/Icinga/Data/Filter/FilterEqualOrLessThan.php26
-rw-r--r--library/Icinga/Data/Filter/FilterException.php15
-rw-r--r--library/Icinga/Data/Filter/FilterExpression.php224
-rw-r--r--library/Icinga/Data/Filter/FilterGreaterThan.php16
-rw-r--r--library/Icinga/Data/Filter/FilterLessThan.php26
-rw-r--r--library/Icinga/Data/Filter/FilterMatch.php8
-rw-r--r--library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php13
-rw-r--r--library/Icinga/Data/Filter/FilterMatchNot.php12
-rw-r--r--library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php13
-rw-r--r--library/Icinga/Data/Filter/FilterNot.php58
-rw-r--r--library/Icinga/Data/Filter/FilterNotEqual.php12
-rw-r--r--library/Icinga/Data/Filter/FilterOr.php39
-rw-r--r--library/Icinga/Data/Filter/FilterParseException.php10
-rw-r--r--library/Icinga/Data/Filter/FilterQueryString.php320
-rw-r--r--library/Icinga/Data/FilterColumns.php21
-rw-r--r--library/Icinga/Data/Filterable.php27
21 files changed, 1455 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;
+ }
+}
diff --git a/library/Icinga/Data/FilterColumns.php b/library/Icinga/Data/FilterColumns.php
new file mode 100644
index 0000000..7eaacea
--- /dev/null
+++ b/library/Icinga/Data/FilterColumns.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+interface FilterColumns
+{
+ /**
+ * Return a filterable's filter columns with their optional label as key
+ *
+ * @return array
+ */
+ public function getFilterColumns();
+
+ /**
+ * Return a filterable's search columns
+ *
+ * @return array
+ */
+ public function getSearchColumns();
+}
diff --git a/library/Icinga/Data/Filterable.php b/library/Icinga/Data/Filterable.php
new file mode 100644
index 0000000..ceca22f
--- /dev/null
+++ b/library/Icinga/Data/Filterable.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Interface for filtering a result set
+ *
+ * @deprecated(EL): addFilter and applyFilter do the same in all usages.
+ * addFilter could be replaced w/ getFilter()->add(). We must no require classes implementing this interface to
+ * implement redundant methods over and over again. This interface must be moved to the namespace Icinga\Data\Filter.
+ * It lacks documentation.
+ */
+interface Filterable
+{
+ public function applyFilter(Filter $filter);
+
+ public function setFilter(Filter $filter);
+
+ public function getFilter();
+
+ public function addFilter(Filter $filter);
+
+ public function where($condition, $value = null);
+}