From e863c3c500b608c9688ef37f4d2142d53684324e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 16 Dec 2025 09:06:58 +0100 Subject: [PATCH 1/6] ci(PHP): Test against 8.5 on CI Signed-off-by: Joas Schilling --- .github/workflows/lint-php.yml | 2 +- .github/workflows/phpunit-mariadb.yml | 4 +++- .github/workflows/phpunit-memcached.yml | 2 +- .github/workflows/phpunit-mysql.yml | 2 ++ .github/workflows/phpunit-nodb.yml | 2 +- .github/workflows/phpunit-oci.yml | 2 ++ .github/workflows/phpunit-pgsql.yml | 2 ++ .github/workflows/phpunit-sqlite.yml | 2 +- lib/versioncheck.php | 8 ++++---- 9 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint-php.yml b/.github/workflows/lint-php.yml index 8310c3cebf1e6..22db44fca033f 100644 --- a/.github/workflows/lint-php.yml +++ b/.github/workflows/lint-php.yml @@ -47,7 +47,7 @@ jobs: strategy: matrix: - php-versions: [ '8.2', '8.3', '8.4' ] + php-versions: [ '8.2', '8.3', '8.4', '8.5' ] name: php-lint diff --git a/.github/workflows/phpunit-mariadb.yml b/.github/workflows/phpunit-mariadb.yml index 1758fc83eac4d..e329261d55a13 100644 --- a/.github/workflows/phpunit-mariadb.yml +++ b/.github/workflows/phpunit-mariadb.yml @@ -60,13 +60,15 @@ jobs: fail-fast: false matrix: php-versions: ['8.2'] - mariadb-versions: ['10.3', '10.6', '10.11', '11.4', '11.8'] + mariadb-versions: ['10.6', '10.11', '11.4', '11.8'] include: - php-versions: '8.3' mariadb-versions: '10.11' coverage: ${{ github.event_name != 'pull_request' }} - php-versions: '8.4' mariadb-versions: '11.8' + - php-versions: '8.5' + mariadb-versions: '11.8' name: MariaDB ${{ matrix.mariadb-versions }} (PHP ${{ matrix.php-versions }}) - database tests diff --git a/.github/workflows/phpunit-memcached.yml b/.github/workflows/phpunit-memcached.yml index 16c7d91827bf5..4c1a32b64b9bd 100644 --- a/.github/workflows/phpunit-memcached.yml +++ b/.github/workflows/phpunit-memcached.yml @@ -56,7 +56,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['8.3', '8.4'] + php-versions: ['8.3', '8.4', '8.5'] include: - php-versions: '8.2' coverage: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/phpunit-mysql.yml b/.github/workflows/phpunit-mysql.yml index f0635d5a3ef70..f7de37051f609 100644 --- a/.github/workflows/phpunit-mysql.yml +++ b/.github/workflows/phpunit-mysql.yml @@ -67,6 +67,8 @@ jobs: coverage: ${{ github.event_name != 'pull_request' }} - mysql-versions: '8.4' php-versions: '8.4' + - mysql-versions: '8.4' + php-versions: '8.5' name: MySQL ${{ matrix.mysql-versions }} (PHP ${{ matrix.php-versions }}) - database tests diff --git a/.github/workflows/phpunit-nodb.yml b/.github/workflows/phpunit-nodb.yml index c23e620da204f..a6487011639b4 100644 --- a/.github/workflows/phpunit-nodb.yml +++ b/.github/workflows/phpunit-nodb.yml @@ -59,7 +59,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['8.3', '8.4'] + php-versions: ['8.3', '8.4', '8.5'] include: - php-versions: '8.2' coverage: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/phpunit-oci.yml b/.github/workflows/phpunit-oci.yml index c489810c6db23..c414dfb76e2e9 100644 --- a/.github/workflows/phpunit-oci.yml +++ b/.github/workflows/phpunit-oci.yml @@ -69,6 +69,8 @@ jobs: php-versions: '8.3' - oracle-versions: '23' php-versions: '8.4' + - oracle-versions: '23' + php-versions: '8.5' name: Oracle ${{ matrix.oracle-versions }} (PHP ${{ matrix.php-versions }}) - database tests diff --git a/.github/workflows/phpunit-pgsql.yml b/.github/workflows/phpunit-pgsql.yml index e67767f61de6b..f139ca90d4d33 100644 --- a/.github/workflows/phpunit-pgsql.yml +++ b/.github/workflows/phpunit-pgsql.yml @@ -68,6 +68,8 @@ jobs: coverage: ${{ github.event_name != 'pull_request' }} - php-versions: '8.4' postgres-versions: '18' + - php-versions: '8.5' + postgres-versions: '18' name: PostgreSQL ${{ matrix.postgres-versions }} (PHP ${{ matrix.php-versions }}) - database tests diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml index 76a66f185077b..9f92d6ba5e238 100644 --- a/.github/workflows/phpunit-sqlite.yml +++ b/.github/workflows/phpunit-sqlite.yml @@ -59,7 +59,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['8.3', '8.4'] + php-versions: ['8.3', '8.4', '8.5'] include: - php-versions: '8.2' coverage: ${{ github.event_name != 'pull_request' }} diff --git a/lib/versioncheck.php b/lib/versioncheck.php index d1073d84c4e83..dd7790ff9616e 100644 --- a/lib/versioncheck.php +++ b/lib/versioncheck.php @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -// Show warning if a PHP version below 8.1 is used, +// Show warning if a PHP version below 8.2 is used, if (PHP_VERSION_ID < 80200) { http_response_code(500); echo 'This version of Nextcloud requires at least PHP 8.2
'; @@ -13,10 +13,10 @@ exit(1); } -// Show warning if >= PHP 8.5 is used as Nextcloud is not compatible with >= PHP 8.5 for now -if (PHP_VERSION_ID >= 80500) { +// Show warning if >= PHP 8.6 is used as Nextcloud is not compatible with >= PHP 8.6 for now +if (PHP_VERSION_ID >= 80600) { http_response_code(500); - echo 'This version of Nextcloud is not compatible with PHP>=8.5.
'; + echo 'This version of Nextcloud is not compatible with PHP>=8.6.
'; echo 'You are currently running ' . PHP_VERSION . '.'; exit(1); } From 121973d336ef2c78087a2cfc09a29ebb9f1dd83f Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 17 Dec 2025 13:20:07 +0100 Subject: [PATCH 2/6] fix(logger): Fix calls to log normalizer Signed-off-by: Joas Schilling --- lib/private/Log.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/private/Log.php b/lib/private/Log.php index cbdd0f767ad80..88df3c032afb2 100644 --- a/lib/private/Log.php +++ b/lib/private/Log.php @@ -342,14 +342,14 @@ public function logException(Throwable $exception, array $context = []): void { $this->error('Failed to load ExceptionSerializer serializer while trying to log ' . $exception->getMessage()); return; } + + $context = array_map($this->normalizer->format(...), $context); $data = $context; - unset($data['app']); - unset($data['level']); + unset($data['app'], $data['level']); + $data = array_merge($serializer->serializeException($exception), $data); $data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: ' . get_class($exception)), 'CustomMessage'); - array_walk($context, [$this->normalizer, 'format']); - $this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $data)); try { @@ -374,8 +374,7 @@ public function logData(string $message, array $data, array $context = []): void $level = $context['level'] ?? ILogger::ERROR; $minLevel = $this->getLogLevel($context, $message); - - array_walk($context, [$this->normalizer, 'format']); + $data = array_map($this->normalizer->format(...), $data); try { if ($level >= $minLevel) { @@ -385,8 +384,6 @@ public function logData(string $message, array $data, array $context = []): void } $this->writeLog($app, $data, $level); } - - $context['level'] = $level; } catch (Throwable $e) { // make sure we dont hard crash if logging fails error_log('Error when trying to log exception: ' . $e->getMessage() . ' ' . $e->getTraceAsString()); From 6bc73b0dab7b91630ade18c5adc2ad991b7f1e1d Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 18 Dec 2025 08:05:13 +0100 Subject: [PATCH 3/6] fix(PDO): Switch away from deprecated PDO parts Signed-off-by: Joas Schilling --- lib/private/DB/ConnectionFactory.php | 23 +++++++++++++++++------ lib/private/DB/SQLiteSessionInit.php | 6 +++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index 2429eeb142fa3..421125603118d 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -88,12 +88,23 @@ public function getDefaultConnectionParams($type) { throw new \InvalidArgumentException("Unsupported type: $type"); } $result = $this->defaultConnectionParams[$normalizedType]; - // \PDO::MYSQL_ATTR_FOUND_ROWS may not be defined, e.g. when the MySQL - // driver is missing. In this case, we won't be able to connect anyway. - if ($normalizedType === 'mysql' && defined('\PDO::MYSQL_ATTR_FOUND_ROWS')) { - $result['driverOptions'] = [ - \PDO::MYSQL_ATTR_FOUND_ROWS => true, - ]; + /** + * {@see \PDO::MYSQL_ATTR_FOUND_ROWS} may not be defined, e.g. when the MySQL + * driver is missing. In this case, we won't be able to connect anyway. + * In PHP 8.5 it's deprecated and {@see \Pdo\Mysql::ATTR_FOUND_ROWS} should be used, + * but that is only available since PHP 8.4 + */ + if ($normalizedType === 'mysql') { + if (PHP_VERSION_ID >= 80500 && class_exists(\Pdo\Mysql::class)) { + /** @psalm-suppress UndefinedClass */ + $result['driverOptions'] = [ + \Pdo\Mysql::ATTR_FOUND_ROWS => true, + ]; + } elseif (PHP_VERSION_ID < 80500 && defined('\PDO::MYSQL_ATTR_FOUND_ROWS')) { + $result['driverOptions'] = [ + \PDO::MYSQL_ATTR_FOUND_ROWS => true, + ]; + } } return $result; } diff --git a/lib/private/DB/SQLiteSessionInit.php b/lib/private/DB/SQLiteSessionInit.php index 5fe0cb3abf64c..3e05b6e59a15c 100644 --- a/lib/private/DB/SQLiteSessionInit.php +++ b/lib/private/DB/SQLiteSessionInit.php @@ -44,7 +44,11 @@ public function postConnect(ConnectionEventArgs $args) { /** @var \Doctrine\DBAL\Driver\PDO\Connection $connection */ $connection = $args->getConnection()->getWrappedConnection(); $pdo = $connection->getWrappedConnection(); - $pdo->sqliteCreateFunction('md5', 'md5', 1); + if (PHP_VERSION_ID >= 80500 && method_exists($pdo, 'createFunction')) { + $pdo->createFunction('md5', 'md5', 1); + } else { + $pdo->sqliteCreateFunction('md5', 'md5', 1); + } } public function getSubscribedEvents() { From 56793fa5b861f8fc389dbfbb78a7b21e8d1c8d4d Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 18 Dec 2025 08:05:51 +0100 Subject: [PATCH 4/6] fix(reflection): ReflectionMethod::setAccessible is noop since 8.1 Signed-off-by: Joas Schilling --- apps/files_sharing/tests/SharedMountTest.php | 1 - tests/lib/HelperStorageTest.php | 2 -- tests/lib/TestCase.php | 5 ----- 3 files changed, 8 deletions(-) diff --git a/apps/files_sharing/tests/SharedMountTest.php b/apps/files_sharing/tests/SharedMountTest.php index bfc0016d31800..f70dae065d234 100644 --- a/apps/files_sharing/tests/SharedMountTest.php +++ b/apps/files_sharing/tests/SharedMountTest.php @@ -375,7 +375,6 @@ public function testShareMountOverShare(): void { $mountProvider = Server::get(MountProvider::class); $reflectionClass = new \ReflectionClass($mountProvider); $reflectionCacheFactory = $reflectionClass->getProperty('cacheFactory'); - $reflectionCacheFactory->setAccessible(true); $reflectionCacheFactory->setValue($mountProvider, $cacheFactory); // share to user diff --git a/tests/lib/HelperStorageTest.php b/tests/lib/HelperStorageTest.php index 81cff4a283e81..914936c9657ad 100644 --- a/tests/lib/HelperStorageTest.php +++ b/tests/lib/HelperStorageTest.php @@ -102,14 +102,12 @@ public function testGetStorageInfo(): void { private function getIncludeExternalStorage(): bool { $class = new \ReflectionClass(\OC_Helper::class); $prop = $class->getProperty('quotaIncludeExternalStorage'); - $prop->setAccessible(true); return $prop->getValue(null) ?? false; } private function setIncludeExternalStorage(bool $include) { $class = new \ReflectionClass(\OC_Helper::class); $prop = $class->getProperty('quotaIncludeExternalStorage'); - $prop->setAccessible(true); $prop->setValue(null, $include); } diff --git a/tests/lib/TestCase.php b/tests/lib/TestCase.php index 54368c93b8bcb..1b387df0eb580 100644 --- a/tests/lib/TestCase.php +++ b/tests/lib/TestCase.php @@ -248,15 +248,10 @@ protected static function invokePrivate($object, $methodName, array $parameters if ($reflection->hasMethod($methodName)) { $method = $reflection->getMethod($methodName); - - $method->setAccessible(true); - return $method->invokeArgs($object, $parameters); } elseif ($reflection->hasProperty($methodName)) { $property = $reflection->getProperty($methodName); - $property->setAccessible(true); - if (!empty($parameters)) { if ($property->isStatic()) { $property->setValue(null, array_pop($parameters)); From c27db5fdadd002f5ad7f67a3f97cd7049c208a41 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 18 Dec 2025 08:06:19 +0100 Subject: [PATCH 5/6] fix(GdImage): imagedestroy is noop since PHP 8.0 Signed-off-by: Joas Schilling --- lib/private/Image.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/private/Image.php b/lib/private/Image.php index 4eabce5e4f869..eb5812c878519 100644 --- a/lib/private/Image.php +++ b/lib/private/Image.php @@ -839,7 +839,6 @@ public function resize(int $maxSize): bool { return false; } $result = $this->resizeNew($maxSize); - imagedestroy($this->resource); $this->resource = $result; return $this->valid(); } @@ -875,7 +874,6 @@ public function preciseResize(int $width, int $height): bool { return false; } $result = $this->preciseResizeNew($width, $height); - imagedestroy($this->resource); $this->resource = $result; return $this->valid(); } From 226b7df65ec6fe562a276d67034a87bc91c238e3 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 18 Dec 2025 08:06:47 +0100 Subject: [PATCH 6/6] fix(Hooks): Don't use offset null as it's deprecated (and not actually used) Signed-off-by: Joas Schilling --- tests/lib/Files/ViewTest.php | 56 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/lib/Files/ViewTest.php b/tests/lib/Files/ViewTest.php index e7ce3ee9526cf..274f8cb6d0975 100644 --- a/tests/lib/Files/ViewTest.php +++ b/tests/lib/Files/ViewTest.php @@ -1811,13 +1811,13 @@ public static function basicOperationProviderForLocks(): array { ['touch', ['test.txt'], 'test.txt', 'touch', null, null, null], // ---- no hooks, no locks --- - ['is_dir', ['dir'], 'dir', null], - ['is_file', ['dir'], 'dir', null], + ['is_dir', ['dir'], 'dir', ''], + ['is_file', ['dir'], 'dir', ''], [ 'stat', ['dir'], 'dir', - null, + '', ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, @@ -1828,7 +1828,7 @@ public static function basicOperationProviderForLocks(): array { 'filetype', ['dir'], 'dir', - null, + '', ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, @@ -1839,7 +1839,7 @@ public static function basicOperationProviderForLocks(): array { 'filesize', ['dir'], 'dir', - null, + '', ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, @@ -1847,17 +1847,17 @@ public static function basicOperationProviderForLocks(): array { /* Return an int */ 100 ], - ['isCreatable', ['dir'], 'dir', null], - ['isReadable', ['dir'], 'dir', null], - ['isUpdatable', ['dir'], 'dir', null], - ['isDeletable', ['dir'], 'dir', null], - ['isSharable', ['dir'], 'dir', null], - ['file_exists', ['dir'], 'dir', null], + ['isCreatable', ['dir'], 'dir', ''], + ['isReadable', ['dir'], 'dir', ''], + ['isUpdatable', ['dir'], 'dir', ''], + ['isDeletable', ['dir'], 'dir', ''], + ['isSharable', ['dir'], 'dir', ''], + ['file_exists', ['dir'], 'dir', ''], [ 'filemtime', ['dir'], 'dir', - null, + '', ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, ILockingProvider::LOCK_SHARED, @@ -1875,23 +1875,23 @@ public static function basicOperationProviderForLocks(): array { * @param array $operationArgs arguments for the operation * @param string $lockedPath path of the locked item to check * @param string $hookType hook type - * @param int $expectedLockBefore expected lock during pre hooks - * @param int $expectedLockDuring expected lock during operation - * @param int $expectedLockAfter expected lock during post hooks - * @param int $expectedStrayLock expected lock after returning, should - * be null (unlock) for most operations + * @param ?int $expectedLockBefore expected lock during pre hooks + * @param ?int $expectedLockDuring expected lock during operation + * @param ?int $expectedLockAfter expected lock during post hooks + * @param ?int $expectedStrayLock expected lock after returning, should + * be null (unlock) for most operations */ #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')] public function testLockBasicOperation( - $operation, - $operationArgs, - $lockedPath, - $hookType, - $expectedLockBefore = ILockingProvider::LOCK_SHARED, - $expectedLockDuring = ILockingProvider::LOCK_SHARED, - $expectedLockAfter = ILockingProvider::LOCK_SHARED, - $expectedStrayLock = null, - $returnValue = true, + string $operation, + array $operationArgs, + string $lockedPath, + string $hookType, + ?int $expectedLockBefore = ILockingProvider::LOCK_SHARED, + ?int $expectedLockDuring = ILockingProvider::LOCK_SHARED, + ?int $expectedLockAfter = ILockingProvider::LOCK_SHARED, + ?int $expectedStrayLock = null, + mixed $returnValue = true, ): void { $view = new View('/' . $this->user . '/files/'); @@ -1931,7 +1931,7 @@ function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) { // do operation call_user_func_array([$view, $operation], $operationArgs); - if ($hookType !== null) { + if ($hookType !== '') { $this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook'); $this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook'); $this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation'); @@ -2530,7 +2530,7 @@ function () use ($view, $path, $onMountPoint, &$lockTypePost): void { } ); - if ($hookType !== null) { + if ($hookType !== '') { Util::connectHook( Filesystem::CLASSNAME, $hookType,