Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 76 additions & 14 deletions system/BaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Exceptions\DataException;
use CodeIgniter\Database\Query;
use CodeIgniter\Database\RawSql;
use CodeIgniter\DataCaster\Cast\CastInterface;
use CodeIgniter\DataConverter\DataConverter;
use CodeIgniter\Entity\Cast\CastInterface as EntityCastInterface;
Expand Down Expand Up @@ -780,6 +781,67 @@ public function getInsertID()
return is_numeric($this->insertID) ? (int) $this->insertID : $this->insertID;
}

/**
* Validates that the primary key values are valid for update/delete/insert operations.
* Throws exception if invalid.
*
* @param int|list<int|string>|RawSql|string $id
* @param bool $allowArray Whether to allow array of IDs (true for update/delete, false for insert)
*
* @throws InvalidArgumentException
*/
protected function validateID($id, bool $allowArray = true): void
{
if (is_array($id)) {
// Check if arrays are allowed
if (! $allowArray) {
throw new InvalidArgumentException(
'Invalid primary key: only a single value is allowed, not an array.',
);
}

// Check for empty array
if ($id === []) {
throw new InvalidArgumentException('Invalid primary key: cannot be an empty array.');
}

// Validate each ID in the array recursively
foreach ($id as $key => $valueId) {
if (is_array($valueId)) {
throw new InvalidArgumentException(
sprintf('Invalid primary key at index %s: nested arrays are not allowed.', $key),
);
}

// Recursive call for each value (single values only in recursion)
$this->validateID($valueId, false);
}

return;
}

// Allow RawSql objects for complex scenarios
if ($id instanceof RawSql) {
return;
}

// Check for invalid single values
if (in_array($id, [null, 0, '0', '', true, false], true)) {
$type = is_bool($id) ? 'boolean ' . var_export($id, true) : var_export($id, true);

throw new InvalidArgumentException(
sprintf('Invalid primary key: %s is not allowed.', $type),
);
}

// Only allow int and string at this point
if (! is_int($id) && ! is_string($id)) {
throw new InvalidArgumentException(
sprintf('Invalid primary key: must be int or string, %s given.', get_debug_type($id)),
);
}
}

/**
* Inserts data into the database. If an object is provided,
* it will attempt to convert it to an array.
Expand Down Expand Up @@ -962,19 +1024,19 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
* Updates a single record in the database. If an object is provided,
* it will attempt to convert it into an array.
*
* @param int|list<int|string>|string|null $id
* @param object|row_array|null $row
* @param int|list<int|string>|RawSql|string|null $id
* @param object|row_array|null $row
*
* @throws ReflectionException
*/
public function update($id = null, $row = null): bool
{
if (is_bool($id)) {
throw new InvalidArgumentException('update(): argument #1 ($id) should not be boolean.');
}
if ($id !== null) {
if (! is_array($id)) {
$id = [$id];
}

if (is_numeric($id) || is_string($id)) {
$id = [$id];
$this->validateID($id);
}

$row = $this->transformDataToArray($row, 'update');
Expand Down Expand Up @@ -1091,21 +1153,21 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc
/**
* Deletes a single record from the database where $id matches.
*
* @param int|list<int|string>|string|null $id The rows primary key(s).
* @param bool $purge Allows overriding the soft deletes setting.
* @param int|list<int|string>|RawSql|string|null $id The rows primary key(s).
* @param bool $purge Allows overriding the soft deletes setting.
*
* @return bool|string Returns a SQL string if in test mode.
*
* @throws DatabaseException
*/
public function delete($id = null, bool $purge = false)
{
if (is_bool($id)) {
throw new InvalidArgumentException('delete(): argument #1 ($id) should not be boolean.');
}
if ($id !== null) {
if (! is_array($id)) {
$id = [$id];
}

if (! in_array($id, [null, 0, '0'], true) && (is_numeric($id) || is_string($id))) {
$id = [$id];
$this->validateID($id);
}

$eventData = [
Expand Down
16 changes: 12 additions & 4 deletions system/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,13 @@ protected function doInsert(array $row)

// Require non-empty primaryKey when
// not using auto-increment feature
if (! $this->useAutoIncrement && ! isset($row[$this->primaryKey])) {
throw DataException::forEmptyPrimaryKey('insert');
if (! $this->useAutoIncrement) {
if (! isset($row[$this->primaryKey])) {
throw DataException::forEmptyPrimaryKey('insert');
}

// Validate the primary key value (arrays not allowed for insert)
$this->validateID($row[$this->primaryKey], false);
}

$builder = $this->builder();
Expand Down Expand Up @@ -368,6 +373,9 @@ protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $
if (! isset($row[$this->primaryKey])) {
throw DataException::forEmptyPrimaryKey('insertBatch');
}

// Validate the primary key value
$this->validateID($row[$this->primaryKey], false);
}
}

Expand All @@ -381,7 +389,7 @@ protected function doUpdate($id = null, $row = null): bool

$builder = $this->builder();

if (! in_array($id, [null, '', 0, '0', []], true)) {
if (is_array($id) && $id !== []) {
$builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
}

Expand Down Expand Up @@ -409,7 +417,7 @@ protected function doDelete($id = null, bool $purge = false)
$set = [];
$builder = $this->builder();

if (! in_array($id, [null, '', 0, '0', []], true)) {
if (is_array($id) && $id !== []) {
$builder = $builder->whereIn($this->primaryKey, $id);
}

Expand Down
Loading
Loading