2015-04-24 14:27:22 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Icinga\Module\Director\Web\Table;
|
|
|
|
|
|
|
|
use Icinga\Application\Icinga;
|
2015-07-29 15:24:56 +02:00
|
|
|
use Icinga\Data\Filter\FilterAnd;
|
|
|
|
use Icinga\Data\Filter\FilterChain;
|
|
|
|
use Icinga\Data\Filter\FilterNot;
|
|
|
|
use Icinga\Data\Filter\FilterOr;
|
2015-04-24 14:27:22 +02:00
|
|
|
use Icinga\Data\Selectable;
|
2015-07-23 16:18:32 +02:00
|
|
|
use Icinga\Data\Paginatable;
|
2015-07-29 15:24:56 +02:00
|
|
|
use Icinga\Exception\QueryException;
|
|
|
|
use Icinga\Web\Request;
|
2015-04-24 14:27:22 +02:00
|
|
|
use Icinga\Web\Url;
|
2015-07-29 15:24:56 +02:00
|
|
|
use Icinga\Web\Widget;
|
2015-07-23 16:18:32 +02:00
|
|
|
use Icinga\Web\Widget\Paginator;
|
2015-04-24 14:27:22 +02:00
|
|
|
|
2015-07-23 16:18:32 +02:00
|
|
|
abstract class QuickTable implements Paginatable
|
2015-04-24 14:27:22 +02:00
|
|
|
{
|
|
|
|
protected $view;
|
|
|
|
|
|
|
|
protected $connection;
|
|
|
|
|
2015-07-23 16:18:32 +02:00
|
|
|
protected $limit;
|
|
|
|
|
|
|
|
protected $offset;
|
|
|
|
|
2015-07-29 15:24:56 +02:00
|
|
|
protected $filter;
|
|
|
|
|
|
|
|
protected $searchColumns = array();
|
|
|
|
|
2015-06-02 17:32:19 +02:00
|
|
|
protected function renderRow($row)
|
2015-04-24 14:27:22 +02:00
|
|
|
{
|
|
|
|
$htm = " <tr>\n";
|
2015-06-01 12:42:40 +02:00
|
|
|
$firstRow = true;
|
2015-04-24 14:27:22 +02:00
|
|
|
|
2015-06-02 17:32:19 +02:00
|
|
|
foreach ($this->getTitles() as $key => $title) {
|
|
|
|
$val = $row->$key;
|
2015-06-01 12:42:40 +02:00
|
|
|
$value = null;
|
|
|
|
|
|
|
|
if ($firstRow) {
|
|
|
|
if ($val !== null && $url = $this->getActionUrl($row)) {
|
|
|
|
$value = $this->view()->qlink($val, $this->getActionUrl($row));
|
|
|
|
}
|
|
|
|
$firstRow = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($value === null) {
|
|
|
|
$value = $val === null ? '-' : $this->view()->escape($val);
|
|
|
|
}
|
|
|
|
|
|
|
|
$htm .= ' <td>' . $value . "</td>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->hasAdditionalActions()) {
|
|
|
|
$htm .= ' <td class="actions">' . $this->renderAdditionalActions($row) . "</td>\n";
|
2015-04-24 14:27:22 +02:00
|
|
|
}
|
2015-06-01 12:42:40 +02:00
|
|
|
|
2015-04-24 14:27:22 +02:00
|
|
|
return $htm . " </tr>\n";
|
|
|
|
}
|
|
|
|
|
2015-06-02 17:32:19 +02:00
|
|
|
abstract protected function getTitles();
|
|
|
|
|
2015-06-01 12:42:40 +02:00
|
|
|
protected function getActionUrl($row)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-04-24 14:27:22 +02:00
|
|
|
public function setConnection(Selectable $connection)
|
|
|
|
{
|
|
|
|
$this->connection = $connection;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2015-07-23 16:18:32 +02:00
|
|
|
abstract protected function getBaseQuery();
|
|
|
|
|
|
|
|
public function fetchData()
|
|
|
|
{
|
|
|
|
$db = $this->connection()->getConnection();
|
|
|
|
$query = $this->getBaseQuery()->columns($this->getColumns());
|
|
|
|
|
|
|
|
if ($this->hasLimit() || $this->hasOffset()) {
|
|
|
|
$query->limit($this->getLimit(), $this->getOffset());
|
|
|
|
}
|
|
|
|
|
2015-07-29 15:24:56 +02:00
|
|
|
if ($this->filter && ! $this->filter->isEmpty()) {
|
|
|
|
$query->where($this->renderFilter($this->filter));
|
|
|
|
}
|
|
|
|
|
2015-07-23 16:18:32 +02:00
|
|
|
return $db->fetchAll($query);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPaginator()
|
|
|
|
{
|
|
|
|
$paginator = new Paginator();
|
|
|
|
$paginator->setQuery($this);
|
|
|
|
|
|
|
|
return $paginator;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function count()
|
|
|
|
{
|
|
|
|
$db = $this->connection()->getConnection();
|
|
|
|
|
|
|
|
return $db->fetchOne($this->getBaseQuery()->columns(array('COUNT(*)')));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function limit($count = null, $offset = null)
|
|
|
|
{
|
|
|
|
$this->limit = $count;
|
|
|
|
$this->offset = $offset;
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function hasLimit()
|
|
|
|
{
|
|
|
|
return $this->limit !== null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getLimit()
|
|
|
|
{
|
|
|
|
return $this->limit;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function hasOffset()
|
|
|
|
{
|
|
|
|
return $this->offset !== null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getOffset()
|
|
|
|
{
|
|
|
|
return $this->offset;
|
|
|
|
}
|
|
|
|
|
2015-06-01 12:42:40 +02:00
|
|
|
public function hasAdditionalActions()
|
|
|
|
{
|
|
|
|
return method_exists($this, 'renderAdditionalActions');
|
|
|
|
}
|
|
|
|
|
2015-04-24 14:27:22 +02:00
|
|
|
protected function connection()
|
|
|
|
{
|
|
|
|
// TODO: Fail if missing? Require connection in constructor?
|
|
|
|
return $this->connection;
|
|
|
|
}
|
|
|
|
|
2015-07-29 15:24:56 +02:00
|
|
|
protected function db()
|
|
|
|
{
|
|
|
|
return $this->connection()->getConnection();
|
|
|
|
}
|
|
|
|
|
2015-04-24 14:27:22 +02:00
|
|
|
protected function renderTitles($row)
|
|
|
|
{
|
2015-07-26 15:37:01 +02:00
|
|
|
$view = $this->view();
|
2015-04-24 14:27:22 +02:00
|
|
|
$htm = "<thead>\n <tr>\n";
|
2015-06-01 12:42:40 +02:00
|
|
|
|
2015-04-24 14:27:22 +02:00
|
|
|
foreach ($row as $title) {
|
|
|
|
$htm .= ' <th>' . $view->escape($title) . "</th>\n";
|
|
|
|
}
|
2015-06-01 12:42:40 +02:00
|
|
|
|
|
|
|
if ($this->hasAdditionalActions()) {
|
|
|
|
$htm .= ' <th class="actions">' . $view->translate('Actions') . "</th>\n";
|
|
|
|
}
|
|
|
|
|
2015-04-24 14:27:22 +02:00
|
|
|
return $htm . " </tr>\n</thead>\n";
|
|
|
|
}
|
|
|
|
|
2015-06-01 12:42:40 +02:00
|
|
|
protected function url($url, $params)
|
|
|
|
{
|
|
|
|
return Url::fromPath($url, $params);
|
|
|
|
}
|
|
|
|
|
2015-04-24 14:27:22 +02:00
|
|
|
public function render()
|
|
|
|
{
|
|
|
|
$data = $this->fetchData();
|
|
|
|
|
|
|
|
$htm = '<table class="simple action">' . "\n"
|
|
|
|
. $this->renderTitles($this->getTitles())
|
|
|
|
. "<tbody>\n";
|
|
|
|
foreach ($data as $row) {
|
|
|
|
$htm .= $this->renderRow($row);
|
|
|
|
}
|
|
|
|
return $htm . "</tbody>\n</table>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function view()
|
|
|
|
{
|
|
|
|
if ($this->view === null) {
|
|
|
|
$this->view = Icinga::app()->getViewRenderer()->view;
|
|
|
|
}
|
|
|
|
return $this->view;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function setView($view)
|
|
|
|
{
|
|
|
|
$this->view = $view;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function __toString()
|
|
|
|
{
|
|
|
|
return $this->render();
|
|
|
|
}
|
2015-07-29 15:24:56 +02:00
|
|
|
|
|
|
|
protected function getSearchColumns()
|
|
|
|
{
|
|
|
|
return $this->searchColumns;
|
|
|
|
}
|
|
|
|
|
2015-07-29 17:15:56 +02:00
|
|
|
abstract public function getColumns();
|
|
|
|
|
|
|
|
public function getFilterColumns()
|
|
|
|
{
|
|
|
|
$keys = array_keys($this->getColumns());
|
|
|
|
return array_combine($keys, $keys);
|
|
|
|
}
|
|
|
|
|
2015-07-29 15:24:56 +02:00
|
|
|
public function setFilter($filter)
|
|
|
|
{
|
|
|
|
$this->filter = $filter;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getFilterEditor(Request $request)
|
|
|
|
{
|
|
|
|
$filterEditor = Widget::create('filterEditor')
|
2015-07-29 17:15:56 +02:00
|
|
|
->setColumns(array_keys($this->getColumns()))
|
2015-07-29 15:24:56 +02:00
|
|
|
->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);
|
|
|
|
}
|
|
|
|
}
|
2015-04-24 14:27:22 +02:00
|
|
|
}
|