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: diff --git a/README.md b/README.md index 1fbb4562eb..d6c1a6c532 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. @@ -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-09-30 - Fix the PHP 8.5 `PDO::MYSQL_ATTR_*` deprecations in the MySQL datasource: on PHP >= 8.4, `Mysql::connect()` uses the `Pdo\Mysql::ATTR_*` constants, including the SSL ones (based on kamilwylegala/cakephp2-php8#86). Apps passing `PDO::MYSQL_ATTR_*` in the datasource `flags` option should switch to `Pdo\Mysql::ATTR_*` on PHP >= 8.4 as well. diff --git a/lib/Cake/Model/Datasource/Database/Postgres.php b/lib/Cake/Model/Datasource/Database/Postgres.php index f81bee5b3b..ae7523cd49 100644 --- a/lib/Cake/Model/Datasource/Database/Postgres.php +++ b/lib/Cake/Model/Datasource/Database/Postgres.php @@ -681,9 +681,9 @@ protected function _alterIndexes($table, $indexes) { */ public function limit($limit, $offset = null) { if ($limit) { - $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 487868be4a..ab045a32c4 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlite.php +++ b/lib/Cake/Model/Datasource/Database/Sqlite.php @@ -393,9 +393,9 @@ public function fetchResult() { */ public function limit($limit, $offset = null) { if ($limit) { - $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 c25934409b..5bdb4df794 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlserver.php +++ b/lib/Cake/Model/Datasource/Database/Sqlserver.php @@ -414,9 +414,9 @@ public function limit($limit, $offset = null) { if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) { $rt = ' TOP'; } - $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 ee77fe1eb3..ba688621b2 100644 --- a/lib/Cake/Model/Datasource/DboSource.php +++ b/lib/Cake/Model/Datasource/DboSource.php @@ -3068,15 +3068,31 @@ public function limit($limit, $offset = null) { $rt = ' LIMIT'; 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); } /** 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 322b29d9e0..2d370aab9c 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -223,7 +223,12 @@ public static function cipher($text, $key) { return ''; } - 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++) {