Repository: Check whether a column is queried from the correct table

refs #8826
This commit is contained in:
Johannes Meyer 2015-05-12 15:38:29 +02:00
parent 399bbf0795
commit 053c9cdcb3
5 changed files with 202 additions and 75 deletions

View File

@ -101,7 +101,7 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
public function insert($table, array $data)
{
$newData['created_at'] = date('Y-m-d H:i:s');
$newData = $this->requireStatementColumns($data);
$newData = $this->requireStatementColumns($table, $data);
$values = array();
foreach ($newData as $column => $_) {
@ -138,9 +138,9 @@ class DbUserBackend extends DbRepository implements UserBackendInterface
public function update($table, array $data, Filter $filter = null)
{
$newData['last_modified'] = date('Y-m-d H:i:s');
$newData = $this->requireStatementColumns($data);
$newData = $this->requireStatementColumns($table, $data);
if ($filter) {
$this->requireFilter($filter);
$this->requireFilter($table, $filter);
}
$set = array();

View File

@ -9,6 +9,7 @@ use Icinga\Data\Reducible;
use Icinga\Data\Updatable;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\StatementException;
/**
* Abstract base class for concrete database repository implementations
@ -101,6 +102,37 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
return $table;
}
/**
* Remove the datasource's prefix from the given table name and return the remaining part
*
* @param mixed $table
*
* @return mixed
*/
protected function removeTablePrefix($table)
{
$prefix = $this->ds->getTablePrefix();
if (! $prefix) {
return $table;
}
if (is_array($table)) {
foreach ($table as & $tableName) {
if (strpos($tableName, $prefix) === 0) {
$tableName = str_replace($prefix, '', $tableName);
}
}
} elseif (is_string($table)) {
if (strpos($table, $prefix) === 0) {
$table = str_replace($prefix, '', $table);
}
} else {
throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
}
return $table;
}
/**
* Insert a table row with the given data
*
@ -109,7 +141,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*/
public function insert($table, array $bind)
{
$this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($bind));
$this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind));
}
/**
@ -122,10 +154,10 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
public function update($table, array $bind, Filter $filter = null)
{
if ($filter) {
$this->requireFilter($filter);
$this->requireFilter($table, $filter);
}
$this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($bind), $filter);
$this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter);
}
/**
@ -137,7 +169,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
public function delete($table, Filter $filter = null)
{
if ($filter) {
$this->requireFilter($filter);
$this->requireFilter($table, $filter);
}
$this->ds->delete($this->prependTablePrefix($table), $filter);
@ -216,17 +248,56 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
}
/**
* Return whether the given column name or alias is a valid statement column
* Return whether the given query column name or alias is available in the given table
*
* @param mixed $table
* @param string $column
*
* @return bool
*/
public function validateQueryColumnAssociation($table, $column)
{
if (is_array($table)) {
$table = array_shift($table);
}
return parent::validateQueryColumnAssociation($this->removeTablePrefix($table), $column);
}
/**
* Return whether the given statement column name or alias is available in the given table
*
* @param mixed $table
* @param string $column
*
* @return bool
*/
public function validateStatementColumnAssociation($table, $column)
{
if (is_array($table)) {
$table = array_shift($table);
}
$statementTableMap = $this->getStatementTableMap();
return $statementTableMap[$column] === $this->removeTablePrefix($table);
}
/**
* Return whether the given column name or alias of the given table is a valid statement column
*
* @param mixed $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasStatementColumn($name)
public function hasStatementColumn($table, $name)
{
$statementColumnMap = $this->getStatementColumnMap();
if (! array_key_exists($name, $statementColumnMap)) {
return parent::hasStatementColumn($name);
if (
! array_key_exists($name, $statementColumnMap)
|| !$this->validateStatementColumnAssociation($table, $name)
) {
return parent::hasStatementColumn($table, $name);
}
return true;
@ -235,17 +306,22 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
/**
* Validate that the given column is a valid statement column and return it or the actual name if it's an alias
*
* @param mixed $table The table for which to require the column
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a statement column
* @throws StatementException In case the given column is not a statement column
*/
public function requireStatementColumn($name)
public function requireStatementColumn($table, $name)
{
$statementColumnMap = $this->getStatementColumnMap();
if (! array_key_exists($name, $statementColumnMap)) {
return parent::requireStatementColumn($name);
return parent::requireStatementColumn($table, $name);
}
if (! $this->validateStatementColumnAssociation($table, $name)) {
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
}
return $statementColumnMap[$name];

View File

@ -34,7 +34,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
*/
public function insert($target, array $data)
{
$newData = $this->requireStatementColumns($data);
$newData = $this->requireStatementColumns($target, $data);
$section = $this->extractSectionName($target, $newData);
if ($this->ds->hasSection($section)) {
@ -65,7 +65,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
*/
public function update($target, array $data, Filter $filter = null)
{
$newData = $this->requireStatementColumns($data);
$newData = $this->requireStatementColumns($target, $data);
$keyColumn = $this->ds->getConfigObject()->getKeyColumn();
if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) {
throw new StatementException(
@ -82,7 +82,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
$contents = array($target => $this->ds->getSection($target));
} else {
if ($filter) {
$this->requireFilter($filter);
$this->requireFilter($target, $filter);
}
$contents = iterator_to_array($this->ds);
@ -148,7 +148,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
$this->ds->removeSection($target);
} else {
if ($filter) {
$this->requireFilter($filter);
$this->requireFilter($target, $filter);
}
foreach (iterator_to_array($this->ds) as $section => $config) {

View File

@ -574,63 +574,87 @@ abstract class Repository implements Selectable
}
/**
* Recurse the given filter, require each filter column and convert all values
* Recurse the given filter, require each column for the given table and convert all values
*
* @param string $table
* @param Filter $filter
*/
public function requireFilter(Filter $filter)
public function requireFilter($table, Filter $filter)
{
if ($filter->isExpression()) {
$column = $filter->getColumn();
$filter->setColumn($this->requireFilterColumn($column));
$filter->setColumn($this->requireFilterColumn($table, $column));
$filter->setExpression($this->persistColumn($column, $filter->getExpression()));
} elseif ($filter->isChain()) {
foreach ($filter->filters() as $chainOrExpression) {
$this->requireFilter($chainOrExpression);
$this->requireFilter($table, $chainOrExpression);
}
}
}
/**
* Return this repository's query columns mapped to their respective aliases
* Return this repository's query columns of the given table mapped to their respective aliases
*
* @param string $table
*
* @return array
*/
public function requireAllQueryColumns()
public function requireAllQueryColumns($table)
{
$map = array();
foreach ($this->getAliasColumnMap() as $alias => $_) {
if ($this->hasQueryColumn($alias)) {
if ($this->hasQueryColumn($table, $alias)) {
// Just in case $this->requireQueryColumn has been overwritten and there is some magic going on
$map[$alias] = $this->requireQueryColumn($alias);
$map[$alias] = $this->requireQueryColumn($table, $alias);
}
}
return $map;
}
/**
* Return whether the given query column name or alias is available in the given table
*
* @param string $table
* @param string $column
*
* @return bool
*/
public function validateQueryColumnAssociation($table, $column)
{
$aliasTableMap = $this->getAliasTableMap();
return $aliasTableMap[$column] === $table;
}
/**
* Return whether the given column name or alias is a valid query column
*
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasQueryColumn($name)
public function hasQueryColumn($table, $name)
{
return array_key_exists($name, $this->getAliasColumnMap()) && !in_array($name, $this->getFilterColumns());
if (in_array($name, $this->getFilterColumns())) {
return false;
}
return array_key_exists($name, $this->getAliasColumnMap())
&& $this->validateQueryColumnAssociation($table, $name);
}
/**
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid query column
*/
public function requireQueryColumn($name)
public function requireQueryColumn($table, $name)
{
if (in_array($name, $this->getFilterColumns())) {
throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
@ -641,62 +665,75 @@ abstract class Repository implements Selectable
throw new QueryException(t('Query column "%s" not found'), $name);
}
return $aliasColumnMap[$name];
}
/**
* Return whether the given column name or alias is a valid filter column
*
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasFilterColumn($name)
{
return array_key_exists($name, $this->getAliasColumnMap());
}
/**
* Validate that the given column is a valid filter target and return it or the actual name if it's an alias
*
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid filter column
*/
public function requireFilterColumn($name)
{
$aliasColumnMap = $this->getAliasColumnMap();
if (! array_key_exists($name, $aliasColumnMap)) {
throw new QueryException(t('Filter column "%s" not found'), $name);
if (! $this->validateQueryColumnAssociation($table, $name)) {
throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
}
return $aliasColumnMap[$name];
}
/**
* Return whether the given column name or alias is a valid statement column
* Return whether the given column name or alias is a valid filter column
*
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasStatementColumn($name)
public function hasFilterColumn($table, $name)
{
return $this->hasQueryColumn($name);
return array_key_exists($name, $this->getAliasColumnMap())
&& $this->validateQueryColumnAssociation($table, $name);
}
/**
* Validate that the given column is a valid filter target and return it or the actual name if it's an alias
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid filter column
*/
public function requireFilterColumn($table, $name)
{
$aliasColumnMap = $this->getAliasColumnMap();
if (! array_key_exists($name, $aliasColumnMap)) {
throw new QueryException(t('Filter column "%s" not found'), $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
}
return $aliasColumnMap[$name];
}
/**
* Return whether the given column name or alias of the given table is a valid statement column
*
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasStatementColumn($table, $name)
{
return $this->hasQueryColumn($table, $name);
}
/**
* Validate that the given column is a valid statement column and return it or the actual name if it's an alias
*
* @param string $table The table for which to require the column
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
* @throws StatementException In case the given column is not a statement column
*/
public function requireStatementColumn($name)
public function requireStatementColumn($table, $name)
{
if (in_array($name, $this->filterColumns)) {
throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
@ -707,21 +744,26 @@ abstract class Repository implements Selectable
throw new StatementException('Statement column "%s" not found', $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
}
return $aliasColumnMap[$name];
}
/**
* Resolve the given aliases or column names supposed to be persisted and convert their values
* Resolve the given aliases or column names of the given table supposed to be persisted and convert their values
*
* @param string $table
* @param array $data
*
* @return array
*/
public function requireStatementColumns(array $data)
public function requireStatementColumns($table, array $data)
{
$resolved = array();
foreach ($data as $alias => $value) {
$resolved[$this->requireStatementColumn($alias)] = $this->persistColumn($alias, $value);
$resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($alias, $value);
}
return $resolved;

View File

@ -28,6 +28,13 @@ class RepositoryQuery implements QueryInterface
*/
protected $query;
/**
* The current target to be queried
*
* @var mixed
*/
protected $target;
/**
* Create a new repository query
*
@ -54,14 +61,15 @@ class RepositoryQuery implements QueryInterface
*
* This notifies the repository about each desired query column.
*
* @param mixed $target The type and purpose of this parameter depends on this query's repository
* @param mixed $target The target from which to fetch the columns
* @param array $columns If null or an empty array, all columns will be fetched
*
* @return $this
*/
public function from($target, array $columns = null)
{
$this->query->from($target, $this->prepareQueryColumns($columns));
$this->query->from($target, $this->prepareQueryColumns($target, $columns));
$this->target = $target;
return $this;
}
@ -86,7 +94,7 @@ class RepositoryQuery implements QueryInterface
*/
public function columns(array $columns)
{
$this->query->columns($this->prepareQueryColumns($columns));
$this->query->columns($this->prepareQueryColumns($this->target, $columns));
return $this;
}
@ -95,18 +103,19 @@ class RepositoryQuery implements QueryInterface
*
* This notifies the repository about each desired query column.
*
* @param mixed $target The target where to look for each column
* @param array $desiredColumns Pass null or an empty array to require all query columns
*
* @return array The desired columns indexed by their respective alias
*/
protected function prepareQueryColumns(array $desiredColumns = null)
protected function prepareQueryColumns($target, array $desiredColumns = null)
{
if (empty($desiredColumns)) {
$columns = $this->repository->requireAllQueryColumns();
$columns = $this->repository->requireAllQueryColumns($target);
} else {
$columns = array();
foreach ($desiredColumns as $customAlias => $columnAlias) {
$resolvedColumn = $this->repository->requireQueryColumn($columnAlias);
$resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias);
if ($resolvedColumn !== $columnAlias) {
$columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn;
} elseif (is_string($customAlias)) {
@ -133,7 +142,7 @@ class RepositoryQuery implements QueryInterface
public function where($column, $value = null)
{
$this->query->where(
$this->repository->requireFilterColumn($column),
$this->repository->requireFilterColumn($this->target, $column),
$this->repository->persistColumn($column, $value)
);
return $this;
@ -164,7 +173,7 @@ class RepositoryQuery implements QueryInterface
*/
public function setFilter(Filter $filter)
{
$this->repository->requireFilter($filter);
$this->repository->requireFilter($this->target, $filter);
$this->query->setFilter($filter);
return $this;
}
@ -180,7 +189,7 @@ class RepositoryQuery implements QueryInterface
*/
public function addFilter(Filter $filter)
{
$this->repository->requireFilter($filter);
$this->repository->requireFilter($this->target, $filter);
$this->query->addFilter($filter);
return $this;
}
@ -245,7 +254,7 @@ class RepositoryQuery implements QueryInterface
try {
$this->query->order(
$this->repository->requireFilterColumn($column),
$this->repository->requireFilterColumn($this->target, $column),
$direction ? $baseDirection : ($specificDirection ?: $baseDirection)
);
} catch (QueryException $_) {