From 98403819a61a6e2f131a819cedc6693b89d062c9 Mon Sep 17 00:00:00 2001 From: 77web Date: Tue, 29 Sep 2026 20:43:10 +0900 Subject: [PATCH 1/6] Added build config for php8.5 --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f728d50f4a..bfbae184b2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,8 @@ jobs: db-type: sqlite - php-version: '8.4' db-type: mysql + - php-version: '8.5' + db-type: mysql services: mysql: From 7e8aa0430b873e4039cc410bae45338311aae1c2 Mon Sep 17 00:00:00 2001 From: 77web Date: Tue, 29 Sep 2026 20:45:10 +0900 Subject: [PATCH 2/6] Updated readme with the latest CI version: php8.5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51dff561c5..63ce407ce9 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Here are steps I took to migrate my project through all versions to PHP 8.1, may ## Before using this fork ⚠️ -- ~~Tests of CakePHP framework aren't refactored yet to support PHP 8. Main issue is old version of PHPUnit that is tightly coupled to framework's tests. Issue for fixing this situation is here: https://github.com/kamilwylegala/cakephp2-php8/issues/7~~ Framework tests are migrated to PHPUnit 9.*. Github actions are running tests on PHP 8.0, 8.4. +- ~~Tests of CakePHP framework aren't refactored yet to support PHP 8. Main issue is old version of PHPUnit that is tightly coupled to framework's tests. Issue for fixing this situation is here: https://github.com/kamilwylegala/cakephp2-php8/issues/7~~ Framework tests are migrated to PHPUnit 9.*. Github actions are running tests on PHP 8.0, 8.4, 8.5. - ~~Due to lack of tests ☝️~~ - **you also need to rely** on tests in your application after integrating with this fork. - If after integration you spot any issues related to framework please let me know by creating an issue or pull request with fix. From 9605558bc4b1feb2b68767e5694205b0ee20971c Mon Sep 17 00:00:00 2001 From: 77web Date: Tue, 29 Sep 2026 21:23:16 +0900 Subject: [PATCH 3/6] Suppress PHP 8.5 warnings on out-of-range float to int casts PHP 8.5 emits "The float ... is not representable as an int" when a float beyond PHP_INT_MAX is cast to int. The resulting values are unchanged from earlier versions, so silence the warning while keeping the existing behavior: - Security::cipher(): cipherSeed exceeds PHP_INT_MAX; the seed must stay the same to decrypt existing data. - DboSource/Postgres/Sqlite/Sqlserver::limit(): sprintf('%u') on huge limit/offset values. Co-Authored-By: Claude Opus 5.5 --- lib/Cake/Model/Datasource/Database/Postgres.php | 5 +++-- lib/Cake/Model/Datasource/Database/Sqlite.php | 5 +++-- lib/Cake/Model/Datasource/Database/Sqlserver.php | 5 +++-- lib/Cake/Model/Datasource/DboSource.php | 5 +++-- lib/Cake/Utility/Security.php | 4 +++- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/Cake/Model/Datasource/Database/Postgres.php b/lib/Cake/Model/Datasource/Database/Postgres.php index f81bee5b3b..865b264641 100644 --- a/lib/Cake/Model/Datasource/Database/Postgres.php +++ b/lib/Cake/Model/Datasource/Database/Postgres.php @@ -681,9 +681,10 @@ protected function _alterIndexes($table, $indexes) { */ public function limit($limit, $offset = null) { if ($limit) { - $rt = sprintf(' LIMIT %u', $limit); + // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. + $rt = @sprintf(' LIMIT %u', $limit); if ($offset) { - $rt .= sprintf(' OFFSET %u', $offset); + $rt .= @sprintf(' OFFSET %u', $offset); } return $rt; } diff --git a/lib/Cake/Model/Datasource/Database/Sqlite.php b/lib/Cake/Model/Datasource/Database/Sqlite.php index 487868be4a..8f207d4db4 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlite.php +++ b/lib/Cake/Model/Datasource/Database/Sqlite.php @@ -393,9 +393,10 @@ public function fetchResult() { */ public function limit($limit, $offset = null) { if ($limit) { - $rt = sprintf(' LIMIT %u', $limit); + // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. + $rt = @sprintf(' LIMIT %u', $limit); if ($offset) { - $rt .= sprintf(' OFFSET %u', $offset); + $rt .= @sprintf(' OFFSET %u', $offset); } return $rt; } diff --git a/lib/Cake/Model/Datasource/Database/Sqlserver.php b/lib/Cake/Model/Datasource/Database/Sqlserver.php index c25934409b..e02dac7c18 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlserver.php +++ b/lib/Cake/Model/Datasource/Database/Sqlserver.php @@ -414,9 +414,10 @@ public function limit($limit, $offset = null) { if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) { $rt = ' TOP'; } - $rt .= sprintf(' %u', $limit); + // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. + $rt .= @sprintf(' %u', $limit); if ((is_int($offset) || ctype_digit($offset)) && $offset > 0) { - $rt = sprintf(' OFFSET %u ROWS FETCH FIRST %u ROWS ONLY', $offset, $limit); + $rt = @sprintf(' OFFSET %u ROWS FETCH FIRST %u ROWS ONLY', $offset, $limit); } return $rt; } diff --git a/lib/Cake/Model/Datasource/DboSource.php b/lib/Cake/Model/Datasource/DboSource.php index ee77fe1eb3..6170c0348d 100644 --- a/lib/Cake/Model/Datasource/DboSource.php +++ b/lib/Cake/Model/Datasource/DboSource.php @@ -3067,11 +3067,12 @@ public function limit($limit, $offset = null) { if ($limit) { $rt = ' LIMIT'; + // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. if ($offset) { - $rt .= sprintf(' %u,', $offset); + $rt .= @sprintf(' %u,', $offset); } - $rt .= sprintf(' %u', $limit); + $rt .= @sprintf(' %u', $limit); return $rt; } return null; diff --git a/lib/Cake/Utility/Security.php b/lib/Cake/Utility/Security.php index 322b29d9e0..ca46ab89e9 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -223,7 +223,9 @@ public static function cipher($text, $key) { return ''; } - srand((int)(float)Configure::read('Security.cipherSeed')); + // cipherSeed usually exceeds PHP_INT_MAX. PHP 8.5+ warns on the lossy cast, + // but the resulting seed must stay the same to decrypt existing data. + srand(@(int)(float)Configure::read('Security.cipherSeed')); $out = ''; $keyLength = strlen($key); for ($i = 0, $textLength = strlen($text); $i < $textLength; $i++) { From 3ae84fc96d893af1c0181f80104b78f4c2bb520f Mon Sep 17 00:00:00 2001 From: 77web Date: Sat, 3 Oct 2026 18:00:20 +0900 Subject: [PATCH 4/6] Avoid out-of-range float to int cast in Security::cipher() Reduce the seed with fmod() instead of silencing the cast with @. mt_srand() only uses the low 32 bits of the seed, so the effective seed and the ciphertext stay the same. Add known-answer tests so that a changed seed can no longer pass unnoticed. Co-Authored-By: Claude Opus 5.5 --- lib/Cake/Test/Case/Utility/SecurityTest.php | 28 +++++++++++++++++++++ lib/Cake/Utility/Security.php | 9 ++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/Cake/Test/Case/Utility/SecurityTest.php b/lib/Cake/Test/Case/Utility/SecurityTest.php index aa6d2af042..8df6ae61aa 100644 --- a/lib/Cake/Test/Case/Utility/SecurityTest.php +++ b/lib/Cake/Test/Case/Utility/SecurityTest.php @@ -266,6 +266,34 @@ public function testCipher() { $this->assertEquals($txt, Security::cipher($result, $key)); } +/** + * Known-answer vectors for Security::cipher(), generated with the former (int)(float) + * seed cast (identical on PHP 8.0, 8.4 and 8.5): a seed within the int range, one + * beyond PHP_INT_MAX and the default seed. + * + * @return array + */ + public static function cipherKnownAnswerProvider() { + return array( + array('1234567890', 'fff77dcb883732592dd235ad622645e18405cb'), + array('12345678901234567890', 'b209c722829c974771a534cebda99e52d70f26'), + array('76859309657453542496749683645', 'a0236698a8b6c059f17f023f2666bfbe328fee'), + ); + } + +/** + * Test that Security::cipher() output does not change, so existing data can still be decrypted. + * + * @dataProvider cipherKnownAnswerProvider + * @param string $seed Security.cipherSeed value. + * @param string $expected Expected ciphertext as hex. + * @return void + */ + public function testCipherKnownAnswer($seed, $expected) { + Configure::write('Security.cipherSeed', $seed); + $this->assertSame($expected, bin2hex(Security::cipher('The quick brown fox', 'my_key'))); + } + /** * testCipherEmptyKey method * diff --git a/lib/Cake/Utility/Security.php b/lib/Cake/Utility/Security.php index ca46ab89e9..2d370aab9c 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -223,9 +223,12 @@ public static function cipher($text, $key) { return ''; } - // cipherSeed usually exceeds PHP_INT_MAX. PHP 8.5+ warns on the lossy cast, - // but the resulting seed must stay the same to decrypt existing data. - srand(@(int)(float)Configure::read('Security.cipherSeed')); + // mt_srand() only uses the low 32 bits of the seed, and fmod() is exact. This keeps + // the seed of the former (int)(float) cast without casting an out-of-range float + // to int (the seed usually exceeds PHP_INT_MAX, and PHP 8.5+ warns on such casts). + // A non-finite seed maps to 0, as that cast did. + $seed = (float)Configure::read('Security.cipherSeed'); + srand(is_finite($seed) ? (int)fmod($seed, 4294967296) : 0); $out = ''; $keyLength = strlen($key); for ($i = 0, $textLength = strlen($text); $i < $textLength; $i++) { From 9f7eb80df4ecd6b980b2d1cac388ec8ead762e4a Mon Sep 17 00:00:00 2001 From: 77web Date: Sat, 3 Oct 2026 18:00:20 +0900 Subject: [PATCH 5/6] Clamp float limit/offset values beyond PHP_INT_MAX instead of silencing sprintf('%u') wraps such floats around (2^64 becomes 0), so e.g. a huge page fetched the first rows. Clamp them to PHP_INT_MAX, which also avoids the PHP 8.5 warning, and assert the exact result in the limit tests. Co-Authored-By: Claude Opus 5.5 --- .../Model/Datasource/Database/Postgres.php | 5 ++--- lib/Cake/Model/Datasource/Database/Sqlite.php | 5 ++--- .../Model/Datasource/Database/Sqlserver.php | 5 ++--- lib/Cake/Model/Datasource/DboSource.php | 21 ++++++++++++++++--- .../Datasource/Database/PostgresTest.php | 10 +++++++-- .../Model/Datasource/Database/SqliteTest.php | 10 +++++++-- .../Case/Model/Datasource/DboSourceTest.php | 10 +++++++-- 7 files changed, 48 insertions(+), 18 deletions(-) diff --git a/lib/Cake/Model/Datasource/Database/Postgres.php b/lib/Cake/Model/Datasource/Database/Postgres.php index 865b264641..ae7523cd49 100644 --- a/lib/Cake/Model/Datasource/Database/Postgres.php +++ b/lib/Cake/Model/Datasource/Database/Postgres.php @@ -681,10 +681,9 @@ protected function _alterIndexes($table, $indexes) { */ public function limit($limit, $offset = null) { if ($limit) { - // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. - $rt = @sprintf(' LIMIT %u', $limit); + $rt = sprintf(' LIMIT %u', $this->_clampLimitValue($limit)); if ($offset) { - $rt .= @sprintf(' OFFSET %u', $offset); + $rt .= sprintf(' OFFSET %u', $this->_clampLimitValue($offset)); } return $rt; } diff --git a/lib/Cake/Model/Datasource/Database/Sqlite.php b/lib/Cake/Model/Datasource/Database/Sqlite.php index 8f207d4db4..ab045a32c4 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlite.php +++ b/lib/Cake/Model/Datasource/Database/Sqlite.php @@ -393,10 +393,9 @@ public function fetchResult() { */ public function limit($limit, $offset = null) { if ($limit) { - // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. - $rt = @sprintf(' LIMIT %u', $limit); + $rt = sprintf(' LIMIT %u', $this->_clampLimitValue($limit)); if ($offset) { - $rt .= @sprintf(' OFFSET %u', $offset); + $rt .= sprintf(' OFFSET %u', $this->_clampLimitValue($offset)); } return $rt; } diff --git a/lib/Cake/Model/Datasource/Database/Sqlserver.php b/lib/Cake/Model/Datasource/Database/Sqlserver.php index e02dac7c18..5bdb4df794 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlserver.php +++ b/lib/Cake/Model/Datasource/Database/Sqlserver.php @@ -414,10 +414,9 @@ public function limit($limit, $offset = null) { if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) { $rt = ' TOP'; } - // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. - $rt .= @sprintf(' %u', $limit); + $rt .= sprintf(' %u', $this->_clampLimitValue($limit)); if ((is_int($offset) || ctype_digit($offset)) && $offset > 0) { - $rt = @sprintf(' OFFSET %u ROWS FETCH FIRST %u ROWS ONLY', $offset, $limit); + $rt = sprintf(' OFFSET %u ROWS FETCH FIRST %u ROWS ONLY', $this->_clampLimitValue($offset), $this->_clampLimitValue($limit)); } return $rt; } diff --git a/lib/Cake/Model/Datasource/DboSource.php b/lib/Cake/Model/Datasource/DboSource.php index 6170c0348d..ba688621b2 100644 --- a/lib/Cake/Model/Datasource/DboSource.php +++ b/lib/Cake/Model/Datasource/DboSource.php @@ -3067,17 +3067,32 @@ public function limit($limit, $offset = null) { if ($limit) { $rt = ' LIMIT'; - // Values beyond PHP_INT_MAX trigger a warning on PHP 8.5+ when cast by %u. if ($offset) { - $rt .= @sprintf(' %u,', $offset); + $rt .= sprintf(' %u,', $this->_clampLimitValue($offset)); } - $rt .= @sprintf(' %u', $limit); + $rt .= sprintf(' %u', $this->_clampLimitValue($limit)); return $rt; } return null; } +/** + * Clamps a float LIMIT/OFFSET value that does not fit in an int. + * + * sprintf('%u') wraps such floats around (2^64 becomes 0), and PHP 8.5+ warns about + * that cast. Numeric strings are already saturated by the same cast. + * + * @param mixed $value Limit or offset value. + * @return mixed PHP_INT_MAX for floats that do not fit in an int, $value otherwise. + */ + protected function _clampLimitValue($value) { + if (is_float($value) && $value >= PHP_INT_MAX) { + return PHP_INT_MAX; + } + return $value; + } + /** * Returns an ORDER BY clause as a string. * diff --git a/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php b/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php index ed499e077d..f5fad0f7cf 100644 --- a/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/Database/PostgresTest.php @@ -1150,8 +1150,14 @@ public function testLimit() { $this->assertEquals(' LIMIT 20 OFFSET 10', $result); $result = $db->limit(10, 300000000000000000000000000000); - $scientificNotation = sprintf('%.1E', 300000000000000000000000000000); - $this->assertStringNotContainsString($scientificNotation, $result); + $this->assertSame(' LIMIT 10 OFFSET ' . PHP_INT_MAX, $result); + + // Offset of a page clamped to PHP_INT_MAX by PaginatorComponent. It must not wrap around to 0. + $result = $db->limit(20, (PHP_INT_MAX - 1) * 20); + $this->assertSame(' LIMIT 20 OFFSET ' . PHP_INT_MAX, $result); + + $result = $db->limit(18446744073709551615); + $this->assertSame(' LIMIT ' . PHP_INT_MAX, $result); } /** diff --git a/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php b/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php index 09d552e3ad..356d0eab1a 100644 --- a/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php @@ -595,8 +595,14 @@ public function testLimit() { $this->assertEquals(' LIMIT 20 OFFSET 10', $result); $result = $db->limit(10, 300000000000000000000000000000); - $scientificNotation = sprintf('%.1E', 300000000000000000000000000000); - $this->assertStringNotContainsString($scientificNotation, $result); + $this->assertSame(' LIMIT 10 OFFSET ' . PHP_INT_MAX, $result); + + // Offset of a page clamped to PHP_INT_MAX by PaginatorComponent. It must not wrap around to 0. + $result = $db->limit(20, (PHP_INT_MAX - 1) * 20); + $this->assertSame(' LIMIT 20 OFFSET ' . PHP_INT_MAX, $result); + + $result = $db->limit(18446744073709551615); + $this->assertSame(' LIMIT ' . PHP_INT_MAX, $result); } /** diff --git a/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php b/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php index 802cefc085..738fc66ec1 100644 --- a/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/DboSourceTest.php @@ -1783,8 +1783,14 @@ public function testLimit() { $this->assertEquals(' LIMIT 10, 20', $result); $result = $db->limit(10, 300000000000000000000000000000); - $scientificNotation = sprintf('%.1E', 300000000000000000000000000000); - $this->assertStringNotContainsString($scientificNotation, $result); + $this->assertSame(' LIMIT ' . PHP_INT_MAX . ', 10', $result); + + // Offset of a page clamped to PHP_INT_MAX by PaginatorComponent. It must not wrap around to 0. + $result = $db->limit(20, (PHP_INT_MAX - 1) * 20); + $this->assertSame(' LIMIT ' . PHP_INT_MAX . ', 20', $result); + + $result = $db->limit(18446744073709551615); + $this->assertSame(' LIMIT ' . PHP_INT_MAX, $result); } /** From 37199a7f595df3055043b51ac160131db8f79069 Mon Sep 17 00:00:00 2001 From: 77web Date: Sat, 3 Oct 2026 18:00:20 +0900 Subject: [PATCH 6/6] Add changelog entry for the PHP 8.5 fixes Co-Authored-By: Claude Opus 5.5 --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 63ce407ce9..96d5cc8163 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,13 @@ It means that composer will look at `master` branch of repository configured und ## Changelog +### 2026-10-03 + +- Fixes for PHP 8.5: avoided out-of-range float to int casts, which warn on PHP 8.5. + - `Security::cipher()` reduces `Security.cipherSeed` with `fmod()` before seeding. The effective seed and the ciphertext are unchanged, so existing data (e.g. `CookieComponent` cookies) can still be decrypted. + - `DboSource::limit()` and the `Postgres` / `Sqlite` / `Sqlserver` overrides clamp float limit / offset values that do not fit in an int to `PHP_INT_MAX`. BC note: such floats (≥ 2^63) used to wrap around in the generated SQL (e.g. 2^64 became `0`); they now become `PHP_INT_MAX`. +- Added PHP 8.5 to CI. + ### 2026-08-26 - Security: backported the view template path containment check from CakePHP 4.5.11 (CVE-2026-48820 / GHSA-wpvj-hjcr-h3p2). Element / view / layout names resolving outside the configured view template paths now throw `InvalidArgumentException`. BC note: `elementExists()` now throws for such names instead of returning `false`.