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

View File

@ -9,6 +9,7 @@ use Icinga\Data\Reducible;
use Icinga\Data\Updatable; use Icinga\Data\Updatable;
use Icinga\Exception\IcingaException; use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\Exception\StatementException;
/** /**
* Abstract base class for concrete database repository implementations * Abstract base class for concrete database repository implementations
@ -101,6 +102,37 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
return $table; 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 * 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) 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) public function update($table, array $bind, Filter $filter = null)
{ {
if ($filter) { 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) public function delete($table, Filter $filter = null)
{ {
if ($filter) { if ($filter) {
$this->requireFilter($filter); $this->requireFilter($table, $filter);
} }
$this->ds->delete($this->prependTablePrefix($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 * @param string $name The column name or alias to check
* *
* @return bool * @return bool
*/ */
public function hasStatementColumn($name) public function hasStatementColumn($table, $name)
{ {
$statementColumnMap = $this->getStatementColumnMap(); $statementColumnMap = $this->getStatementColumnMap();
if (! array_key_exists($name, $statementColumnMap)) { if (
return parent::hasStatementColumn($name); ! array_key_exists($name, $statementColumnMap)
|| !$this->validateStatementColumnAssociation($table, $name)
) {
return parent::hasStatementColumn($table, $name);
} }
return true; 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 * 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 * @param string $name The name or alias of the column to validate
* *
* @return string The given column's name * @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(); $statementColumnMap = $this->getStatementColumnMap();
if (! array_key_exists($name, $statementColumnMap)) { 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]; return $statementColumnMap[$name];

View File

@ -34,7 +34,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
*/ */
public function insert($target, array $data) public function insert($target, array $data)
{ {
$newData = $this->requireStatementColumns($data); $newData = $this->requireStatementColumns($target, $data);
$section = $this->extractSectionName($target, $newData); $section = $this->extractSectionName($target, $newData);
if ($this->ds->hasSection($section)) { 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) public function update($target, array $data, Filter $filter = null)
{ {
$newData = $this->requireStatementColumns($data); $newData = $this->requireStatementColumns($target, $data);
$keyColumn = $this->ds->getConfigObject()->getKeyColumn(); $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) { if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) {
throw new StatementException( throw new StatementException(
@ -82,7 +82,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
$contents = array($target => $this->ds->getSection($target)); $contents = array($target => $this->ds->getSection($target));
} else { } else {
if ($filter) { if ($filter) {
$this->requireFilter($filter); $this->requireFilter($target, $filter);
} }
$contents = iterator_to_array($this->ds); $contents = iterator_to_array($this->ds);
@ -148,7 +148,7 @@ abstract class IniRepository extends Repository implements Extensible, Updatable
$this->ds->removeSection($target); $this->ds->removeSection($target);
} else { } else {
if ($filter) { if ($filter) {
$this->requireFilter($filter); $this->requireFilter($target, $filter);
} }
foreach (iterator_to_array($this->ds) as $section => $config) { 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 * @param Filter $filter
*/ */
public function requireFilter(Filter $filter) public function requireFilter($table, Filter $filter)
{ {
if ($filter->isExpression()) { if ($filter->isExpression()) {
$column = $filter->getColumn(); $column = $filter->getColumn();
$filter->setColumn($this->requireFilterColumn($column)); $filter->setColumn($this->requireFilterColumn($table, $column));
$filter->setExpression($this->persistColumn($column, $filter->getExpression())); $filter->setExpression($this->persistColumn($column, $filter->getExpression()));
} elseif ($filter->isChain()) { } elseif ($filter->isChain()) {
foreach ($filter->filters() as $chainOrExpression) { 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 * @return array
*/ */
public function requireAllQueryColumns() public function requireAllQueryColumns($table)
{ {
$map = array(); $map = array();
foreach ($this->getAliasColumnMap() as $alias => $_) { 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 // 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 $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 * 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 * @param string $name The column name or alias to check
* *
* @return bool * @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 * 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 * @param string $name The name or alias of the column to validate
* *
* @return string The given column's name * @return string The given column's name
* *
* @throws QueryException In case the given column is not a valid query column * @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())) { if (in_array($name, $this->getFilterColumns())) {
throw new QueryException(t('Filter column "%s" cannot be queried'), $name); 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); throw new QueryException(t('Query column "%s" not found'), $name);
} }
return $aliasColumnMap[$name]; if (! $this->validateQueryColumnAssociation($table, $name)) {
} throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
/**
* 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);
} }
return $aliasColumnMap[$name]; 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 * @param string $name The column name or alias to check
* *
* @return bool * @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 * 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 * @param string $name The name or alias of the column to validate
* *
* @return string The given column's name * @return string The given column's name
* *
* @throws StatementException 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)
{ {
if (in_array($name, $this->filterColumns)) { if (in_array($name, $this->filterColumns)) {
throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name); 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); 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]; 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 * @param array $data
* *
* @return array * @return array
*/ */
public function requireStatementColumns(array $data) public function requireStatementColumns($table, array $data)
{ {
$resolved = array(); $resolved = array();
foreach ($data as $alias => $value) { foreach ($data as $alias => $value) {
$resolved[$this->requireStatementColumn($alias)] = $this->persistColumn($alias, $value); $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($alias, $value);
} }
return $resolved; return $resolved;

View File

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