Skip to content

Commit 64f7ecc

Browse files
authored
Merge pull request #6883 from kenjis/fix-Model-update-null
feat: disallow `Model::update()` without WHERE clause
2 parents 8e79533 + 9b690af commit 64f7ecc

File tree

8 files changed

+103
-4
lines changed

8 files changed

+103
-4
lines changed

system/BaseModel.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,10 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch
899899
*/
900900
public function update($id = null, $data = null): bool
901901
{
902+
if (is_bool($id)) {
903+
throw new InvalidArgumentException('update(): argument #1 ($id) should not be boolean.');
904+
}
905+
902906
if (is_numeric($id) || is_string($id)) {
903907
$id = [$id];
904908
}
@@ -910,12 +914,12 @@ public function update($id = null, $data = null): bool
910914
return false;
911915
}
912916

913-
// Must be called first so we don't
917+
// Must be called first, so we don't
914918
// strip out updated_at values.
915919
$data = $this->doProtectFields($data);
916920

917921
// doProtectFields() can further remove elements from
918-
// $data so we need to check for empty dataset again
922+
// $data, so we need to check for empty dataset again
919923
if (empty($data)) {
920924
throw DataException::forEmptyDataset('update');
921925
}
@@ -1037,6 +1041,10 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc
10371041
*/
10381042
public function delete($id = null, bool $purge = false)
10391043
{
1044+
if (is_bool($id)) {
1045+
throw new InvalidArgumentException('delete(): argument #1 ($id) should not be boolean.');
1046+
}
1047+
10401048
if ($id && (is_numeric($id) || is_string($id))) {
10411049
$id = [$id];
10421050
}

system/Config/Services.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ public static function image(?string $handler = null, ?Images $config = null, bo
332332
}
333333

334334
$config ??= config('Images');
335+
assert($config instanceof Images);
336+
335337
$handler = $handler ?: $config->defaultHandler;
336338
$class = $config->handlers[$handler];
337339

@@ -639,6 +641,8 @@ public static function session(?App $config = null, bool $getShared = true)
639641
}
640642

641643
$config ??= config('App');
644+
assert($config instanceof App);
645+
642646
$logger = AppServices::logger();
643647

644648
$driverName = $config->sessionDriver;

system/Model.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,12 @@ protected function doUpdate($id = null, $data = null): bool
386386
$builder->set($key, $val, $escape[$key] ?? null);
387387
}
388388

389+
if ($builder->getCompiledQBWhere() === []) {
390+
throw new DatabaseException(
391+
'Updates are not allowed unless they contain a "where" or "like" clause.'
392+
);
393+
}
394+
389395
return $builder->update();
390396
}
391397

@@ -589,7 +595,7 @@ public function countAllResults(bool $reset = true, bool $test = false)
589595
}
590596

591597
// When $reset === false, the $tempUseSoftDeletes will be
592-
// dependant on $useSoftDeletes value because we don't
598+
// dependent on $useSoftDeletes value because we don't
593599
// want to add the same "where" condition for the second time
594600
$this->tempUseSoftDeletes = $reset
595601
? $this->useSoftDeletes

tests/system/Models/DeleteModelTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,42 @@ public function testPurgeDeletedWithSoftDeleteFalse(): void
239239
$jobs = $this->model->findAll();
240240
$this->assertCount(4, $jobs);
241241
}
242+
243+
/**
244+
* @dataProvider emptyPkValues
245+
*
246+
* @param int|string|null $id
247+
*/
248+
public function testDeleteThrowDatabaseExceptionWithoutWhereClause($id): void
249+
{
250+
// BaseBuilder throws Exception.
251+
$this->expectException(DatabaseException::class);
252+
$this->expectExceptionMessage(
253+
'Deletes are not allowed unless they contain a "where" or "like" clause.'
254+
);
255+
256+
// $useSoftDeletes = false
257+
$this->createModel(JobModel::class);
258+
259+
$this->model->delete($id);
260+
}
261+
262+
/**
263+
* @dataProvider emptyPkValues
264+
*
265+
* @param int|string|null $id
266+
*/
267+
public function testDeleteWithSoftDeleteThrowDatabaseExceptionWithoutWhereClause($id): void
268+
{
269+
// Model throws Exception.
270+
$this->expectException(DatabaseException::class);
271+
$this->expectExceptionMessage(
272+
'Deletes are not allowed unless they contain a "where" or "like" clause.'
273+
);
274+
275+
// $useSoftDeletes = true
276+
$this->createModel(UserModel::class);
277+
278+
$this->model->delete($id);
279+
}
242280
}

