Skip to content

Commit 3726881

Browse files
committed
Whitelist: first implementation (still some issues to fix)
1 parent 36c690d commit 3726881

23 files changed

+455
-239
lines changed

src/AbstractTDBMObject.php

+12-24
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public function __construct(?string $tableName = null, array $primaryKeys = [],
9999
$this->_setStatus(TDBMObjectStateEnum::STATE_DETACHED);
100100
} else {
101101
$this->_attach($tdbmService);
102-
if (!empty($primaryKeys)) {
102+
if (!empty($primaryKeys)) { // @TODO (gua) might not be fully loaded
103103
$this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED);
104104
} else {
105105
$this->_setStatus(TDBMObjectStateEnum::STATE_NEW);
@@ -110,7 +110,7 @@ public function __construct(?string $tableName = null, array $primaryKeys = [],
110110
/**
111111
* Alternative constructor called when data is fetched from database via a SELECT.
112112
*
113-
* @param array[] $beanData array<table, array<column, value>>
113+
* @param array<string, array<string, mixed>> $beanData array<table, array<column, value>>
114114
* @param TDBMService $tdbmService
115115
*/
116116
public function _constructFromData(array $beanData, TDBMService $tdbmService): void
@@ -121,7 +121,7 @@ public function _constructFromData(array $beanData, TDBMService $tdbmService): v
121121
$this->dbRows[$table] = new DbRow($this, $table, static::getForeignKeys($table), $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns);
122122
}
123123

124-
$this->status = TDBMObjectStateEnum::STATE_LOADED;
124+
$this->status = TDBMObjectStateEnum::STATE_LOADED; // @TODO might be not fully loaded
125125
}
126126

127127
/**
@@ -251,27 +251,14 @@ protected function set(string $var, $value, ?string $tableName = null): void
251251
}
252252
}
253253

254-
/**
255-
* @param string $foreignKeyName
256-
* @param AbstractTDBMObject $bean
257-
*/
258-
protected function setRef(string $foreignKeyName, AbstractTDBMObject $bean = null, string $tableName = null): void
254+
protected function setRef(string $foreignKeyName, ?AbstractTDBMObject $bean, string $tableName, string $className, string $resultIteratorClass): void
259255
{
260-
if ($tableName === null) {
261-
if (count($this->dbRows) > 1) {
262-
throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
263-
} elseif (count($this->dbRows) === 1) {
264-
$tableName = (string) array_keys($this->dbRows)[0];
265-
} else {
266-
throw new TDBMException('Please specify a table for this object.');
267-
}
268-
}
269-
256+
assert($bean === null || is_a($bean, $className), new TDBMInvalidArgumentException('$bean should be `null` or `' . $className . '`. `' . ($bean === null ? 'null' : get_class($bean)) . '` provided.'));
270257
if (!isset($this->dbRows[$tableName])) {
271258
$this->registerTable($tableName);
272259
}
273260

274-
$oldLinkedBean = $this->dbRows[$tableName]->getRef($foreignKeyName);
261+
$oldLinkedBean = $this->dbRows[$tableName]->getRef($foreignKeyName, $className, $resultIteratorClass);
275262
if ($oldLinkedBean !== null) {
276263
$oldLinkedBean->removeManyToOneRelationship($tableName, $foreignKeyName, $this);
277264
}
@@ -291,19 +278,19 @@ protected function setRef(string $foreignKeyName, AbstractTDBMObject $bean = nul
291278
*
292279
* @return AbstractTDBMObject|null
293280
*/
294-
protected function getRef(string $foreignKeyName, ?string $tableName = null) : ?AbstractTDBMObject
281+
protected function getRef(string $foreignKeyName, string $tableName, string $className, string $resultIteratorClass) : ?AbstractTDBMObject
295282
{
296283
$tableName = $this->checkTableName($tableName);
297284

298285
if (!isset($this->dbRows[$tableName])) {
299286
return null;
300287
}
301288

302-
return $this->dbRows[$tableName]->getRef($foreignKeyName);
289+
return $this->dbRows[$tableName]->getRef($foreignKeyName, $className, $resultIteratorClass);
303290
}
304291

305292
/**
306-
* Adds a many to many relationship to this bean.
293+
* Adds a many to many$table relationship to this bean.
307294
*
308295
* @param string $pivotTableName
309296
* @param AbstractTDBMObject $remoteBean
@@ -525,15 +512,16 @@ private function removeManyToOneRelationship(string $tableName, string $foreignK
525512
*
526513
* @return AlterableResultIterator
527514
*/
528-
protected function retrieveManyToOneRelationshipsStorage(string $tableName, string $foreignKeyName, array $searchFilter, string $orderString = null) : AlterableResultIterator
515+
protected function retrieveManyToOneRelationshipsStorage(string $tableName, string $foreignKeyName, array $searchFilter, ?string $orderString, string $resultIteratorClass) : AlterableResultIterator
529516
{
517+
assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'));
530518
$key = $tableName.'___'.$foreignKeyName;
531519
$alterableResultIterator = $this->getManyToOneAlterableResultIterator($tableName, $foreignKeyName);
532520
if ($this->status === TDBMObjectStateEnum::STATE_DETACHED || $this->status === TDBMObjectStateEnum::STATE_NEW || (isset($this->manyToOneRelationships[$key]) && $this->manyToOneRelationships[$key]->getUnderlyingResultIterator() !== null)) {
533521
return $alterableResultIterator;
534522
}
535523

536-
$unalteredResultIterator = $this->tdbmService->findObjects($tableName, $searchFilter, [], $orderString);
524+
$unalteredResultIterator = $this->tdbmService->findObjects($tableName, $searchFilter, [], $orderString, [], null, null, $resultIteratorClass);
537525

538526
$alterableResultIterator->setResultIterator($unalteredResultIterator->getIterator());
539527

src/AlterableResultIterator.php

+2-5
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,8 @@ public function add($object): void
7575
{
7676
$this->alterations->attach($object, 'add');
7777

78-
if ($this->resultArray !== null) {
79-
$foundKey = array_search($object, $this->resultArray, true);
80-
if ($foundKey === false) {
81-
$this->resultArray[] = $object;
82-
}
78+
if ($this->resultArray !== null && !in_array($object, $this->resultArray, true)) {
79+
$this->resultArray[] = $object;
8380
}
8481
}
8582

src/DbRow.php

+7-13
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public function __construct(AbstractTDBMObject $object, string $tableName, Forei
134134
$this->_setPrimaryKeys($primaryKeys);
135135
if (!empty($dbRow)) {
136136
$this->dbRow = $dbRow;
137+
// @TODO (gua): might not be fully loaded
137138
$this->status = TDBMObjectStateEnum::STATE_LOADED;
138139
} else {
139140
$this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
@@ -219,6 +220,9 @@ public function _dbLoadIfNotLoaded(): void
219220
*/
220221
public function get(string $var)
221222
{
223+
if ($this->_getStatus() === TDBMObjectStateEnum::STATE_PARTIALLY_LOADED && !array_key_exists($var, $this->dbRow)) {
224+
throw new TDBMInvalidArgumentException('Cannot load `'.$var.'` in partially loaded object with data ' . print_r($this->dbRow, true));
225+
}
222226
if (!isset($this->primaryKeys[$var])) {
223227
$this->_dbLoadIfNotLoaded();
224228
}
@@ -235,16 +239,6 @@ public function set(string $var, $value): void
235239
{
236240
$this->_dbLoadIfNotLoaded();
237241

238-
/*
239-
// Ok, let's start by checking the column type
240-
$type = $this->db_connection->getColumnType($this->dbTableName, $var);
241-
242-
// Throws an exception if the type is not ok.
243-
if (!$this->db_connection->checkType($value, $type)) {
244-
throw new TDBMException("Error! Invalid value passed for attribute '$var' of table '$this->dbTableName'. Passed '$value', but expecting '$type'");
245-
}
246-
*/
247-
248242
/*if ($var == $this->getPrimaryKey() && isset($this->dbRow[$var]))
249243
throw new TDBMException("Error! Changing primary key value is forbidden.");*/
250244
$this->dbRow[$var] = $value;
@@ -275,7 +269,7 @@ public function setRef(string $foreignKeyName, AbstractTDBMObject $bean = null):
275269
*
276270
* @return AbstractTDBMObject|null
277271
*/
278-
public function getRef(string $foreignKeyName) : ?AbstractTDBMObject
272+
public function getRef(string $foreignKeyName, string $className, string $resultIteratorClass) : ?AbstractTDBMObject
279273
{
280274
if (array_key_exists($foreignKeyName, $this->references)) {
281275
return $this->references[$foreignKeyName];
@@ -303,9 +297,9 @@ public function getRef(string $foreignKeyName) : ?AbstractTDBMObject
303297

304298
// If the foreign key points to the primary key, let's use findObjectByPk
305299
if ($this->tdbmService->getPrimaryKeyColumns($foreignTableName) === $foreignColumns) {
306-
return $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true);
300+
return $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true, $className, $resultIteratorClass);
307301
} else {
308-
return $this->tdbmService->findObject($foreignTableName, $filter);
302+
return $this->tdbmService->findObject($foreignTableName, $filter, [], [], $className, $resultIteratorClass);
309303
}
310304
}
311305
}

src/InnerResultIterator.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class InnerResultIterator implements \Iterator, InnerResultIteratorInterface
4242
private $objectStorage;
4343
private $className;
4444

45+
/** @var TDBMService */
4546
private $tdbmService;
4647
private $magicSql;
4748
private $parameters;
@@ -78,7 +79,7 @@ private function __construct()
7879
*/
7980
public static function createInnerResultIterator(string $magicSql, array $parameters, ?int $limit, ?int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, LoggerInterface $logger): self
8081
{
81-
$iterator = new static();
82+
$iterator = new static(); // @TODO (gua) Should I know here if it's a partial load ? (to allow to give that state to DBRow and TDBMObject)
8283
$iterator->magicSql = $magicSql;
8384
$iterator->objectStorage = $objectStorage;
8485
$iterator->className = $className;
@@ -170,10 +171,12 @@ public function key()
170171
*/
171172
public function next()
172173
{
174+
/** @var array<string, string> $row */
173175
$row = $this->statement->fetch(\PDO::FETCH_ASSOC);
174176
if ($row) {
175177

176178
// array<tablegroup, array<table, array<column, value>>>
179+
/** @var array<string, array<string, array<string, mixed>>> $beansData */
177180
$beansData = [];
178181
foreach ($row as $key => $value) {
179182
if (!isset($this->columnDescriptors[$key])) {
@@ -201,13 +204,15 @@ public function next()
201204

202205
list($actualClassName, $mainBeanTableName, $tablesUsed) = $this->tdbmService->_getClassNameFromBeanData($beanData);
203206

204-
if ($this->className !== null) {
207+
// @TODO (gua) this is a weird hack to be able to force a TDBMObject...
208+
// ClassName could be used to override $actualClassName
209+
if ($this->className !== null && is_a($this->className, TDBMObject::class, true)) {
205210
$actualClassName = $this->className;
206211
}
207212

208213
// Let's filter out the beanData that is not used (because it belongs to a part of the hierarchy that is not fetched:
209214
foreach ($beanData as $tableName => $descriptors) {
210-
if (!in_array($tableName, $tablesUsed)) {
215+
if (!in_array($tableName, $tablesUsed, true)) {
211216
unset($beanData[$tableName]);
212217
}
213218
}

src/OrderByAnalyzer.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use Doctrine\Common\Cache\Cache;
77
use PHPSQLParser\PHPSQLParser;
8+
use PHPSQLParser\utils\ExpressionType;
89

910
/**
1011
* Class in charge of analyzing order by clauses.
@@ -85,7 +86,7 @@ private function analyzeOrderByNoCache(string $orderBy) : array
8586

8687
for ($i = 0, $count = count($parsed['ORDER']); $i < $count; ++$i) {
8788
$orderItem = $parsed['ORDER'][$i];
88-
if ($orderItem['expr_type'] === 'colref') {
89+
if ($orderItem['expr_type'] === ExpressionType::COLREF) {
8990
$parts = $orderItem['no_quotes']['parts'];
9091
$columnName = array_pop($parts);
9192
if (!empty($parts)) {
@@ -95,7 +96,7 @@ private function analyzeOrderByNoCache(string $orderBy) : array
9596
}
9697

9798
$results[] = [
98-
'type' => 'colref',
99+
'type' => ExpressionType::COLREF,
99100
'table' => $tableName,
100101
'column' => $columnName,
101102
'direction' => $orderItem['direction'],

src/PageIterator.php

+14-2
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,20 @@ private function __construct()
7676
* @param mixed[] $parameters
7777
* @param array[] $columnDescriptors
7878
*/
79-
public static function createResultIterator(ResultIterator $parentResult, string $magicSql, array $parameters, int $limit, int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, int $mode, LoggerInterface $logger): self
80-
{
79+
public static function createResultIterator(
80+
ResultIterator $parentResult,
81+
string $magicSql,
82+
array $parameters,
83+
int $limit,
84+
int $offset,
85+
array $columnDescriptors,
86+
ObjectStorageInterface $objectStorage,
87+
?string $className,
88+
TDBMService $tdbmService,
89+
MagicQuery $magicQuery,
90+
int $mode,
91+
LoggerInterface $logger
92+
): self {
8193
$iterator = new self();
8294
$iterator->parentResult = $parentResult;
8395
$iterator->magicSql = $magicSql;

src/QueryFactory/AbstractQueryFactory.php

+28-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
namespace TheCodingMachine\TDBM\QueryFactory;
55

6+
use PHPSQLParser\utils\ExpressionType;
7+
use TheCodingMachine\TDBM\ResultIterator;
68
use function array_unique;
79
use Doctrine\DBAL\Platforms\MySqlPlatform;
810
use Doctrine\DBAL\Schema\Schema;
@@ -52,6 +54,8 @@ abstract class AbstractQueryFactory implements QueryFactory
5254
* @var string
5355
*/
5456
protected $mainTable;
57+
/** @var null|ResultIterator */
58+
protected $resultIterator;
5559

5660
/**
5761
* @param TDBMService $tdbmService
@@ -68,6 +72,11 @@ public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAna
6872
$this->mainTable = $mainTable;
6973
}
7074

75+
public function setResultIterator(ResultIterator $resultIterator): void
76+
{
77+
$this->resultIterator = $resultIterator;
78+
}
79+
7180
/**
7281
* Returns the column list that must be fetched for the SQL request.
7382
*
@@ -120,7 +129,7 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc
120129
// If we sort by a column, there is a high chance we will fetch the bean containing this column.
121130
// Hence, we should add the table to the $additionalTablesFetch
122131
foreach ($orderByColumns as $orderByColumn) {
123-
if ($orderByColumn['type'] === 'colref') {
132+
if ($orderByColumn['type'] === ExpressionType::COLREF) {
124133
if ($orderByColumn['table'] !== null) {
125134
if ($canAddAdditionalTablesFetch) {
126135
$additionalTablesFetch[] = $orderByColumn['table'];
@@ -140,16 +149,16 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc
140149
$reconstructedOrderBys[] = ($orderByColumn['table'] !== null ? $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.' : '').$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' '.$orderByColumn['direction'];
141150
}
142151
} elseif ($orderByColumn['type'] === 'expr') {
152+
if ($securedOrderBy) {
153+
throw new TDBMInvalidArgumentException('Invalid ORDER BY column: "'.$orderByColumn['expr'].'". If you want to use expression in your ORDER BY clause, you must wrap them in a UncheckedOrderBy object. For instance: new UncheckedOrderBy("col1 + col2 DESC")');
154+
}
155+
143156
$sortColumnName = 'sort_column_'.$sortColumn;
144157
$columnsList[] = $orderByColumn['expr'].' as '.$sortColumnName;
145158
$columnDescList[$sortColumnName] = [
146159
'tableGroup' => null,
147160
];
148161
++$sortColumn;
149-
150-
if ($securedOrderBy) {
151-
throw new TDBMInvalidArgumentException('Invalid ORDER BY column: "'.$orderByColumn['expr'].'". If you want to use expression in your ORDER BY clause, you must wrap them in a UncheckedOrderBy object. For instance: new UncheckedOrderBy("col1 + col2 DESC")');
152-
}
153162
}
154163
}
155164

@@ -181,15 +190,20 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc
181190
foreach ($allFetchedTables as $table) {
182191
foreach ($this->schema->getTable($table)->getColumns() as $column) {
183192
$columnName = $column->getName();
184-
$columnDescList[$table.'____'.$columnName] = [
185-
'as' => $table.'____'.$columnName,
186-
'table' => $table,
187-
'column' => $columnName,
188-
'type' => $column->getType(),
189-
'tableGroup' => $tableGroups[$table],
190-
];
191-
$columnsList[] = $mysqlPlatform->quoteIdentifier($table).'.'.$mysqlPlatform->quoteIdentifier($columnName).' as '.
192-
$connection->quoteIdentifier($table.'____'.$columnName);
193+
if ($this->resultIterator === null // @TODO (gua) don't take care of whitelist in case of LIMIT below 2
194+
|| $table !== $mainTable
195+
|| $this->resultIterator->isInWhitelist($columnName, $table)
196+
) {
197+
$columnDescList[$table . '____' . $columnName] = [
198+
'as' => $table . '____' . $columnName,
199+
'table' => $table,
200+
'column' => $columnName,
201+
'type' => $column->getType(),
202+
'tableGroup' => $tableGroups[$table],
203+
];
204+
$columnsList[] = $mysqlPlatform->quoteIdentifier($table) . '.' . $mysqlPlatform->quoteIdentifier($columnName) . ' as ' .
205+
$connection->quoteIdentifier($table . '____' . $columnName);
206+
}
193207
}
194208
}
195209

0 commit comments

Comments
 (0)