diff --git a/library/Director/Web/Table/QuickTable.php b/library/Director/Web/Table/QuickTable.php index cf0e9cb5..1ea2b9db 100644 --- a/library/Director/Web/Table/QuickTable.php +++ b/library/Director/Web/Table/QuickTable.php @@ -3,9 +3,16 @@ namespace Icinga\Module\Director\Web\Table; use Icinga\Application\Icinga; +use Icinga\Data\Filter\FilterAnd; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterNot; +use Icinga\Data\Filter\FilterOr; use Icinga\Data\Selectable; use Icinga\Data\Paginatable; +use Icinga\Exception\QueryException; +use Icinga\Web\Request; use Icinga\Web\Url; +use Icinga\Web\Widget; use Icinga\Web\Widget\Paginator; abstract class QuickTable implements Paginatable @@ -18,6 +25,10 @@ abstract class QuickTable implements Paginatable protected $offset; + protected $filter; + + protected $searchColumns = array(); + protected function renderRow($row) { $htm = " \n"; @@ -72,6 +83,10 @@ abstract class QuickTable implements Paginatable $query->limit($this->getLimit(), $this->getOffset()); } + if ($this->filter && ! $this->filter->isEmpty()) { + $query->where($this->renderFilter($this->filter)); + } + return $db->fetchAll($query); } @@ -129,6 +144,11 @@ abstract class QuickTable implements Paginatable return $this->connection; } + protected function db() + { + return $this->connection()->getConnection(); + } + protected function renderTitles($row) { $view = $this->view(); @@ -181,4 +201,174 @@ abstract class QuickTable implements Paginatable { return $this->render(); } + + protected function getSearchColumns() + { + return $this->searchColumns; + } + + public function setFilter($filter) + { + $this->filter = $filter; + return $this; + } + + public function getFilterEditor(Request $request) + { + $filterEditor = Widget::create('filterEditor') + ->setQuery($this) + ->setSearchColumns($this->getSearchColumns()) + ->preserveParams('limit', 'sort', 'dir', 'view', 'backend') + ->ignoreParams('page') + ->handleRequest($request); + + $filter = $filterEditor->getFilter(); + $this->setFilter($filter); + + return $filterEditor; + } + + protected function mapFilterColumn($col) + { + $cols = $this->getColumns(); + return $cols[$col]; + } + + protected function renderFilter($filter, $level = 0) + { + $str = ''; + if ($filter instanceof FilterChain) { + if ($filter instanceof FilterAnd) { + $op = ' AND '; + } elseif ($filter instanceof FilterOr) { + $op = ' OR '; + } elseif ($filter instanceof FilterNot) { + $op = ' AND '; + $str .= ' NOT '; + } else { + throw new QueryException( + 'Cannot render filter: %s', + $filter + ); + } + $parts = array(); + if (! $filter->isEmpty()) { + foreach ($filter->filters() as $f) { + $filterPart = $this->renderFilter($f, $level + 1); + if ($filterPart !== '') { + $parts[] = $filterPart; + } + } + if (! empty($parts)) { + if ($level > 0) { + $str .= ' (' . implode($op, $parts) . ') '; + } else { + $str .= implode($op, $parts); + } + } + } + } else { + $str .= $this->whereToSql($this->mapFilterColumn($filter->getColumn()), $filter->getSign(), $filter->getExpression()); + } + + return $str; + } + + protected function escapeForSql($value) + { + // bindParam? bindValue? + if (is_array($value)) { + $ret = array(); + foreach ($value as $val) { + $ret[] = $this->escapeForSql($val); + } + return implode(', ', $ret); + } else { + //if (preg_match('/^\d+$/', $value)) { + // return $value; + //} else { + return $this->db()->quote($value); + //} + } + } + + protected function escapeWildcards($value) + { + return preg_replace('/\*/', '%', $value); + } + + protected function valueToTimestamp($value) + { + // We consider integers as valid timestamps. Does not work for URL params + if (ctype_digit($value)) { + return $value; + } + $value = strtotime($value); + if (! $value) { + /* + NOTE: It's too late to throw exceptions, we might finish in __toString + throw new QueryException(sprintf( + '"%s" is not a valid time expression', + $value + )); + */ + } + return $value; + } + + protected function timestampForSql($value) + { + // TODO: do this db-aware + return $this->escapeForSql(date('Y-m-d H:i:s', $value)); + } + + /** + * Check for timestamp fields + * + * TODO: This is not here to do automagic timestamp stuff. One may + * override this function for custom voodoo, IdoQuery right now + * does. IMO we need to split whereToSql functionality, however + * I'd prefer to wait with this unless we understood how other + * backends will work. We probably should also rename this + * function to isTimestampColumn(). + * + * @param string $field Field Field name to checked + * @return bool Whether this field expects timestamps + */ + public function isTimestamp($field) + { + return false; + } + + public function whereToSql($col, $sign, $expression) + { + if ($this->isTimestamp($col)) { + $expression = $this->valueToTimestamp($expression); + } + + if (is_array($expression) && $sign === '=') { + // TODO: Should we support this? Doesn't work for blub* + return $col . ' IN (' . $this->escapeForSql($expression) . ')'; + } elseif ($sign === '=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + // We'll ignore such filters as it prevents index usage and because "*" means anything, anything means + // all whereas all means that whether we use a filter to match anything or no filter at all makes no + // difference, except for performance reasons... + return ''; + } + + return $col . ' LIKE ' . $this->escapeForSql($this->escapeWildcards($expression)); + } elseif ($sign === '!=' && strpos($expression, '*') !== false) { + if ($expression === '*') { + // We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're + // using a real column with a valid comparison here or just an expression which cannot be evaluated to + // true makes no difference, except for performance reasons... + return $this->escapeForSql(0); + } + + return $col . ' NOT LIKE ' . $this->escapeForSql($this->escapeWildcards($expression)); + } else { + return $col . ' ' . $sign . ' ' . $this->escapeForSql($expression); + } + } }