tests/system/Models/UpdateModelTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace CodeIgniter\Models;
1313

14+
use CodeIgniter\Database\Exceptions\DatabaseException;
1415
use CodeIgniter\Database\Exceptions\DataException;
1516
use CodeIgniter\Entity\Entity;
17+
use Generator;
18+
use InvalidArgumentException;
1619
use stdClass;
1720
use Tests\Support\Models\EventModel;
1821
use Tests\Support\Models\JobModel;
@@ -378,4 +381,36 @@ public function testUpdateWithSetAndEscape(): void
378381
'email' => '1+1',
379382
]);
380383
}
384+
385+
/**
386+
* @dataProvider provideInvalidIds
387+
*
388+
* @param false|null $id
389+
*/
390+
public function testUpdateThrowDatabaseExceptionWithoutWhereClause($id, string $exception, string $exceptionMessage): void
391+
{
392+
$this->expectException($exception);
393+
$this->expectExceptionMessage($exceptionMessage);
394+
395+
// $useSoftDeletes = false
396+
$this->createModel(JobModel::class);
397+
398+
$this->model->update($id, ['name' => 'Foo Bar']);
399+
}
400+
401+
public function provideInvalidIds(): Generator
402+
{
403+
yield from [
404+
[
405+
null,
406+
DatabaseException::class,
407+
'Updates are not allowed unless they contain a "where" or "like" clause.',
408+
],
409+
[
410+
false,
411+
InvalidArgumentException::class,
412+
'update(): argument #1 ($id) should not be boolean.',
413+
],
414+
];
415+
}
381416
}

user_guide_src/source/changelogs/v4.3.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Others
8181
- **Database:** ``InvalidArgumentException`` that is a kind of ``LogicException`` in ``BaseBuilder::_whereIn()`` is not suppressed by the configuration. Previously if ``CI_DEBUG`` was false, the exception was suppressed.
8282
- **Database:** The data structure returned by :ref:`BaseConnection::getForeignKeyData() <metadata-getforeignkeydata>` has been changed.
8383
- **Database:** ``CodeIgniter\Database\BasePreparedQuery`` class returns now a bool value for write-type queries instead of the ``Result`` class object.
84+
- **Model:** ``Model::update()`` method now raises a ``DatabaseException`` if it generates an SQL
85+
statement without a WHERE clause; Model does not support operations that update all records.
8486
- **Routing:** ``RouteCollection::resetRoutes()`` resets Auto-Discovery of Routes. Previously once discovered, RouteCollection never discover Routes files again even if ``RouteCollection::resetRoutes()`` is called.
8587

8688
.. _v430-interface-changes:

user_guide_src/source/installation/upgrade_430.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ Database
210210
- The second parameter ``$index`` of ``BaseBuilder::updateBatch()`` has changed to ``$constraints``. It now accepts types array, string, or ``RawSql``. Extending classes should likewise change types.
211211
- The ``$set`` parameter of ``BaseBuilder::insertBatch()`` and ``BaseBuilder::updateBatch()`` now accepts an object of a single row of data. Extending classes should likewise change the type.
212212
- The third parameter ``$index`` of ``BaseBuilder::_updateBatch()`` has changed to ``$values``, and the parameter type has changed to ``array``. Extending classes should likewise change the type.
213+
- The ``Model::update()`` method now raises a ``DatabaseException`` if it generates an SQL
214+
statement without a WHERE clause. If you need to update all records in a table, use Query Builder instead. E.g., ``$model->builder()->update($data)``.
213215

214216
Others
215217
======

user_guide_src/source/models/model.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,11 @@ of the columns in a ``$table``, while the array's values are the values to save
353353

354354
.. literalinclude:: model/016.php
355355

356-
.. important:: If the ``$primaryKey`` field is set to ``null`` then the update will affect all records in the table.
356+
.. important:: Since v4.3.0, this method raises a ``DatabaseException``
357+
if it generates an SQL statement without a WHERE clause.
358+
In previous versions, if it is called without ``$primaryKey`` specified and
359+
an SQL statement was generated without a WHERE clause, the query would still
360+
execute and all records in the table would be updated.
357361

358362
Multiple records may be updated with a single call by passing an array of primary keys as the first parameter:
359363

0 commit comments

Comments
 (0)