diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
commit | f66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch) | |
tree | fbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Web/Table/QuickTable.php | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip |
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Director/Web/Table/QuickTable.php')
-rw-r--r-- | library/Director/Web/Table/QuickTable.php | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/library/Director/Web/Table/QuickTable.php b/library/Director/Web/Table/QuickTable.php new file mode 100644 index 0000000..ff3edcc --- /dev/null +++ b/library/Director/Web/Table/QuickTable.php @@ -0,0 +1,547 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Application\Icinga; +use Icinga\Data\Filter\FilterAnd; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Data\Filter\FilterNot; +use Icinga\Data\Filter\FilterOr; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Selectable; +use Icinga\Data\Paginatable; +use Icinga\Exception\QueryException; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Web\Request; +use gipfl\IcingaWeb2\Url; +use Icinga\Web\View; +use Icinga\Web\Widget; +use Icinga\Web\Widget\Paginator; +use ipl\Html\ValidHtml; +use stdClass; +use Zend_Db_Select as ZfDbSelect; + +abstract class QuickTable implements Paginatable, ValidHtml +{ + protected $view; + + /** @var Db */ + protected $connection; + + protected $limit; + + protected $offset; + + /** @var Filter */ + protected $filter; + + protected $enforcedFilters = array(); + + protected $searchColumns = array(); + + protected function getRowClasses($row) + { + return array(); + } + + protected function getRowClassesString($row) + { + return $this->createClassAttribute($this->getRowClasses($row)); + } + + protected function createClassAttribute($classes) + { + $str = $this->createClassesString($classes); + if (strlen($str) > 0) { + return ' class="' . $str . '"'; + } else { + return ''; + } + } + + private function createClassesString($classes) + { + if (is_string($classes)) { + $classes = array($classes); + } + + if (empty($classes)) { + return ''; + } else { + return implode(' ', $classes); + } + } + + protected function getMultiselectProperties() + { + /* array( + * 'url' => 'director/hosts/edit', + * 'sourceUrl' => 'director/hosts', + * 'keys' => 'name' + * ) */ + + return array(); + } + + protected function renderMultiselectAttributes() + { + $props = $this->getMultiselectProperties(); + + if (empty($props)) { + return ''; + } + + $prefix = 'data-icinga-multiselect-'; + $view = $this->view(); + $parts = array(); + $multi = array( + 'url' => $view->href($props['url']), + 'controllers' => $view->href($props['sourceUrl']), + 'data' => implode(',', $props['keys']), + ); + + foreach ($multi as $k => $v) { + $parts[] = $prefix . $k . '="' . $v . '"'; + } + + return ' ' . implode(' ', $parts); + } + + protected function renderRow($row) + { + $htm = " <tr" . $this->getRowClassesString($row) . ">\n"; + $firstCol = true; + + foreach ($this->getTitles() as $key => $title) { + // Support missing columns + if (property_exists($row, $key)) { + $val = $row->$key; + } else { + $val = null; + } + + $value = null; + + if ($firstCol) { + if ($val !== null && $url = $this->getActionUrl($row)) { + $value = $this->view()->qlink($val, $this->getActionUrl($row)); + } + $firstCol = false; + } + + if ($value === null) { + if ($val === null) { + $value = '-'; + } elseif (is_array($val) || $val instanceof stdClass || is_bool($val)) { + $value = '<pre>' + . $this->view()->escape(PlainObjectRenderer::render($val)) + . '</pre>'; + } else { + $value = $this->view()->escape($val); + } + } + + $htm .= ' <td>' . $value . "</td>\n"; + } + + if ($this->hasAdditionalActions()) { + $htm .= ' <td class="actions">' . $this->renderAdditionalActions($row) . "</td>\n"; + } + + return $htm . " </tr>\n"; + } + + abstract protected function getTitles(); + + protected function getActionUrl($row) + { + return false; + } + + public function setConnection(Selectable $connection) + { + $this->connection = $connection; + return $this; + } + + /** + * @return ZfDbSelect + */ + abstract protected function getBaseQuery(); + + public function fetchData() + { + $db = $this->db(); + $query = $this->getBaseQuery()->columns($this->getColumns()); + + if ($this->hasLimit() || $this->hasOffset()) { + $query->limit($this->getLimit(), $this->getOffset()); + } + + $this->applyFiltersToQuery($query); + + return $db->fetchAll($query); + } + + protected function applyFiltersToQuery(ZfDbSelect $query) + { + $filter = null; + $enforced = $this->enforcedFilters; + if ($this->filter && ! $this->filter->isEmpty()) { + $filter = $this->filter; + } elseif (! empty($enforced)) { + $filter = array_shift($enforced); + } + if ($filter) { + foreach ($enforced as $f) { + $filter = $filter->andFilter($f); + } + $query->where($this->renderFilter($filter)); + } + + return $query; + } + + public function getPaginator() + { + $paginator = new Paginator(); + $paginator->setQuery($this); + + return $paginator; + } + + #[\ReturnTypeWillChange] + public function count() + { + $db = $this->db(); + $query = clone($this->getBaseQuery()); + $query->reset('order')->columns(array('COUNT(*)')); + $this->applyFiltersToQuery($query); + + return $db->fetchOne($query); + } + + 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; + } + + public function hasAdditionalActions() + { + return method_exists($this, 'renderAdditionalActions'); + } + + /** @return Db */ + protected function connection() + { + // TODO: Fail if missing? Require connection in constructor? + return $this->connection; + } + + protected function db() + { + return $this->connection()->getDbAdapter(); + } + + protected function renderTitles($row) + { + $view = $this->view(); + $htm = "<thead>\n <tr>\n"; + + foreach ($row as $title) { + $htm .= ' <th>' . $view->escape($title) . "</th>\n"; + } + + if ($this->hasAdditionalActions()) { + $htm .= ' <th class="actions">' . $view->translate('Actions') . "</th>\n"; + } + + return $htm . " </tr>\n</thead>\n"; + } + + protected function url($url, $params) + { + return Url::fromPath($url, $params); + } + + protected function listTableClasses() + { + $classes = array('simple', 'common-table', 'table-row-selectable'); + $multi = $this->getMultiselectProperties(); + if (! empty($multi)) { + $classes[] = 'multiselect'; + } + + return $classes; + } + + public function render() + { + $data = $this->fetchData(); + + $htm = '<table' + . $this->createClassAttribute($this->listTableClasses()) + . $this->renderMultiselectAttributes() + . '>' . "\n" + . $this->renderTitles($this->getTitles()) + . $this->beginTableBody(); + foreach ($data as $row) { + $htm .= $this->renderRow($row); + } + return $htm . $this->endTableBody() . $this->endTable(); + } + + protected function beginTableBody() + { + return "<tbody>\n"; + } + + protected function endTableBody() + { + return "</tbody>\n"; + } + + protected function endTable() + { + return "</table>\n"; + } + + /** + * @return View + */ + 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(); + } + + protected function getSearchColumns() + { + return $this->searchColumns; + } + + abstract public function getColumns(); + + public function getFilterColumns() + { + $keys = array_keys($this->getColumns()); + return array_combine($keys, $keys); + } + + public function setFilter($filter) + { + $this->filter = $filter; + return $this; + } + + public function enforceFilter($filter, $expression = null) + { + if (! $filter instanceof Filter) { + $filter = Filter::where($filter, $expression); + } + $this->enforcedFilters[] = $filter; + return $this; + } + + public function getFilterEditor(Request $request) + { + $filterEditor = Widget::create('filterEditor') + ->setColumns(array_keys($this->getColumns())) + ->setSearchColumns($this->getSearchColumns()) + ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', '_dev') + ->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 $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 { + /** @var FilterExpression $filter */ + $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 (! is_string($value) || 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); + } + } +} |