diff options
Diffstat (limited to 'library/X509/Web/Control/SearchBar/ObjectSuggestions.php')
-rw-r--r-- | library/X509/Web/Control/SearchBar/ObjectSuggestions.php | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/library/X509/Web/Control/SearchBar/ObjectSuggestions.php b/library/X509/Web/Control/SearchBar/ObjectSuggestions.php new file mode 100644 index 0000000..ca9630f --- /dev/null +++ b/library/X509/Web/Control/SearchBar/ObjectSuggestions.php @@ -0,0 +1,203 @@ +<?php + +namespace Icinga\Module\X509\Web\Control\SearchBar; + +use Exception; +use Icinga\Module\X509\Common\Database; +use ipl\Orm\Exception\InvalidColumnException; +use ipl\Orm\Model; +use ipl\Orm\Relation; +use ipl\Orm\Relation\HasOne; +use ipl\Orm\Resolver; +use ipl\Orm\UnionModel; +use ipl\Stdlib\Filter; +use ipl\Stdlib\Seq; +use ipl\Stdlib\Str; +use ipl\Web\Control\SearchBar\SearchException; +use ipl\Web\Control\SearchBar\Suggestions; + +class ObjectSuggestions extends Suggestions +{ + /** @var Model */ + protected $model; + + /** + * Set the model to show suggestions for + * + * @param string|Model $model + * + * @return $this + */ + public function setModel($model): self + { + if (is_string($model)) { + /** @var Model $model */ + $model = new $model(); + } + + $this->model = $model; + + return $this; + } + + protected function shouldShowRelationFor(string $column): bool + { + $columns = Str::trimSplit($column, '.'); + + switch (count($columns)) { + case 2: + return $columns[0] !== $this->model->getTableAlias(); + default: + return true; + } + } + + protected function createQuickSearchFilter($searchTerm) + { + $model = $this->model; + $resolver = $model::on(Database::get())->getResolver(); + + $quickFilter = Filter::any(); + foreach ($model->getSearchColumns() as $column) { + $where = Filter::like($resolver->qualifyColumn($column, $model->getTableAlias()), $searchTerm); + $where->metaData()->set('columnLabel', $resolver->getColumnDefinition($where->getColumn())->getLabel()); + $quickFilter->add($where); + } + + return $quickFilter; + } + + protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $searchFilter) + { + $model = $this->model; + $query = $model::on(Database::get()); + $query->limit(static::DEFAULT_LIMIT); + + if (strpos($column, ' ') !== false) { + // Searching for `Host Name` and continue typing without accepting/clicking the suggested + // column name will cause the search bar to use a label as a filter column + list($path, $_) = Seq::find( + self::collectFilterColumns($query->getModel(), $query->getResolver()), + $column, + false + ); + if ($path !== null) { + $column = $path; + } + } + + $columnPath = $query->getResolver()->qualifyPath($column, $model->getTableAlias()); + $inputFilter = Filter::like($columnPath, $searchTerm); + + $query->columns($columnPath); + $query->orderBy($columnPath); + + if ($searchFilter instanceof Filter\None) { + $query->filter($inputFilter); + } elseif ($searchFilter instanceof Filter\All) { + $searchFilter->add($inputFilter); + + // When 10 hosts are sharing the same certificate, filtering in the search bar by + // `Host Name=foo&Host Name=` will suggest only `foo` for the second filter. So, we have + // to force the filter processor to optimize search bar filter + $searchFilter->metaData()->set('forceOptimization', true); + $inputFilter->metaData()->set('forceOptimization', false); + } else { + $searchFilter = $inputFilter; + } + + $query->filter($searchFilter); + // Not to suggest something like Port=443,443,443.... + $query->getSelectBase()->distinct(); + + try { + $steps = Str::trimSplit($column, '.'); + $columnName = array_pop($steps); + if ($steps[0] === $model->getTableAlias()) { + array_shift($steps); + } + + foreach ($query as $row) { + $model = $row; + foreach ($steps as $step) { + try { + $model = $model->$step; + } catch (Exception $_) { + // pass + break; + } + } + + $value = $model->$columnName; + if ($value && is_string($value) && ! ctype_print($value)) { // Is binary + $value = bin2hex($value); + } elseif ($value === false || $value === true) { + // TODO: The search bar is never going to suggest boolean types, so this + // is a hack to workaround this limitation!! + $value = $value ? 'y' : 'n'; + } + + yield $value; + } + } catch (InvalidColumnException $e) { + throw new SearchException(sprintf(t('"%s" is not a valid column'), $e->getColumn())); + } + } + + protected function fetchColumnSuggestions($searchTerm) + { + $model = $this->model; + $query = $model::on(Database::get()); + + yield from self::collectFilterColumns($model, $query->getResolver()); + } + + public static function collectFilterColumns(Model $model, Resolver $resolver) + { + if ($model instanceof UnionModel) { + $models = []; + foreach ($model->getUnions() as $union) { + /** @var Model $unionModel */ + $unionModel = new $union[0](); + $models[$unionModel->getTableAlias()] = $unionModel; + self::collectRelations($resolver, $unionModel, $models, []); + } + } else { + $models = [$model->getTableAlias() => $model]; + self::collectRelations($resolver, $model, $models, []); + } + + /** @var Model $targetModel */ + foreach ($models as $path => $targetModel) { + foreach ($resolver->getColumnDefinitions($targetModel) as $columnName => $definition) { + yield "$path.$columnName" => $definition->getLabel(); + } + } + } + + protected static function collectRelations(Resolver $resolver, Model $subject, array &$models, array $path) + { + foreach ($resolver->getRelations($subject) as $name => $relation) { + /** @var Relation $relation */ + $isHasOne = $relation instanceof HasOne; + $relationPath = [$name]; + + if (! isset($models[$name]) && ! in_array($name, $path, true)) { + if ($isHasOne || empty($path)) { + array_unshift($relationPath, $subject->getTableAlias()); + } + + $relationPath = array_merge($path, $relationPath); + $targetPath = implode('.', $relationPath); + + if (! isset($models[$targetPath])) { + $models[$targetPath] = $relation->getTarget(); + self::collectRelations($resolver, $relation->getTarget(), $models, $relationPath); + return; + } + } else { + $path = []; + } + } + } +} |