From f7eab2f75e4b7d69d7f9e2abba95711e8f827ede Mon Sep 17 00:00:00 2001 From: provokateurin Date: Wed, 4 Mar 2026 09:48:17 +0100 Subject: [PATCH 1/3] chore(psalm): Update stubs Signed-off-by: provokateurin --- lib/ACL/ACLStorageWrapper.php | 2 +- lib/DAV/ACLPlugin.php | 16 ++- lib/Mount/GroupMountPoint.php | 3 + lib/Mount/RootPermissionsMask.php | 4 +- psalm.xml | 2 + tests/psalm-baseline.xml | 16 --- tests/stubs/oc.php | 2 + .../oc_appframework_ocs_baseresponse.php | 25 +--- .../stubs/oc_appframework_ocs_v1response.php | 11 +- ...c_appframework_utility_simplecontainer.php | 37 +++--- .../stubs/oc_db_querybuilder_querybuilder.php | 46 +++---- .../oc_db_querybuilder_typedquerybuilder.php | 26 ++++ tests/stubs/oc_files_cache_cache.php | 30 ++--- tests/stubs/oc_files_cache_cacheentry.php | 9 +- tests/stubs/oc_files_cache_scanner.php | 50 +++----- .../oc_files_cache_wrapper_cachejail.php | 3 +- .../oc_files_cache_wrapper_cachewrapper.php | 19 ++- tests/stubs/oc_files_filesystem.php | 30 +++-- tests/stubs/oc_files_mount_mountpoint.php | 46 +++---- tests/stubs/oc_files_node_lazyfolder.php | 14 +- ...c_files_objectstore_objectstorestorage.php | 16 ++- tests/stubs/oc_files_setupmanager.php | 50 ++++++-- tests/stubs/oc_files_storage_common.php | 19 +-- tests/stubs/oc_files_storage_local.php | 5 +- tests/stubs/oc_files_storage_storage.php | 8 +- tests/stubs/oc_files_storage_wrapper_jail.php | 10 +- ..._files_storage_wrapper_permissionsmask.php | 6 +- .../oc_files_storage_wrapper_wrapper.php | 34 +++-- tests/stubs/oc_files_view.php | 25 +++- tests/stubs/oc_group_database.php | 1 + tests/stubs/oc_group_manager.php | 27 ++-- tests/stubs/oc_hooks_basicemitter.php | 2 + tests/stubs/oc_hooks_emitter.php | 2 + tests/stubs/oc_server.php | 114 ++++++++++------- tests/stubs/oc_servercontainer.php | 7 +- tests/stubs/oc_settings_authorizedgroup.php | 33 +++++ .../oc_settings_authorizedgroupmapper.php | 27 ++-- tests/stubs/oc_user_user.php | 102 ++++++--------- tests/stubs/oca_circles_circlesmanager.php | 2 +- .../stubs/oca_circles_db_corequerybuilder.php | 14 +- ..._circles_tools_db_extendedquerybuilder.php | 4 +- .../oca_dav_connector_sabre_directory.php | 13 +- tests/stubs/oca_dav_connector_sabre_node.php | 113 ++++++++--------- .../oca_dav_connector_sabre_principal.php | 6 +- ...oca_files_trashbin_trash_itrashbackend.php | 2 + .../oca_files_trashbin_trash_itrashitem.php | 2 + ...les_versions_versions_imetadataversion.php | 2 +- ...ettings_service_authorizedgroupservice.php | 25 +++- tests/stubs/test_testcase.php | 120 +++++------------- 49 files changed, 611 insertions(+), 571 deletions(-) create mode 100644 tests/stubs/oc_db_querybuilder_typedquerybuilder.php create mode 100644 tests/stubs/oc_settings_authorizedgroup.php diff --git a/lib/ACL/ACLStorageWrapper.php b/lib/ACL/ACLStorageWrapper.php index 97c9039ee..796203ae3 100644 --- a/lib/ACL/ACLStorageWrapper.php +++ b/lib/ACL/ACLStorageWrapper.php @@ -76,7 +76,7 @@ public function isSharable(string $path): bool { #[\Override] public function getPermissions(string $path): int { - return $this->storage->getPermissions($path) & $this->getACLPermissionsForPath($path); + return $this->getWrapperStorage()->getPermissions($path) & $this->getACLPermissionsForPath($path); } #[\Override] diff --git a/lib/DAV/ACLPlugin.php b/lib/DAV/ACLPlugin.php index da5f1f6d4..652201421 100644 --- a/lib/DAV/ACLPlugin.php +++ b/lib/DAV/ACLPlugin.php @@ -17,6 +17,7 @@ use OCA\GroupFolders\Mount\GroupMountPoint; use OCP\Constants; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\GenericFileException; use OCP\IL10N; use OCP\IUser; use OCP\IUserSession; @@ -124,6 +125,11 @@ public function propFind(PropFind $propFind, INode $node): void { return []; } + $fileInfoId = $fileInfo->getId(); + if ($fileInfoId === null) { + throw new GenericFileException('Failed to get id of fileinfo.'); + } + $parentInternalPaths = $this->getParents($fileInfo->getInternalPath()); $parentPaths = array_map(fn (string $internalPath): string => trim($mount->getSourcePath() . '/' . $internalPath, '/'), $parentInternalPaths); if ($this->isAdmin($this->user, $fileInfo->getPath())) { @@ -160,7 +166,7 @@ public function propFind(PropFind $propFind, INode $node): void { return array_map(fn (IUserMapping $mapping, int $permissions, int $mask): Rule => new Rule( $mapping, - $fileInfo->getId(), + $fileInfoId, $mask, $permissions ), $mappings, $inheritedPermissionsByMapping, $inheritedMaskByMapping); @@ -222,6 +228,12 @@ public function propPatch(string $path, PropPatch $propPatch): void { } $fileInfo = $node->getFileInfo(); + + $fileInfoId = $fileInfo->getId(); + if ($fileInfoId === null) { + throw new GenericFileException('Failed to get id of fileinfo.'); + } + $mount = $fileInfo->getMountPoint(); if (!$mount instanceof GroupMountPoint) { return false; @@ -236,7 +248,7 @@ public function propPatch(string $path, PropPatch $propPatch): void { // populate fileid in rules $rules = array_values(array_map(fn (Rule $rule): Rule => new Rule( $rule->getUserMapping(), - $fileInfo->getId(), + $fileInfoId, $rule->getMask(), $rule->getPermissions() ), $rawRules)); diff --git a/lib/Mount/GroupMountPoint.php b/lib/Mount/GroupMountPoint.php index 2588a6b7d..4f1c01b56 100644 --- a/lib/Mount/GroupMountPoint.php +++ b/lib/Mount/GroupMountPoint.php @@ -17,6 +17,9 @@ use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IStorageFactory; +/** + * @psalm-suppress PropertyNotSetInConstructor $class, $mountPoint and $mountProvider are set through the parent constructor + */ class GroupMountPoint extends MountPoint implements ISystemMountPoint, IShareOwnerlessMount { public function __construct( private readonly FolderDefinition $folder, diff --git a/lib/Mount/RootPermissionsMask.php b/lib/Mount/RootPermissionsMask.php index c767b20ef..39a397fd2 100644 --- a/lib/Mount/RootPermissionsMask.php +++ b/lib/Mount/RootPermissionsMask.php @@ -79,10 +79,10 @@ public function isSharable(string $path): bool { #[\Override] public function getPermissions(string $path): int { if ($path === '') { - return $this->storage->getPermissions($path) & $this->mask; + return $this->getWrapperStorage()->getPermissions($path) & $this->mask; } - return $this->storage->getPermissions($path); + return $this->getWrapperStorage()->getPermissions($path); } #[\Override] diff --git a/psalm.xml b/psalm.xml index 6d12b4b3e..1ca5138cc 100644 --- a/psalm.xml +++ b/psalm.xml @@ -28,6 +28,7 @@ + @@ -58,6 +59,7 @@ + diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 5c6ac16e5..f2fa7f973 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -20,11 +20,6 @@ - - - - - @@ -51,12 +46,6 @@ - - - - - - @@ -72,11 +61,6 @@ - - - - - diff --git a/tests/stubs/oc.php b/tests/stubs/oc.php index 4216334cb..d75dc744e 100644 --- a/tests/stubs/oc.php +++ b/tests/stubs/oc.php @@ -17,7 +17,9 @@ use OCP\Files\Events\BeforeFileSystemSetupEvent; use OCP\Group\Events\GroupDeletedEvent; use OCP\Group\Events\UserRemovedEvent; +use OCP\IAppConfig; use OCP\IConfig; +use OCP\IInitialStateService; use OCP\ILogger; use OCP\IRequest; use OCP\IURLGenerator; diff --git a/tests/stubs/oc_appframework_ocs_baseresponse.php b/tests/stubs/oc_appframework_ocs_baseresponse.php index c554ba7b7..02751f562 100644 --- a/tests/stubs/oc_appframework_ocs_baseresponse.php +++ b/tests/stubs/oc_appframework_ocs_baseresponse.php @@ -11,38 +11,21 @@ use OCP\AppFramework\Http\Response; /** - * @psalm-import-type DataResponseType from DataResponse - * @template S of Http::STATUS_* - * @template-covariant T of DataResponseType - * @template H of array + * @template-covariant S of Http::STATUS_* + * @template-covariant T of mixed + * @template-covariant H of array * @template-extends Response> */ abstract class BaseResponse extends Response { /** @var array */ protected $data; - /** @var string */ - protected $format; - - /** @var ?string */ - protected $statusMessage; - - /** @var ?int */ - protected $itemsCount; - - /** @var ?int */ - protected $itemsPerPage; - /** * BaseResponse constructor. * * @param DataResponse $dataResponse - * @param string $format - * @param string|null $statusMessage - * @param int|null $itemsCount - * @param int|null $itemsPerPage */ - public function __construct(DataResponse $dataResponse, $format = 'xml', $statusMessage = null, $itemsCount = null, $itemsPerPage = null) + public function __construct(DataResponse $dataResponse, protected string $format = 'xml', protected ?string $statusMessage = null, protected ?int $itemsCount = null, protected ?int $itemsPerPage = null) { } diff --git a/tests/stubs/oc_appframework_ocs_v1response.php b/tests/stubs/oc_appframework_ocs_v1response.php index b9ccfecfc..12e7dfc6d 100644 --- a/tests/stubs/oc_appframework_ocs_v1response.php +++ b/tests/stubs/oc_appframework_ocs_v1response.php @@ -1,5 +1,7 @@ - * @template-extends BaseResponse> + * @template-covariant S of Http::STATUS_* + * @template-covariant H of array + * @template-extends BaseResponse> */ class V1Response extends BaseResponse { /** diff --git a/tests/stubs/oc_appframework_utility_simplecontainer.php b/tests/stubs/oc_appframework_utility_simplecontainer.php index 5d7358a0e..da1f0bb53 100644 --- a/tests/stubs/oc_appframework_utility_simplecontainer.php +++ b/tests/stubs/oc_appframework_utility_simplecontainer.php @@ -19,6 +19,7 @@ use ReflectionException; use ReflectionNamedType; use ReflectionParameter; +use RuntimeException; use function class_exists; /** @@ -34,10 +35,7 @@ public function __construct() /** * @template T * @param class-string|string $id - * @return T|mixed - * @psalm-template S as class-string|string - * @psalm-param S $id - * @psalm-return (S is class-string ? T : mixed) + * @return ($id is class-string ? T : mixed) */ public function get(string $id): mixed { @@ -47,32 +45,27 @@ public function has(string $id): bool { } - public function resolve($name) + /** + * @inheritDoc + * @param list $chain + */ + public function resolve(string $name, array $chain = []): mixed { } - public function query(string $name, bool $autoload = true) + /** + * @inheritDoc + * @param list $chain + */ + public function query(string $name, bool $autoload = true, array $chain = []): mixed { } - /** - * @param string $name - * @param mixed $value - */ - public function registerParameter($name, $value) + public function registerParameter(string $name, mixed $value): void { } - /** - * The given closure is call the first time the given service is queried. - * The closure has to return the instance for the given service. - * Created instance will be cached in case $shared is true. - * - * @param string $name name of the service to register another backend for - * @param Closure $closure the closure to be called on service creation - * @param bool $shared - */ - public function registerService($name, Closure $closure, $shared = true) + public function registerService(string $name, Closure $closure, bool $shared = true): void { } @@ -83,7 +76,7 @@ public function registerService($name, Closure $closure, $shared = true) * @param string $alias the alias that should be registered * @param string $target the target that should be resolved instead */ - public function registerAlias($alias, $target): void + public function registerAlias(string $alias, string $target): void { } diff --git a/tests/stubs/oc_db_querybuilder_querybuilder.php b/tests/stubs/oc_db_querybuilder_querybuilder.php index 10b17d62f..507a3b5e6 100644 --- a/tests/stubs/oc_db_querybuilder_querybuilder.php +++ b/tests/stubs/oc_db_querybuilder_querybuilder.php @@ -9,7 +9,6 @@ use Doctrine\DBAL\Query\QueryException; use OC\DB\ConnectionAdapter; -use OC\DB\Exceptions\DbalException; use OC\DB\QueryBuilder\ExpressionBuilder\MySqlExpressionBuilder; use OC\DB\QueryBuilder\ExpressionBuilder\OCIExpressionBuilder; use OC\DB\QueryBuilder\ExpressionBuilder\PgSqlExpressionBuilder; @@ -20,25 +19,26 @@ use OC\DB\QueryBuilder\FunctionBuilder\SqliteFunctionBuilder; use OC\SystemConfig; use OCP\DB\IResult; +use OCP\DB\QueryBuilder\ConflictResolutionMode; use OCP\DB\QueryBuilder\ICompositeExpression; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IFunctionBuilder; use OCP\DB\QueryBuilder\ILiteral; use OCP\DB\QueryBuilder\IParameter; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryFunction; use OCP\IDBConnection; +use Override; use Psr\Log\LoggerInterface; +use RuntimeException; -class QueryBuilder implements IQueryBuilder { - /** @var string */ - protected $lastInsertedTable; +class QueryBuilder extends TypedQueryBuilder { + protected ?string $lastInsertedTable = null; /** * Initializes a new QueryBuilder. - * - * @param ConnectionAdapter $connection - * @param SystemConfig $systemConfig */ - public function __construct(ConnectionAdapter $connection, SystemConfig $systemConfig, LoggerInterface $logger) + public function __construct(private ConnectionAdapter $connection, private SystemConfig $systemConfig, private LoggerInterface $logger) { } @@ -67,7 +67,7 @@ public function automaticTablePrefix($enabled) * For more complex expression construction, consider storing the expression * builder object in a local variable. * - * @return \OCP\DB\QueryBuilder\IExpressionBuilder + * @return IExpressionBuilder */ public function expr() { @@ -87,7 +87,7 @@ public function expr() * For more complex function construction, consider storing the function * builder object in a local variable. * - * @return \OCP\DB\QueryBuilder\IFunctionBuilder + * @return IFunctionBuilder */ public function func() { @@ -105,7 +105,7 @@ public function getType() /** * Gets the associated DBAL Connection for this query builder. * - * @return \OCP\IDBConnection + * @return IDBConnection */ public function getConnection() { @@ -120,18 +120,6 @@ public function getConnection() */ public function getState() { - } - - /** - * Executes this query using the bound parameters and their types. - * - * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate} - * for insert, update and delete statements. - * - * @return IResult|int - */ - public function execute(?IDBConnection $connection = null) - { } public function executeQuery(?IDBConnection $connection = null): IResult @@ -323,7 +311,7 @@ public function select(...$selects) * * @return $this This QueryBuilder instance. */ - public function selectAlias($select, $alias) + public function selectAlias($select, $alias): self { } @@ -355,11 +343,11 @@ public function selectDistinct($select) * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); * * - * @param mixed ...$selects The selection expression. + * @param mixed ...$select The selection expression. * * @return $this This QueryBuilder instance. */ - public function addSelect(...$selects) + public function addSelect(...$select) { } @@ -682,7 +670,7 @@ public function addGroupBy(...$groupBy) * * * @param string $column The column into which the value should be inserted. - * @param IParameter|string $value The value that should be inserted into the column. + * @param IParameter|IQueryFunction|string $value The value that should be inserted into the column. * * @return $this This QueryBuilder instance. */ @@ -994,4 +982,8 @@ public function runAcrossAllShards(): self { } + #[Override] + public function forUpdate(ConflictResolutionMode $conflictResolutionMode = ConflictResolutionMode::Ordinary): self + { + } } diff --git a/tests/stubs/oc_db_querybuilder_typedquerybuilder.php b/tests/stubs/oc_db_querybuilder_typedquerybuilder.php new file mode 100644 index 000000000..42ec3a935 --- /dev/null +++ b/tests/stubs/oc_db_querybuilder_typedquerybuilder.php @@ -0,0 +1,26 @@ + + */ +abstract class TypedQueryBuilder implements ITypedQueryBuilder { + public function selectColumns(string ...$columns): static + { + } + + public function selectColumnsDistinct(string ...$columns): static + { + } +} diff --git a/tests/stubs/oc_files_cache_cache.php b/tests/stubs/oc_files_cache_cache.php index 90a240793..c616ab7ac 100644 --- a/tests/stubs/oc_files_cache_cache.php +++ b/tests/stubs/oc_files_cache_cache.php @@ -7,6 +7,7 @@ */ namespace OC\Files\Cache; +use OC\DatabaseException; use OC\DB\Exceptions\DbalException; use OC\DB\QueryBuilder\Sharded\ShardDefinition; use OC\Files\Cache\Wrapper\CacheJail; @@ -18,6 +19,7 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\CacheEntriesRemovedEvent; use OCP\Files\Cache\CacheEntryInsertedEvent; use OCP\Files\Cache\CacheEntryRemovedEvent; use OCP\Files\Cache\CacheEntryUpdatedEvent; @@ -33,7 +35,9 @@ use OCP\Files\Storage\IStorage; use OCP\FilesMetadata\IFilesMetadataManager; use OCP\IDBConnection; +use OCP\Server; use OCP\Util; +use Override; use Psr\Log\LoggerInterface; /** @@ -103,32 +107,18 @@ public function get($file) /** * Create a CacheEntry from database row - * - * @param array $data - * @param IMimeTypeLoader $mimetypeLoader - * @return CacheEntry */ - public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) + public static function cacheEntryFromData(array $data, IMimeTypeLoader $mimetypeLoader): CacheEntry { } - /** - * get the metadata of all files stored in $folder - * - * @param string $folder - * @return ICacheEntry[] - */ - public function getFolderContents($folder) + #[Override] + public function getFolderContents(string $folder, ?string $mimeTypeFilter = null) { } - /** - * get the metadata of all files stored in $folder - * - * @param int $fileId the file id of the folder - * @return ICacheEntry[] - */ - public function getFolderContentsById($fileId) + #[Override] + public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = null) { } @@ -257,7 +247,7 @@ protected function shouldEncrypt(string $targetPath): bool * @param ICache $sourceCache * @param string $sourcePath * @param string $targetPath - * @throws \OC\DatabaseException + * @throws DatabaseException * @throws \Exception if the given storages have an invalid id */ public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) diff --git a/tests/stubs/oc_files_cache_cacheentry.php b/tests/stubs/oc_files_cache_cacheentry.php index d6d4b62ca..e60c802b6 100644 --- a/tests/stubs/oc_files_cache_cacheentry.php +++ b/tests/stubs/oc_files_cache_cacheentry.php @@ -13,9 +13,10 @@ * meta data for a file or folder */ class CacheEntry implements ICacheEntry { - public function __construct(array $data) - { - } + public function __construct( + private array $data, + ) { + } public function offsetSet($offset, $value): void { @@ -81,7 +82,7 @@ public function getEtag() { } - public function getPermissions() + public function getPermissions(): int { } diff --git a/tests/stubs/oc_files_cache_scanner.php b/tests/stubs/oc_files_cache_scanner.php index 127128ba0..b0d59e4b2 100644 --- a/tests/stubs/oc_files_cache_scanner.php +++ b/tests/stubs/oc_files_cache_scanner.php @@ -8,17 +8,25 @@ namespace OC\Files\Cache; use Doctrine\DBAL\Exception; +use OC\Files\Filesystem; +use OC\Files\Storage\Storage; use OC\Files\Storage\Wrapper\Encryption; use OC\Files\Storage\Wrapper\Jail; use OC\Hooks\BasicEmitter; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\ICacheEntry; use OCP\Files\Cache\IScanner; use OCP\Files\ForbiddenException; +use OCP\Files\IMimeTypeLoader; use OCP\Files\NotFoundException; use OCP\Files\Storage\ILockingStorage; use OCP\Files\Storage\IReliableEtagStorage; +use OCP\Files\StorageInvalidException; +use OCP\Files\StorageNotAvailableException; use OCP\IConfig; use OCP\IDBConnection; use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; use OCP\Server; use Psr\Log\LoggerInterface; @@ -34,39 +42,15 @@ * @package OC\Files\Cache */ class Scanner extends BasicEmitter implements IScanner { - /** - * @var \OC\Files\Storage\Storage $storage - */ - protected $storage; - - /** - * @var string $storageId - */ - protected $storageId; - - /** - * @var \OC\Files\Cache\Cache $cache - */ - protected $cache; - - /** - * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache - */ - protected $cacheActive; - - /** - * @var bool $useTransactions whether to use transactions - */ - protected $useTransactions = true; - - /** - * @var \OCP\Lock\ILockingProvider - */ - protected $lockingProvider; - + protected string $storageId; + protected ICache $cache; + /** @var bool $cacheActive Whether to perform cache operations */ + protected bool $cacheActive; + protected bool $useTransactions = true; + protected ILockingProvider $lockingProvider; protected IDBConnection $connection; - public function __construct(\OC\Files\Storage\Storage $storage) + public function __construct(protected Storage $storage) { } @@ -101,7 +85,7 @@ protected function getData($path) * @param bool $lock set to false to disable getting an additional read lock during scanning * @param array|null $data the metadata for the file, as returned by the storage * @return array|null an array of metadata of the scanned file - * @throws \OCP\Lock\LockedException + * @throws LockedException */ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) { @@ -166,7 +150,7 @@ protected function array_diff_assoc_multi(array $array1, array $array2) * Get the children currently in the cache * * @param int $folderId - * @return array + * @return array */ protected function getExistingChildren($folderId): array { diff --git a/tests/stubs/oc_files_cache_wrapper_cachejail.php b/tests/stubs/oc_files_cache_wrapper_cachejail.php index 9fff517d1..1232b8680 100644 --- a/tests/stubs/oc_files_cache_wrapper_cachejail.php +++ b/tests/stubs/oc_files_cache_wrapper_cachejail.php @@ -229,11 +229,10 @@ public function getPathById($id) * * Note that this should make sure the entries are removed from the source cache * - * @param \OCP\Files\Cache\ICache $sourceCache * @param string $sourcePath * @param string $targetPath */ - public function moveFromCache(\OCP\Files\Cache\ICache $sourceCache, $sourcePath, $targetPath) + public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) { } diff --git a/tests/stubs/oc_files_cache_wrapper_cachewrapper.php b/tests/stubs/oc_files_cache_wrapper_cachewrapper.php index b0cf27190..a4d9c3ca9 100644 --- a/tests/stubs/oc_files_cache_wrapper_cachewrapper.php +++ b/tests/stubs/oc_files_cache_wrapper_cachewrapper.php @@ -16,12 +16,7 @@ use OCP\Server; class CacheWrapper extends Cache { - /** - * @var ?ICache - */ - protected $cache; - - public function __construct(?ICache $cache, ?CacheDependencies $dependencies = null) + public function __construct(protected ?ICache $cache, ?CacheDependencies $dependencies = null) { } @@ -31,6 +26,10 @@ public function getCache(): ICache protected function hasEncryptionWrapper(): bool { + } + + protected function shouldEncrypt(string $targetPath): bool + { } /** @@ -59,17 +58,17 @@ public function get($file) * @param string $folder * @return ICacheEntry[] */ - public function getFolderContents($folder) + public function getFolderContents(string $folder, ?string $mimeTypeFilter = null): array { } /** - * get the metadata of all files stored in $folder + * Get the metadata of all files stored in given folder * * @param int $fileId the file id of the folder - * @return array + * @return ICacheEntry[] */ - public function getFolderContentsById($fileId) + public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = null) { } diff --git a/tests/stubs/oc_files_filesystem.php b/tests/stubs/oc_files_filesystem.php index 8de245a50..1b92f5ce5 100644 --- a/tests/stubs/oc_files_filesystem.php +++ b/tests/stubs/oc_files_filesystem.php @@ -8,17 +8,21 @@ namespace OC\Files; use OC\Files\Mount\MountPoint; +use OC\Files\Storage\Storage; use OC\Files\Storage\StorageFactory; use OC\User\NoUserException; -use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Events\Node\FilesystemTornDownEvent; +use OCP\Files\InvalidPathException; use OCP\Files\Mount\IMountManager; +use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; +use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IStorageFactory; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Server; use Psr\Log\LoggerInterface; class Filesystem { @@ -202,7 +206,7 @@ public static function getMountPoints($path) * get the storage mounted at $mountPoint * * @param string $mountPoint - * @return \OC\Files\Storage\Storage|null + * @return IStorage|null */ public static function getStorage($mountPoint) { @@ -210,7 +214,7 @@ public static function getStorage($mountPoint) /** * @param string $id - * @return Mount\MountPoint[] + * @return IMountPoint[] */ public static function getMountByStorageId($id) { @@ -218,7 +222,7 @@ public static function getMountByStorageId($id) /** * @param int $id - * @return Mount\MountPoint[] + * @return IMountPoint[] */ public static function getMountByNumericId($id) { @@ -228,7 +232,7 @@ public static function getMountByNumericId($id) * resolve a path to a storage and internal path * * @param string $path - * @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path + * @return array{?IStorage, string} an array consisting of the storage and the internal path */ public static function resolvePath($path): array { @@ -249,7 +253,7 @@ public static function initMountManager(): void /** * Initialize system and personal mount points for a user * - * @throws \OC\User\NoUserException if the user is not available + * @throws NoUserException if the user is not available */ public static function initMountPoints(string|IUser|null $user = ''): void { @@ -283,7 +287,7 @@ public static function getRoot() /** * mount an \OC\Files\Storage\Storage in our virtual filesystem * - * @param \OC\Files\Storage\Storage|string $class + * @param Storage|string $class * @param array $arguments * @param string $mountpoint */ @@ -418,7 +422,7 @@ public static function fopen($path, $mode) /** * @param string $path - * @throws \OCP\Files\InvalidPathException + * @throws InvalidPathException */ public static function toTmpFile($path): string|false { @@ -491,7 +495,7 @@ public static function normalizePath($path, $stripTrailingSlash = true, $isAbsol * @param string $path * @param bool|string $includeMountPoints whether to add mountpoint sizes, * defaults to true - * @return \OC\Files\FileInfo|false False if file does not exist + * @return FileInfo|false False if file does not exist */ public static function getFileInfo($path, $includeMountPoints = true) { @@ -511,13 +515,13 @@ public static function putFileInfo($path, $data) } /** - * get the content of a directory + * Get the content of a directory. * * @param string $directory path under datadirectory - * @param string $mimetype_filter limit returned content to this mimetype or mimepart - * @return \OC\Files\FileInfo[] + * @param ?non-empty-string $mimeTypeFilter limit returned content to this mimetype or mimepart + * @return FileInfo[] */ - public static function getDirectoryContent($directory, $mimetype_filter = '') + public static function getDirectoryContent($directory, ?string $mimeTypeFilter = null): array { } diff --git a/tests/stubs/oc_files_mount_mountpoint.php b/tests/stubs/oc_files_mount_mountpoint.php index 02dc4ee50..af9ab2cd7 100644 --- a/tests/stubs/oc_files_mount_mountpoint.php +++ b/tests/stubs/oc_files_mount_mountpoint.php @@ -11,51 +11,41 @@ use OC\Files\Storage\Storage; use OC\Files\Storage\StorageFactory; use OCP\Files\Mount\IMountPoint; +use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IStorageFactory; +use OCP\Server; use Psr\Log\LoggerInterface; class MountPoint implements IMountPoint { - /** - * @var \OC\Files\Storage\Storage|null $storage - */ + /** @var IStorage|null $storage */ protected $storage = null; - protected $class; - protected $storageId; - protected $numericStorageId = null; - protected $rootId = null; + /** @var class-string */ + protected string $class; + protected ?string $storageId = null; + protected ?int $numericStorageId = null; + protected ?int $rootId = null; /** * Configuration options for the storage backend - * - * @var array */ - protected $arguments = []; - protected $mountPoint; + protected array $arguments = []; + protected string $mountPoint; /** * Mount specific options - * - * @var array */ - protected $mountOptions = []; - - /** @var int|null */ - protected $mountId; - - /** @var string */ - protected $mountProvider; + protected array $mountOptions = []; + protected string $mountProvider; /** - * @param string|\OC\Files\Storage\Storage $storage - * @param string $mountpoint + * @param IStorage|class-string $storage * @param array $arguments (optional) configuration for the storage backend - * @param \OCP\Files\Storage\IStorageFactory $loader - * @param array $mountOptions mount specific options - * @param int|null $mountId - * @param string|null $mountProvider + * @param ?array $mountOptions mount specific options + * @param ?int $mountId + * @param ?string $mountProvider * @throws \Exception */ - public function __construct($storage, string $mountpoint, ?array $arguments = null, ?IStorageFactory $loader = null, ?array $mountOptions = null, ?int $mountId = null, ?string $mountProvider = null) + public function __construct(string|IStorage $storage, string $mountpoint, ?array $arguments = null, ?IStorageFactory $loader = null, ?array $mountOptions = null, protected ?int $mountId = null, ?string $mountProvider = null) { } @@ -78,7 +68,7 @@ public function setMountPoint($mountPoint) } /** - * @return \OC\Files\Storage\Storage|null + * @return IStorage|null */ public function getStorage() { diff --git a/tests/stubs/oc_files_node_lazyfolder.php b/tests/stubs/oc_files_node_lazyfolder.php index 4b7481b94..af3c29245 100644 --- a/tests/stubs/oc_files_node_lazyfolder.php +++ b/tests/stubs/oc_files_node_lazyfolder.php @@ -14,6 +14,7 @@ use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; use OCP\Files\NotPermittedException; +use Override; /** * Class LazyFolder @@ -129,6 +130,11 @@ public function unMount($mount) public function get($path) { + } + + #[Override] + public function getOrCreateFolder(string $path, int $maxRetries = 5): Folder + { } /** @@ -349,10 +355,8 @@ public function isSubNode($node) { } - /** - * @inheritDoc - */ - public function getDirectoryListing() + #[Override] + public function getDirectoryListing(?string $mimetypeFilter = null): array { } @@ -427,7 +431,7 @@ public function isCreatable() /** * @inheritDoc */ - public function getNonExistingName($name) + public function getNonExistingName($filename) { } diff --git a/tests/stubs/oc_files_objectstore_objectstorestorage.php b/tests/stubs/oc_files_objectstore_objectstorestorage.php index 5dc76614d..913bd965a 100644 --- a/tests/stubs/oc_files_objectstore_objectstorestorage.php +++ b/tests/stubs/oc_files_objectstore_objectstorestorage.php @@ -14,13 +14,16 @@ use Icewind\Streams\IteratorDirectory; use OC\Files\Cache\Cache; use OC\Files\Cache\CacheEntry; +use OC\Files\Storage\Common; use OC\Files\Storage\PolyFill\CopyDirectory; +use OCP\Constants; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Cache\IScanner; use OCP\Files\FileInfo; use OCP\Files\GenericFileException; +use OCP\Files\IMimeTypeDetector; use OCP\Files\NotFoundException; use OCP\Files\ObjectStore\IObjectStore; use OCP\Files\ObjectStore\IObjectStoreMetaData; @@ -28,10 +31,12 @@ use OCP\Files\Storage\IChunkedFileWrite; use OCP\Files\Storage\IStorage; use OCP\IDBConnection; +use OCP\ITempManager; use OCP\Server; +use Override; use Psr\Log\LoggerInterface; -class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite { +class ObjectStoreStorage extends Common implements IChunkedFileWrite { use CopyDirectory; protected IObjectStore $objectStore; @@ -183,13 +188,14 @@ public function setPreserveCacheOnDelete(bool $preserve) public function free_space(string $path): int|float|false { } - - public function getDirectDownloadById(string $fileId): array|false + + #[Override] + public function getDirectDownloadById(string $fileId): array|false { } - public function getDirectDownload(string $path): array|false + #[Override] + public function getDirectDownload(string $path): array|false { } - } diff --git a/tests/stubs/oc_files_setupmanager.php b/tests/stubs/oc_files_setupmanager.php index 6bb940563..22b044181 100644 --- a/tests/stubs/oc_files_setupmanager.php +++ b/tests/stubs/oc_files_setupmanager.php @@ -1,6 +1,7 @@ [] $providers + */ + public function dropPartialMountsForUser(IUser $user, array $providers = []): void { } } diff --git a/tests/stubs/oc_files_storage_common.php b/tests/stubs/oc_files_storage_common.php index a03ad0e10..fe099a94b 100644 --- a/tests/stubs/oc_files_storage_common.php +++ b/tests/stubs/oc_files_storage_common.php @@ -19,15 +19,18 @@ use OC\Files\Storage\Wrapper\Encryption; use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\Wrapper; +use OCP\Constants; use OCP\Files; use OCP\Files\Cache\ICache; use OCP\Files\Cache\IPropagator; use OCP\Files\Cache\IScanner; use OCP\Files\Cache\IUpdater; use OCP\Files\Cache\IWatcher; +use OCP\Files\FileInfo; use OCP\Files\ForbiddenException; use OCP\Files\GenericFileException; use OCP\Files\IFilenameValidator; +use OCP\Files\IMimeTypeDetector; use OCP\Files\InvalidPathException; use OCP\Files\Storage\IConstructableStorage; use OCP\Files\Storage\ILockingStorage; @@ -35,9 +38,12 @@ use OCP\Files\Storage\IWriteStreamStorage; use OCP\Files\StorageNotAvailableException; use OCP\IConfig; +use OCP\IDBConnection; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use OCP\Server; +use OCP\Util; +use Override; use Psr\Log\LoggerInterface; /** @@ -225,16 +231,13 @@ public function instanceOfStorage(string $class): bool { } - /** - * A custom storage implementation can return an url for direct download of a give file. - * - * For now the returned array can hold the parameter url - in future more attributes might follow. - */ - public function getDirectDownload(string $path): array|false + #[Override] + public function getDirectDownload(string $path): array|false { } - public function getDirectDownloadById(string $fileId): array|false + #[Override] + public function getDirectDownloadById(string $fileId): array|false { } @@ -283,7 +286,7 @@ public function changeLock(string $path, int $type, ILockingProvider $provider): } /** - * @return array [ available, last_checked ] + * @return array{available: bool, last_checked: int} */ public function getAvailability(): array { diff --git a/tests/stubs/oc_files_storage_local.php b/tests/stubs/oc_files_storage_local.php index 4fe5790a0..5df4fc58f 100644 --- a/tests/stubs/oc_files_storage_local.php +++ b/tests/stubs/oc_files_storage_local.php @@ -10,7 +10,10 @@ use OC\Files\Filesystem; use OC\Files\Storage\Wrapper\Encryption; use OC\Files\Storage\Wrapper\Jail; +use OC\LargeFileHelper; +use OCA\FilesAccessControl\StorageWrapper; use OCP\Constants; +use OCP\Files\FileInfo; use OCP\Files\ForbiddenException; use OCP\Files\GenericFileException; use OCP\Files\IMimeTypeDetector; @@ -24,7 +27,7 @@ /** * for local filestore, we only have to map the paths */ -class Local extends \OC\Files\Storage\Common { +class Local extends Common { protected $datadir; protected $dataDirLength; diff --git a/tests/stubs/oc_files_storage_storage.php b/tests/stubs/oc_files_storage_storage.php index 8afb2fb77..f74af48f5 100644 --- a/tests/stubs/oc_files_storage_storage.php +++ b/tests/stubs/oc_files_storage_storage.php @@ -1,11 +1,12 @@ + */ public function getMetaData(string $path): ?array { } @@ -62,6 +66,8 @@ public function getMetaData(string $path): ?array * - etag * - storage_mtime * - permissions + * + * @return \Traversable> */ public function getDirectoryContent(string $directory): \Traversable { diff --git a/tests/stubs/oc_files_storage_wrapper_jail.php b/tests/stubs/oc_files_storage_wrapper_jail.php index 622224ace..968bb16ab 100644 --- a/tests/stubs/oc_files_storage_wrapper_jail.php +++ b/tests/stubs/oc_files_storage_wrapper_jail.php @@ -11,13 +11,17 @@ use OC\Files\Cache\Wrapper\JailPropagator; use OC\Files\Cache\Wrapper\JailWatcher; use OC\Files\Filesystem; -use OCP\Files; +use OC\Files\Storage\FailedStorage; use OCP\Files\Cache\ICache; use OCP\Files\Cache\IPropagator; use OCP\Files\Cache\IWatcher; +use OCP\Files\GenericFileException; use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IWriteStreamStorage; +use OCP\IDBConnection; use OCP\Lock\ILockingProvider; +use OCP\Server; +use Psr\Log\LoggerInterface; /** * Jail to a subdirectory of the wrapped storage @@ -205,7 +209,9 @@ public function changeLock(string $path, int $type, ILockingProvider $provider): } /** - * Resolve the path for the source of the share + * Resolve the path for the source of the share. + * + * @return array{0: IStorage, 1: string} */ public function resolvePath(string $path): array { diff --git a/tests/stubs/oc_files_storage_wrapper_permissionsmask.php b/tests/stubs/oc_files_storage_wrapper_permissionsmask.php index 73d73e812..705b2bbee 100644 --- a/tests/stubs/oc_files_storage_wrapper_permissionsmask.php +++ b/tests/stubs/oc_files_storage_wrapper_permissionsmask.php @@ -9,6 +9,8 @@ use OC\Files\Cache\Wrapper\CachePermissionsMask; use OCP\Constants; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IScanner; use OCP\Files\Storage\IStorage; /** @@ -81,7 +83,7 @@ public function fopen(string $path, string $mode) { } - public function getCache(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\ICache + public function getCache(string $path = '', ?IStorage $storage = null): ICache { } @@ -89,7 +91,7 @@ public function getMetaData(string $path): ?array { } - public function getScanner(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\IScanner + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { } diff --git a/tests/stubs/oc_files_storage_wrapper_wrapper.php b/tests/stubs/oc_files_storage_wrapper_wrapper.php index 4aea048bc..e66b22791 100644 --- a/tests/stubs/oc_files_storage_wrapper_wrapper.php +++ b/tests/stubs/oc_files_storage_wrapper_wrapper.php @@ -1,5 +1,7 @@ */ - public function getUserGroups(?IUser $user = null) + public function getUserGroups(?IUser $user = null): array { } /** * @param string $uid the user id - * @return \OC\Group\Group[] + * @return array */ public function getUserIdGroups(string $uid): array { @@ -192,21 +196,12 @@ public function getUserGroupNames(IUser $user) { } - /** - * get a list of all display names in a group - * - * @param string $gid - * @param string $search - * @param int $limit - * @param int $offset - * @return array an array of display names (value) and user ids (key) - */ public function displayNamesInGroup($gid, $search = '', $limit = -1, $offset = 0) { } /** - * @return \OC\SubAdmin + * @return SubAdmin */ public function getSubAdmin() { diff --git a/tests/stubs/oc_hooks_basicemitter.php b/tests/stubs/oc_hooks_basicemitter.php index 091334b71..775a7d47a 100644 --- a/tests/stubs/oc_hooks_basicemitter.php +++ b/tests/stubs/oc_hooks_basicemitter.php @@ -1,5 +1,7 @@ get($app) instead */ public function getAppDataDir($app) @@ -635,7 +659,7 @@ public function getAppDataDir($app) } /** - * @return \OCP\Federation\ICloudIdManager + * @return ICloudIdManager * @deprecated 20.0.0 */ public function getCloudIdManager() diff --git a/tests/stubs/oc_servercontainer.php b/tests/stubs/oc_servercontainer.php index 2c7c3cafb..befcf7f60 100644 --- a/tests/stubs/oc_servercontainer.php +++ b/tests/stubs/oc_servercontainer.php @@ -79,6 +79,7 @@ public function has($id, bool $noRecursion = false): bool /** * @template T * @param class-string|string $name + * @param list $chain * @return T|mixed * @psalm-template S as class-string|string * @psalm-param S $name @@ -86,7 +87,7 @@ public function has($id, bool $noRecursion = false): bool * @throws QueryException * @deprecated 20.0.0 use \Psr\Container\ContainerInterface::get */ - public function query(string $name, bool $autoload = true) + public function query(string $name, bool $autoload = true, array $chain = []): mixed { } @@ -97,5 +98,9 @@ public function query(string $name, bool $autoload = true) */ public function getAppContainerForService(string $id): ?DIContainer { + } + + public function getWebRoot() + { } } diff --git a/tests/stubs/oc_settings_authorizedgroup.php b/tests/stubs/oc_settings_authorizedgroup.php new file mode 100644 index 000000000..6f7bcd692 --- /dev/null +++ b/tests/stubs/oc_settings_authorizedgroup.php @@ -0,0 +1,33 @@ + + */ + public function jsonSerialize(): array + { + } +} diff --git a/tests/stubs/oc_settings_authorizedgroupmapper.php b/tests/stubs/oc_settings_authorizedgroupmapper.php index 411448cc5..231506506 100644 --- a/tests/stubs/oc_settings_authorizedgroupmapper.php +++ b/tests/stubs/oc_settings_authorizedgroupmapper.php @@ -1,12 +1,15 @@ @@ -31,9 +35,9 @@ public function findAllClassesForUser(IUser $user): array } /** - * @throws \OCP\AppFramework\Db\DoesNotExistException - * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException - * @throws \OCP\DB\Exception + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception */ public function find(int $id): AuthorizedGroup { @@ -43,19 +47,24 @@ public function find(int $id): AuthorizedGroup * Get all the authorizations stored in the database. * * @return AuthorizedGroup[] - * @throws \OCP\DB\Exception + * @throws Exception */ public function findAll(): array { } - public function findByGroupIdAndClass(string $groupId, string $class) + /** + * @throws DoesNotExistException + * @throws Exception + * @throws MultipleObjectsReturnedException + */ + public function findByGroupIdAndClass(string $groupId, string $class): AuthorizedGroup { } /** - * @return Entity[] - * @throws \OCP\DB\Exception + * @return list + * @throws Exception */ public function findExistingGroupsForClass(string $class): array { @@ -64,7 +73,7 @@ public function findExistingGroupsForClass(string $class): array /** * @throws Exception */ - public function removeGroup(string $gid) + public function removeGroup(string $gid): void { } } diff --git a/tests/stubs/oc_user_user.php b/tests/stubs/oc_user_user.php index f20db3a93..66ca42458 100644 --- a/tests/stubs/oc_user_user.php +++ b/tests/stubs/oc_user_user.php @@ -14,6 +14,7 @@ use OCP\Accounts\IAccountManager; use OCP\Comments\ICommentsManager; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\FileInfo; use OCP\Group\Events\BeforeUserRemovedEvent; use OCP\Group\Events\UserRemovedEvent; use OCP\IAvatarManager; @@ -25,8 +26,10 @@ use OCP\IUser; use OCP\IUserBackend; use OCP\Notification\IManager as INotificationManager; +use OCP\Server; use OCP\User\Backend\IGetHomeBackend; use OCP\User\Backend\IPasswordHashBackend; +use OCP\User\Backend\IPropertyPermissionBackend; use OCP\User\Backend\IProvideAvatarBackend; use OCP\User\Backend\IProvideEnabledStateBackend; use OCP\User\Backend\ISetDisplayNameBackend; @@ -38,6 +41,7 @@ use OCP\User\Events\UserDeletedEvent; use OCP\User\GetQuotaEvent; use OCP\UserInterface; +use OCP\Util; use Psr\Log\LoggerInterface; use function json_decode; @@ -45,49 +49,39 @@ class User implements IUser { private const CONFIG_KEY_MANAGERS = 'manager'; + protected ?IAccountManager $accountManager = null; - /** @var IAccountManager */ - protected $accountManager; - - public function __construct(private string $uid, private ?UserInterface $backend, private IEventDispatcher $dispatcher, $emitter = null, ?IConfig $config = null, $urlGenerator = null) + public function __construct(private string $uid, private ?UserInterface $backend, private IEventDispatcher $dispatcher, private Emitter|Manager|null $emitter = null, ?IConfig $config = null, $urlGenerator = null) { } - /** - * get the user id - * - * @return string - */ - public function getUID() + public function getUID(): string { } /** - * get the display name for the user, if no specific display name is set it will fallback to the user id - * - * @return string + * Get the display name for the user, if no specific display name is set it will fallback to the user id */ - public function getDisplayName() + public function getDisplayName(): string { } /** - * set the displayname for the user + * Set the displayname for the user * * @param string $displayName - * @return bool * * @since 25.0.0 Throw InvalidArgumentException * @throws \InvalidArgumentException */ - public function setDisplayName($displayName) + public function setDisplayName($displayName): bool { } /** * @inheritDoc */ - public function setEMailAddress($mailAddress) + public function setEMailAddress($mailAddress): void { } @@ -130,10 +124,8 @@ public function updateLastLoginTimestamp(): bool /** * Delete the user - * - * @return bool */ - public function delete() + public function delete(): bool { } @@ -142,9 +134,8 @@ public function delete() * * @param string $password * @param string $recoveryPassword for the encryption app to reset encryption keys - * @return bool */ - public function setPassword($password, $recoveryPassword = null) + public function setPassword($password, $recoveryPassword = null): bool { } @@ -157,20 +148,16 @@ public function setPasswordHash(string $passwordHash): bool } /** - * get the users home folder to mount - * - * @return string + * Get the users home folder to mount */ - public function getHome() + public function getHome(): string { } /** * Get the name of the backend class the user is connected with - * - * @return string */ - public function getBackendClassName() + public function getBackendClassName(): string { } @@ -178,30 +165,15 @@ public function getBackend(): ?UserInterface { } - /** - * Check if the backend allows the user to change their avatar on Personal page - * - * @return bool - */ - public function canChangeAvatar() + public function canChangeAvatar(): bool { } - /** - * check if the backend supports changing passwords - * - * @return bool - */ - public function canChangePassword() + public function canChangePassword(): bool { } - /** - * check if the backend supports changing display names - * - * @return bool - */ - public function canChangeDisplayName() + public function canChangeDisplayName(): bool { } @@ -210,11 +182,16 @@ public function canChangeEmail(): bool } /** - * check if the user is enabled - * - * @return bool + * @param IAccountManager::PROPERTY_*|IAccountManager::COLLECTION_* $property */ - public function isEnabled() + public function canEditProperty(string $property): bool + { + } + + /** + * Check if the user is enabled + */ + public function isEnabled(): bool { } @@ -228,12 +205,11 @@ public function setEnabled(bool $enabled = true) } /** - * get the users email address + * Get the users email address * - * @return string|null * @since 9.0.0 */ - public function getEMailAddress() + public function getEMailAddress(): ?string { } @@ -254,10 +230,9 @@ public function getPrimaryEMailAddress(): ?string /** * get the users' quota * - * @return string * @since 9.0.0 */ - public function getQuota() + public function getQuota(): string { } @@ -266,14 +241,13 @@ public function getQuotaBytes(): int|float } /** - * set the users' quota + * Set the users' quota * * @param string $quota - * @return void * @throws InvalidArgumentException * @since 9.0.0 */ - public function setQuota($quota) + public function setQuota($quota): void { } @@ -289,24 +263,22 @@ public function setManagerUids(array $uids): void * get the avatar image if it exists * * @param int $size - * @return IImage|null * @since 9.0.0 */ - public function getAvatarImage($size) + public function getAvatarImage($size): ?IImage { } /** * get the federation cloud id * - * @return string * @since 9.0.0 */ - public function getCloudId() + public function getCloudId(): string { } - public function triggerChange($feature, $value = null, $oldValue = null) + public function triggerChange($feature, $value = null, $oldValue = null): void { } } diff --git a/tests/stubs/oca_circles_circlesmanager.php b/tests/stubs/oca_circles_circlesmanager.php index 333f0052e..e772a37b2 100644 --- a/tests/stubs/oca_circles_circlesmanager.php +++ b/tests/stubs/oca_circles_circlesmanager.php @@ -444,7 +444,7 @@ public function getDefinition(IEntity $circle): string * @param CircleProbe|null $circleProbe * @param DataProbe|null $dataProbe * - * @return array + * @return list * @throws InitiatorNotFoundException * @throws RequestBuilderException */ diff --git a/tests/stubs/oca_circles_db_corequerybuilder.php b/tests/stubs/oca_circles_db_corequerybuilder.php index d180e4e12..2e7e6b8de 100644 --- a/tests/stubs/oca_circles_db_corequerybuilder.php +++ b/tests/stubs/oca_circles_db_corequerybuilder.php @@ -332,6 +332,14 @@ public function limitToShareWith(string $shareWith): void */ public function limitToFileSource(int $nodeId): void { + } + + public function limitToFileTarget(string $target, string $alias): void + { + } + + public function limitToFileTargetLike(string $target, string $alias): void + { } /** @@ -627,7 +635,7 @@ public function leftJoinInitiator(string $alias, IFederatedUser $initiator, stri } - public function completeProbeWithInitiator(string $alias, string $field = 'single_id', string $helperAlias = ''): void + public function completeProbeWithInitiator(string $alias, string $field = 'single_id', string $helperAlias = ''): ?string { } @@ -667,9 +675,11 @@ public function leftJoinFileCache(string $aliasShare) * @param string $aliasShare * @param string $aliasShareMemberships * + * @return string the alias generated for the join + * * @throws RequestBuilderException */ - public function leftJoinShareChild(string $aliasShare, string $aliasShareMemberships = '') + public function leftJoinShareChild(string $aliasShare, string $aliasShareMemberships = ''): string { } diff --git a/tests/stubs/oca_circles_tools_db_extendedquerybuilder.php b/tests/stubs/oca_circles_tools_db_extendedquerybuilder.php index 630b79cab..3bda9a85b 100644 --- a/tests/stubs/oca_circles_tools_db_extendedquerybuilder.php +++ b/tests/stubs/oca_circles_tools_db_extendedquerybuilder.php @@ -638,7 +638,7 @@ public function asItem(string $object, array $params = []): IQueryRow * @param string $object * @param array $params * - * @return IQueryRow[] + * @return list */ public function asItems(string $object, array $params = []): array { @@ -686,7 +686,7 @@ public function getRow(callable $method, string $object = '', array $params = [] * @param string $object * @param array $params * - * @return IQueryRow[] + * @return list */ public function getRows(callable $method, string $object = '', array $params = []): array { diff --git a/tests/stubs/oca_dav_connector_sabre_directory.php b/tests/stubs/oca_dav_connector_sabre_directory.php index 63c8e89ae..c1cb45b81 100644 --- a/tests/stubs/oca_dav_connector_sabre_directory.php +++ b/tests/stubs/oca_dav_connector_sabre_directory.php @@ -8,6 +8,7 @@ namespace OCA\DAV\Connector\Sabre; use OC\Files\Mount\MoveableMount; +use OC\Files\Utils\PathHelper; use OC\Files\View; use OCA\DAV\AppInfo\Application; use OCA\DAV\Connector\Sabre\Exception\FileLocked; @@ -38,8 +39,14 @@ use Sabre\DAV\Exception\ServiceUnavailable; use Sabre\DAV\IFile; use Sabre\DAV\INode; +use Sabre\DAV\INodeByPath; -class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget { +class Directory extends Node implements + \Sabre\DAV\ICollection, + \Sabre\DAV\IQuota, + \Sabre\DAV\IMoveTarget, + \Sabre\DAV\ICopyTarget, + INodeByPath { /** * Sets up the node, expects a full path name */ @@ -187,5 +194,9 @@ public function copyInto($targetName, $sourcePath, INode $sourceNode) public function getNode(): Folder { + } + + public function getNodeForPath($path): INode + { } } diff --git a/tests/stubs/oca_dav_connector_sabre_node.php b/tests/stubs/oca_dav_connector_sabre_node.php index a571ca9ed..933d8edb4 100644 --- a/tests/stubs/oca_dav_connector_sabre_node.php +++ b/tests/stubs/oca_dav_connector_sabre_node.php @@ -1,5 +1,7 @@ * @since 29.0.0 */ public function getMetadata(): array diff --git a/tests/stubs/oca_settings_service_authorizedgroupservice.php b/tests/stubs/oca_settings_service_authorizedgroupservice.php index ddca80bbb..153f659dd 100644 --- a/tests/stubs/oca_settings_service_authorizedgroupservice.php +++ b/tests/stubs/oca_settings_service_authorizedgroupservice.php @@ -1,5 +1,7 @@ + */ public function findExistingGroupsForClass(string $class): array { } + /** + * @throws Throwable + * @throws NotFoundException + */ public function removeAuthorizationAssociatedTo(IGroup $group): void { } diff --git a/tests/stubs/test_testcase.php b/tests/stubs/test_testcase.php index cfb88fb6a..754bb8839 100644 --- a/tests/stubs/test_testcase.php +++ b/tests/stubs/test_testcase.php @@ -8,11 +8,12 @@ namespace Test; -use DOMDocument; -use DOMNode; +use OC\App\AppStore\Fetcher\AppFetcher; use OC\Command\QueueBus; +use OC\Files\AppData\Factory; use OC\Files\Cache\Storage; use OC\Files\Config\MountProviderCollection; +use OC\Files\Config\UserMountCache; use OC\Files\Filesystem; use OC\Files\Mount\CacheMountProvider; use OC\Files\Mount\LocalHomeMountProvider; @@ -20,14 +21,13 @@ use OC\Files\ObjectStore\PrimaryObjectStoreConfig; use OC\Files\SetupManager; use OC\Files\View; -use OC\Template\Base; -use OCP\AppFramework\QueryException; +use OC\Installer; +use OC\Updater; use OCP\Command\IBus; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\Defaults; +use OCP\Files\IRootFolder; use OCP\IConfig; use OCP\IDBConnection; -use OCP\IL10N; use OCP\IUserManager; use OCP\IUserSession; use OCP\Lock\ILockingProvider; @@ -35,70 +35,29 @@ use OCP\Security\ISecureRandom; use OCP\Server; use PHPUnit\Framework\Attributes\Group; - -if (version_compare(\PHPUnit\Runner\Version::id(), 10, '>=')) { - trait OnNotSuccessfulTestTrait { - protected function onNotSuccessfulTest(\Throwable $t): never { - $this->restoreAllServices(); - - // restore database connection - if (!$this->IsDatabaseAccessAllowed()) { - \OC::$server->registerService(IDBConnection::class, function () { - return self::$realDatabase; - }); - } - - parent::onNotSuccessfulTest($t); - } - } -} else { - trait OnNotSuccessfulTestTrait { - protected function onNotSuccessfulTest(\Throwable $t): void { - $this->restoreAllServices(); - - // restore database connection - if (!$this->IsDatabaseAccessAllowed()) { - \OC::$server->registerService(IDBConnection::class, function () { - return self::$realDatabase; - }); - } - - parent::onNotSuccessfulTest($t); - } - } -} +use Psr\Container\ContainerExceptionInterface; abstract class TestCase extends \PHPUnit\Framework\TestCase { - /** @var IDBConnection */ - protected static $realDatabase = null; - - /** @var array */ - protected $services = []; + protected static ?IDBConnection $realDatabase = null; + protected array $services = []; - use OnNotSuccessfulTestTrait; + protected function onNotSuccessfulTest(\Throwable $t): never + { + } - /** - * @param string $name - * @param mixed $newService - * @return bool - */ - public function overwriteService(string $name, $newService): bool + public function overwriteService(string $name, mixed $newService): bool { } - /** - * @param string $name - * @return bool - */ public function restoreService(string $name): bool { } - public function restoreAllServices() + public function restoreAllServices(): void { } - protected function getTestTraits() + protected function getTestTraits(): array { } @@ -154,28 +113,22 @@ public static function tearDownAfterClass(): void /** * Remove all entries from the share table - * - * @param IQueryBuilder $queryBuilder */ - protected static function tearDownAfterClassCleanShares(IQueryBuilder $queryBuilder) + protected static function tearDownAfterClassCleanShares(IQueryBuilder $queryBuilder): void { } /** * Remove all entries from the storages table - * - * @param IQueryBuilder $queryBuilder */ - protected static function tearDownAfterClassCleanStorages(IQueryBuilder $queryBuilder) + protected static function tearDownAfterClassCleanStorages(IQueryBuilder $queryBuilder): void { } /** * Remove all entries from the filecache table - * - * @param IQueryBuilder $queryBuilder */ - protected static function tearDownAfterClassCleanFileCache(IQueryBuilder $queryBuilder) + protected static function tearDownAfterClassCleanFileCache(IQueryBuilder $queryBuilder): void { } @@ -184,7 +137,7 @@ protected static function tearDownAfterClassCleanFileCache(IQueryBuilder $queryB * * @param string $dataDir */ - protected static function tearDownAfterClassCleanStrayDataFiles($dataDir) + protected static function tearDownAfterClassCleanStrayDataFiles(string $dataDir): void { } @@ -193,21 +146,21 @@ protected static function tearDownAfterClassCleanStrayDataFiles($dataDir) * * @param string $dir */ - protected static function tearDownAfterClassCleanStrayDataUnlinkDir($dir) + protected static function tearDownAfterClassCleanStrayDataUnlinkDir(string $dir): void { } /** * Clean up the list of hooks */ - protected static function tearDownAfterClassCleanStrayHooks() + protected static function tearDownAfterClassCleanStrayHooks(): void { } /** * Clean up the list of locks */ - protected static function tearDownAfterClassCleanStrayLocks() + protected static function tearDownAfterClassCleanStrayLocks(): void { } @@ -217,21 +170,21 @@ protected static function tearDownAfterClassCleanStrayLocks() * * @param string $user user id or empty for a generic FS */ - protected static function loginAsUser($user = '') + protected static function loginAsUser(string $user = ''): void { } /** * Logout the current user and tear down the filesystem. */ - protected static function logout() + protected static function logout(): void { } /** * Run all commands pushed to the bus */ - protected function runCommands() + protected function runCommands(): void { } @@ -247,33 +200,18 @@ protected function runCommands() * @return boolean true if the file is locked with the * given type, false otherwise */ - protected function isFileLocked($view, $path, $type, $onMountPoint = false) - { - } - - protected function getGroupAnnotations(): array - { - } - - protected function IsDatabaseAccessAllowed(): bool + protected function isFileLocked(View $view, string $path, int $type, bool $onMountPoint = false) { } /** - * @param string $expectedHtml - * @param string $template - * @param array $vars + * @return list */ - protected function assertTemplate($expectedHtml, $template, $vars = []) + protected function getGroupAnnotations(): array { } - /** - * @param string $expectedHtml - * @param string $actualHtml - * @param string $message - */ - protected function assertHtmlStringEqualsHtmlString($expectedHtml, $actualHtml, $message = '') + protected function IsDatabaseAccessAllowed(): bool { } } From e976a6a3699dc173fb2fa905a2bcc403cdfe4560 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 5 Feb 2026 12:05:36 +0100 Subject: [PATCH 2/3] chore(psalm): Remove Signed-off-by: provokateurin --- .github/dependabot.yml | 1 - .github/workflows/psalm.yml | 59 - composer.json | 3 - lib/ACL/ACLCacheWrapper.php | 1 - lib/Controller/DelegationController.php | 1 - lib/Controller/FolderController.php | 3 - lib/DAV/PropFindPlugin.php | 1 - lib/Folder/FolderManager.php | 1 - lib/Mount/GroupFolderNoEncryptionStorage.php | 5 - lib/Mount/GroupMountPoint.php | 3 - lib/Versions/ExpireManager.php | 1 - lib/Versions/VersionsBackend.php | 4 - psalm.xml | 140 - tests/ACL/ACLScannerTest.php | 3 - tests/ACL/ACLStorageWrapperTest.php | 1 - .../CircleDestroyedEventListenerTest.php | 1 - tests/Listeners/NodeRenamedListenerTest.php | 1 - tests/psalm-baseline.xml | 69 - tests/psalm-baseline.xml.license | 2 - vendor-bin/psalm/composer.json | 11 - vendor-bin/psalm/composer.lock | 3406 ----------------- 21 files changed, 3717 deletions(-) delete mode 100644 .github/workflows/psalm.yml delete mode 100644 psalm.xml delete mode 100644 tests/psalm-baseline.xml delete mode 100644 tests/psalm-baseline.xml.license delete mode 100644 vendor-bin/psalm/composer.json delete mode 100644 vendor-bin/psalm/composer.lock diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a3c5ddb51..e548184cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,7 +16,6 @@ updates: - /vendor-bin/cs-fixer - /vendor-bin/openapi-extractor - /vendor-bin/phpunit - - /vendor-bin/psalm - /vendor-bin/rector schedule: interval: weekly diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml deleted file mode 100644 index 9a0d2efef..000000000 --- a/.github/workflows/psalm.yml +++ /dev/null @@ -1,59 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Static analysis - -on: pull_request - -concurrency: - group: psalm-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - static-analysis: - runs-on: ubuntu-latest - - name: static-psalm-analysis - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - name: Get php version - id: versions - uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 - - - name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min }} in psalm.xml - run: grep 'phpVersion="${{ steps.versions.outputs.php-min }}' psalm.xml - - - name: Set up php${{ steps.versions.outputs.php-available }} - uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1 - with: - php-version: ${{ steps.versions.outputs.php-available }} - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - # Temporary workaround for missing pcntl_* in PHP 8.3 - ini-values: disable_functions= - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install dependencies - run: | - composer remove nextcloud/ocp --dev --no-scripts - composer i - - - name: Install nextcloud/ocp - run: composer require --dev nextcloud/ocp:dev-${{ steps.versions.outputs.branches-max }} --ignore-platform-reqs --with-dependencies - - - name: Run coding standards check - run: composer run psalm -- --threads=1 --monochrome --no-progress --output-format=github diff --git a/composer.json b/composer.json index 2f232a488..d20dafc47 100644 --- a/composer.json +++ b/composer.json @@ -15,9 +15,6 @@ "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './vendor-bin/*' -not -path './tests/stubs/*' -print0 | xargs -0 -n1 php -l", "cs:check": "php-cs-fixer fix --dry-run --diff", "cs:fix": "php-cs-fixer fix", - "psalm": "psalm --threads=$(nproc) --no-cache", - "psalm:update-baseline": "psalm --threads=$(nproc) --no-cache --update-baseline", - "psalm:fix": "psalm --no-cache --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType,MissingOverrideAttribute", "test:unit": "phpunit -c tests/phpunit.xml", "test:unit:coverage": "XDEBUG_MODE=coverage phpunit -c tests/phpunit.xml", "rector": "rector && composer cs:fix", diff --git a/lib/ACL/ACLCacheWrapper.php b/lib/ACL/ACLCacheWrapper.php index ce341078f..e474a8eb4 100644 --- a/lib/ACL/ACLCacheWrapper.php +++ b/lib/ACL/ACLCacheWrapper.php @@ -58,7 +58,6 @@ protected function formatCacheEntry($entry, array $rules = []): ICacheEntry|fals #[\Override] public function getFolderContentsById($fileId, ?string $mimeTypeFilter = null): array { - /** @psalm-suppress TooManyArguments Remove this in a few days */ $results = $this->getCache()->getFolderContentsById($fileId, $mimeTypeFilter); $rules = $this->preloadEntries($results); diff --git a/lib/Controller/DelegationController.php b/lib/Controller/DelegationController.php index b09434269..3f072ef43 100644 --- a/lib/Controller/DelegationController.php +++ b/lib/Controller/DelegationController.php @@ -96,7 +96,6 @@ public function getAllCircles(): DataResponse { // As admin, get all circles, // As non-admin, only returns circles current user is members of. - /** @psalm-suppress PossiblyNullReference current user cannot be null */ if ($this->groupManager->isAdmin($this->userSession->getUser()->getUID())) { $circlesManager->startSuperSession(); } else { diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index e198f7d5a..151c68606 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -113,17 +113,14 @@ private function formatFolder(FolderWithMappingsAndCache $folder): array { #[NoAdminRequired] #[FrontpageRoute(verb: 'GET', url: '/folders')] public function getFolders(bool $applicable = false, int $offset = 0, ?int $limit = null, string $orderBy = 'mount_point', string $order = 'asc'): DataResponse { - /** @psalm-suppress DocblockTypeContradiction */ if ($limit !== null && $limit <= 0) { throw new OCSBadRequestException('The limit must be greater than 0.'); } - /** @psalm-suppress DocblockTypeContradiction */ if (!in_array($orderBy, ['mount_point', 'quota', 'groups', 'acl'], true)) { throw new OCSBadRequestException('The orderBy is not allowed.'); } - /** @psalm-suppress DocblockTypeContradiction */ if (!in_array($order, ['asc', 'desc'], true)) { throw new OCSBadRequestException('The order is not allowed.'); } diff --git a/lib/DAV/PropFindPlugin.php b/lib/DAV/PropFindPlugin.php index 0444bc6f4..da707b324 100644 --- a/lib/DAV/PropFindPlugin.php +++ b/lib/DAV/PropFindPlugin.php @@ -50,7 +50,6 @@ public function propFind(PropFind $propFind, INode $node): void { if ($node instanceof GroupFolderNode) { $propFind->handle( self::MOUNT_POINT_PROPERTYNAME, - /** @psalm-suppress PossiblyNullReference Null already checked above */ fn () => $this->userFolder->getRelativePath($node->getFileInfo()->getMountPoint()->getMountPoint()) ); $propFind->handle( diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index 69b92c999..cd3d82444 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -719,7 +719,6 @@ public function getFoldersFromCircleMemberships(IUser $user, ?int $folderId = nu $query->andWhere($query->expr()->eq('f.folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))); } - /** @psalm-suppress RedundantCondition */ if (method_exists($queryHelper, 'limitToMemberships')) { $queryHelper->limitToMemberships('a', 'circle_id', $federatedUser); } else { diff --git a/lib/Mount/GroupFolderNoEncryptionStorage.php b/lib/Mount/GroupFolderNoEncryptionStorage.php index 3cad1f7e6..9b1dbd18a 100644 --- a/lib/Mount/GroupFolderNoEncryptionStorage.php +++ b/lib/Mount/GroupFolderNoEncryptionStorage.php @@ -10,10 +10,5 @@ use OCP\Files\Storage\IDisableEncryptionStorage; -/** - * @psalm-suppress UnimplementedInterfaceMethod - * Psalm gets confused about missing methods, but those are implemented in OC\Files\Storage\Wrapper\Wrapper, - * so this suppression is fine and necessary as there is nothing wrong. - */ class GroupFolderNoEncryptionStorage extends GroupFolderStorage implements IDisableEncryptionStorage { } diff --git a/lib/Mount/GroupMountPoint.php b/lib/Mount/GroupMountPoint.php index 4f1c01b56..2588a6b7d 100644 --- a/lib/Mount/GroupMountPoint.php +++ b/lib/Mount/GroupMountPoint.php @@ -17,9 +17,6 @@ use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IStorageFactory; -/** - * @psalm-suppress PropertyNotSetInConstructor $class, $mountPoint and $mountProvider are set through the parent constructor - */ class GroupMountPoint extends MountPoint implements ISystemMountPoint, IShareOwnerlessMount { public function __construct( private readonly FolderDefinition $folder, diff --git a/lib/Versions/ExpireManager.php b/lib/Versions/ExpireManager.php index 161225f3c..567d78756 100644 --- a/lib/Versions/ExpireManager.php +++ b/lib/Versions/ExpireManager.php @@ -85,7 +85,6 @@ protected function getAutoExpireList(int $time, array $versions): array { $newInterval = false; // version checked so we can move to the next one } else { // time to move on to the next interval $interval++; - /** @psalm-suppress InvalidArrayOffset We know that $interval is <= 6 thanks to the -1 intervalEndsAfter in the last step */ $step = self::MAX_VERSIONS_PER_INTERVAL[$interval]['step']; $nextVersion = $prevTimestamp - $step; if (self::MAX_VERSIONS_PER_INTERVAL[$interval]['intervalEndsAfter'] === -1) { diff --git a/lib/Versions/VersionsBackend.php b/lib/Versions/VersionsBackend.php index 18c94e1a7..045035748 100644 --- a/lib/Versions/VersionsBackend.php +++ b/lib/Versions/VersionsBackend.php @@ -406,10 +406,6 @@ private function currentUserHasPermissions(FileInfo $sourceFile, int $permission return ($sourceFile->getPermissions() & $permissions) === $permissions; } - /** - * @inheritdoc - * @psalm-suppress MethodSignatureMismatch - The signature of the method is correct, but psalm somehow can't understand it - */ public function importVersionsForFile(IUser $user, Node $source, Node $target, array $versions): void { if (!$target->getStorage()->instanceOfStorage(GroupFolderStorage::class)) { return; diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 1ca5138cc..000000000 --- a/psalm.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/ACL/ACLScannerTest.php b/tests/ACL/ACLScannerTest.php index 003630db7..e72ea020c 100644 --- a/tests/ACL/ACLScannerTest.php +++ b/tests/ACL/ACLScannerTest.php @@ -43,7 +43,6 @@ public function testScanAclStorage(): void { $cache->calculateFolderSize('foo/bar'); $cache->calculateFolderSize('foo'); - /** @psalm-suppress PossiblyFalseReference */ $this->assertEquals(-1, $cache->get('foo/bar')->getSize()); $acls = $this->getAclManager([ @@ -63,10 +62,8 @@ public function testScanAclStorage(): void { $aclCache = $aclStorage->getCache(); $scanner->scan(''); - /** @psalm-suppress PossiblyFalseReference */ $this->assertEquals(0, $cache->get('foo/bar')->getSize()); - /** @psalm-suppress PossiblyFalseReference */ $this->assertEquals(31, $cache->get('foo/bar')->getPermissions()); $this->assertEquals(false, $aclCache->get('foo/bar')); } diff --git a/tests/ACL/ACLStorageWrapperTest.php b/tests/ACL/ACLStorageWrapperTest.php index 0e5d75810..6b8b48cbe 100644 --- a/tests/ACL/ACLStorageWrapperTest.php +++ b/tests/ACL/ACLStorageWrapperTest.php @@ -62,7 +62,6 @@ public function testOpenDir(): void { $dh = $this->storage->opendir('foo'); $result = []; - /** @psalm-suppress PossiblyFalseArgument */ while ($file = readdir($dh)) { $result[] = $file; } diff --git a/tests/Listeners/CircleDestroyedEventListenerTest.php b/tests/Listeners/CircleDestroyedEventListenerTest.php index 38ac59cb6..5cac85aea 100644 --- a/tests/Listeners/CircleDestroyedEventListenerTest.php +++ b/tests/Listeners/CircleDestroyedEventListenerTest.php @@ -37,7 +37,6 @@ public function testHandleInvalid(): void { ->expects($this->never()) ->method('deleteCircle'); - /** @psalm-suppress InvalidArgument on purpose */ $this->listener->handle($event); } diff --git a/tests/Listeners/NodeRenamedListenerTest.php b/tests/Listeners/NodeRenamedListenerTest.php index a7f97b49d..930b9f3eb 100644 --- a/tests/Listeners/NodeRenamedListenerTest.php +++ b/tests/Listeners/NodeRenamedListenerTest.php @@ -86,7 +86,6 @@ public function testHandleInvalid(): void { ->expects($this->never()) ->method('getSource'); - /** @psalm-suppress InvalidArgument on purpose */ $this->listener->handle($event); } diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml deleted file mode 100644 index f2fa7f973..000000000 --- a/tests/psalm-baseline.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/psalm-baseline.xml.license b/tests/psalm-baseline.xml.license deleted file mode 100644 index 7e235b600..000000000 --- a/tests/psalm-baseline.xml.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json deleted file mode 100644 index 254fa9322..000000000 --- a/vendor-bin/psalm/composer.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "require-dev": { - "vimeo/psalm": "^6.14", - "psalm/plugin-phpunit": "^0.19.3" - }, - "config": { - "platform": { - "php": "8.2.27" - } - } -} diff --git a/vendor-bin/psalm/composer.lock b/vendor-bin/psalm/composer.lock deleted file mode 100644 index 8a9cf92de..000000000 --- a/vendor-bin/psalm/composer.lock +++ /dev/null @@ -1,3406 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "f9f7139e009b603ee856af72f6b6f2e9", - "packages": [], - "packages-dev": [ - { - "name": "amphp/amp", - "version": "v3.1.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "5.23.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Future/functions.php", - "src/Internal/functions.php" - ], - "psr-4": { - "Amp\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - } - ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", - "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" - ], - "support": { - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v3.1.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-08-27T21:42:00+00:00" - }, - { - "name": "amphp/byte-stream", - "version": "v2.1.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/parser": "^1.1", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2.3" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.22.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php" - ], - "psr-4": { - "Amp\\ByteStream\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "https://amphp.org/byte-stream", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" - ], - "support": { - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-03-16T17:10:27+00:00" - }, - { - "name": "amphp/cache", - "version": "v2.0.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/cache.git", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Cache\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - } - ], - "description": "A fiber-aware cache API based on Amp and Revolt.", - "homepage": "https://amphp.org/cache", - "support": { - "issues": "https://github.com/amphp/cache/issues", - "source": "https://github.com/amphp/cache/tree/v2.0.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-19T03:38:06+00:00" - }, - { - "name": "amphp/dns", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/dns.git", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/parser": "^1", - "amphp/process": "^2", - "daverandom/libdns": "^2.0.2", - "ext-filter": "*", - "ext-json": "*", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.20" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Dns\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Wright", - "email": "addr@daverandom.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Async DNS resolution for Amp.", - "homepage": "https://github.com/amphp/dns", - "keywords": [ - "amp", - "amphp", - "async", - "client", - "dns", - "resolve" - ], - "support": { - "issues": "https://github.com/amphp/dns/issues", - "source": "https://github.com/amphp/dns/tree/v2.4.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-01-19T15:43:40+00:00" - }, - { - "name": "amphp/parallel", - "version": "v2.3.3", - "source": { - "type": "git", - "url": "https://github.com/amphp/parallel.git", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/parser": "^1", - "amphp/pipeline": "^1", - "amphp/process": "^2", - "amphp/serialization": "^1", - "amphp/socket": "^2", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" - }, - "type": "library", - "autoload": { - "files": [ - "src/Context/functions.php", - "src/Context/Internal/functions.php", - "src/Ipc/functions.php", - "src/Worker/functions.php" - ], - "psr-4": { - "Amp\\Parallel\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" - } - ], - "description": "Parallel processing component for Amp.", - "homepage": "https://github.com/amphp/parallel", - "keywords": [ - "async", - "asynchronous", - "concurrent", - "multi-processing", - "multi-threading" - ], - "support": { - "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.3" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-11-15T06:23:42+00:00" - }, - { - "name": "amphp/parser", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/parser.git", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", - "shasum": "" - }, - "require": { - "php": ">=7.4" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Parser\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A generator parser to make streaming parsers simple.", - "homepage": "https://github.com/amphp/parser", - "keywords": [ - "async", - "non-blocking", - "parser", - "stream" - ], - "support": { - "issues": "https://github.com/amphp/parser/issues", - "source": "https://github.com/amphp/parser/tree/v1.1.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-03-21T19:16:53+00:00" - }, - { - "name": "amphp/pipeline", - "version": "v1.2.3", - "source": { - "type": "git", - "url": "https://github.com/amphp/pipeline.git", - "reference": "7b52598c2e9105ebcddf247fc523161581930367" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", - "reference": "7b52598c2e9105ebcddf247fc523161581930367", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Pipeline\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Asynchronous iterators and operators.", - "homepage": "https://amphp.org/pipeline", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "iterator", - "non-blocking" - ], - "support": { - "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.3" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-03-16T16:33:53+00:00" - }, - { - "name": "amphp/process", - "version": "v2.0.3", - "source": { - "type": "git", - "url": "https://github.com/amphp/process.git", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Process\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A fiber-aware process manager based on Amp and Revolt.", - "homepage": "https://amphp.org/process", - "support": { - "issues": "https://github.com/amphp/process/issues", - "source": "https://github.com/amphp/process/tree/v2.0.3" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-19T03:13:44+00:00" - }, - { - "name": "amphp/serialization", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Serialization\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Serialization tools for IPC and data storage in PHP.", - "homepage": "https://github.com/amphp/serialization", - "keywords": [ - "async", - "asynchronous", - "serialization", - "serialize" - ], - "support": { - "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" - }, - "time": "2020-03-25T21:39:07+00:00" - }, - { - "name": "amphp/socket", - "version": "v2.3.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/socket.git", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/dns": "^2", - "ext-openssl": "*", - "kelunik/certificate": "^1.1", - "league/uri": "^6.5 | ^7", - "league/uri-interfaces": "^2.3 | ^7", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "amphp/process": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "5.20" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php", - "src/SocketAddress/functions.php" - ], - "psr-4": { - "Amp\\Socket\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@gmail.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", - "homepage": "https://github.com/amphp/socket", - "keywords": [ - "amp", - "async", - "encryption", - "non-blocking", - "sockets", - "tcp", - "tls" - ], - "support": { - "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.3.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-21T14:33:03+00:00" - }, - { - "name": "amphp/sync", - "version": "v2.3.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/sync.git", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.23" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Sync\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" - } - ], - "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", - "homepage": "https://github.com/amphp/sync", - "keywords": [ - "async", - "asynchronous", - "mutex", - "semaphore", - "synchronization" - ], - "support": { - "issues": "https://github.com/amphp/sync/issues", - "source": "https://github.com/amphp/sync/tree/v2.3.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-08-03T19:31:26+00:00" - }, - { - "name": "composer/pcre", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" - }, - "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-11-12T16:29:46+00:00" - }, - { - "name": "composer/semver", - "version": "3.4.4", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.11", - "symfony/phpunit-bridge": "^3 || ^7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.4" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2025-08-20T19:15:30+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", - "shasum": "" - }, - "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" - }, - "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Composer\\XdebugHandler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", - "keywords": [ - "Xdebug", - "performance" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-05-06T16:37:16+00:00" - }, - { - "name": "danog/advanced-json-rpc", - "version": "v3.2.3", - "source": { - "type": "git", - "url": "https://github.com/danog/php-advanced-json-rpc.git", - "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/ae703ea7b4811797a10590b6078de05b3b33dd91", - "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91", - "shasum": "" - }, - "require": { - "netresearch/jsonmapper": "^5", - "php": ">=8.1", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0 || ^6" - }, - "replace": { - "felixfbecker/php-advanced-json-rpc": "^3" - }, - "require-dev": { - "phpunit/phpunit": "^9" - }, - "type": "library", - "autoload": { - "psr-4": { - "AdvancedJsonRpc\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - }, - { - "name": "Daniil Gentili", - "email": "daniil@daniil.it" - } - ], - "description": "A more advanced JSONRPC implementation", - "support": { - "issues": "https://github.com/danog/php-advanced-json-rpc/issues", - "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.3" - }, - "time": "2026-01-12T21:07:10+00:00" - }, - { - "name": "daverandom/libdns", - "version": "v2.1.0", - "source": { - "type": "git", - "url": "https://github.com/DaveRandom/LibDNS.git", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "Required for IDN support" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "LibDNS\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "DNS protocol implementation written in pure PHP", - "keywords": [ - "dns" - ], - "support": { - "issues": "https://github.com/DaveRandom/LibDNS/issues", - "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" - }, - "time": "2024-04-12T12:12:48+00:00" - }, - { - "name": "dnoegel/php-xdg-base-dir", - "version": "v0.1.1", - "source": { - "type": "git", - "url": "https://github.com/dnoegel/php-xdg-base-dir.git", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "XdgBaseDir\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "implementation of xdg base directory specification for php", - "support": { - "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", - "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" - }, - "time": "2019-12-04T15:06:13+00:00" - }, - { - "name": "doctrine/deprecations", - "version": "1.1.6", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "phpunit/phpunit": "<=7.5 || >=14" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^14", - "phpstan/phpstan": "1.4.10 || 2.1.30", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.6" - }, - "time": "2026-02-07T07:09:04+00:00" - }, - { - "name": "felixfbecker/language-server-protocol", - "version": "v1.5.3", - "source": { - "type": "git", - "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpstan/phpstan": "*", - "squizlabs/php_codesniffer": "^3.1", - "vimeo/psalm": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "LanguageServerProtocol\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - } - ], - "description": "PHP classes for the Language Server Protocol", - "keywords": [ - "language", - "microsoft", - "php", - "server" - ], - "support": { - "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" - }, - "time": "2024-04-30T00:40:11+00:00" - }, - { - "name": "fidry/cpu-core-counter", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-deprecation-rules": "^2.0.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^8.5.31 || ^9.5.26", - "webmozarts/strict-phpunit": "^7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" - } - ], - "description": "Tiny utility to get the number of CPU cores.", - "keywords": [ - "CPU", - "core" - ], - "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" - }, - "funding": [ - { - "url": "https://github.com/theofidry", - "type": "github" - } - ], - "time": "2025-08-14T07:29:31+00:00" - }, - { - "name": "kelunik/certificate", - "version": "v1.1.3", - "source": { - "type": "git", - "url": "https://github.com/kelunik/certificate.git", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "php": ">=7.0" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^6 | 7 | ^8 | ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Kelunik\\Certificate\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Access certificate details and transform between different formats.", - "keywords": [ - "DER", - "certificate", - "certificates", - "openssl", - "pem", - "x509" - ], - "support": { - "issues": "https://github.com/kelunik/certificate/issues", - "source": "https://github.com/kelunik/certificate/tree/v1.1.3" - }, - "time": "2023-02-03T21:26:53+00:00" - }, - { - "name": "league/uri", - "version": "7.8.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", - "shasum": "" - }, - "require": { - "league/uri-interfaces": "^7.8", - "php": "^8.1", - "psr/http-factory": "^1" - }, - "conflict": { - "league/uri-schemes": "^1.0" - }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-dom": "to convert the URI into an HTML anchor tag", - "ext-fileinfo": "to create Data URI from file contennts", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "ext-uri": "to use the PHP native URI class", - "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", - "league/uri-components": "to provide additional tools to manipulate URI objects components", - "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", - "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Uri\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "URI manipulation library", - "homepage": "https://uri.thephpleague.com", - "keywords": [ - "URN", - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "middleware", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc2141", - "rfc3986", - "rfc3987", - "rfc6570", - "rfc8141", - "uri", - "uri-template", - "url", - "ws" - ], - "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2026-01-14T17:24:56+00:00" - }, - { - "name": "league/uri-interfaces", - "version": "7.8.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^8.1", - "psr/http-message": "^1.1 || ^2.0" - }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Uri\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", - "homepage": "https://uri.thephpleague.com", - "keywords": [ - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc3986", - "rfc3987", - "rfc6570", - "uri", - "url", - "ws" - ], - "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2026-01-15T06:54:53+00:00" - }, - { - "name": "netresearch/jsonmapper", - "version": "v5.0.0", - "source": { - "type": "git", - "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", - "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", - "squizlabs/php_codesniffer": "~3.5" - }, - "type": "library", - "autoload": { - "psr-0": { - "JsonMapper": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "OSL-3.0" - ], - "authors": [ - { - "name": "Christian Weiske", - "email": "cweiske@cweiske.de", - "homepage": "http://github.com/cweiske/jsonmapper/", - "role": "Developer" - } - ], - "description": "Map nested JSON structures onto PHP classes", - "support": { - "email": "cweiske@cweiske.de", - "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" - }, - "time": "2024-09-08T10:20:00+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v5.7.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" - }, - "time": "2025-12-06T11:56:16+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "6.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", - "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.1", - "ext-filter": "*", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^2.0", - "phpstan/phpdoc-parser": "^2.0", - "webmozart/assert": "^1.9.1 || ^2" - }, - "require-dev": { - "mockery/mockery": "~1.3.5 || ~1.6.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26", - "shipmonk/dead-code-detector": "^0.5.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.1" - }, - "time": "2026-01-20T15:30:42+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", - "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev", - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" - }, - "time": "2026-01-06T21:53:42+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "2.3.2", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" - }, - "time": "2026-01-25T14:56:51+00:00" - }, - { - "name": "psalm/plugin-phpunit", - "version": "0.19.5", - "source": { - "type": "git", - "url": "https://github.com/psalm/psalm-plugin-phpunit.git", - "reference": "143f9d5e049fffcdbc0da3fbb99f6149f9d3e2dc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/psalm/psalm-plugin-phpunit/zipball/143f9d5e049fffcdbc0da3fbb99f6149f9d3e2dc", - "reference": "143f9d5e049fffcdbc0da3fbb99f6149f9d3e2dc", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "php": ">=8.1", - "vimeo/psalm": "dev-master || ^6.10.0" - }, - "conflict": { - "phpspec/prophecy": "<1.20.0", - "phpspec/prophecy-phpunit": "<2.3.0", - "phpunit/phpunit": "<8.5.1" - }, - "require-dev": { - "php": "^7.3 || ^8.0", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", - "squizlabs/php_codesniffer": "^3.3.1", - "weirdan/prophecy-shim": "^1.0 || ^2.0" - }, - "type": "psalm-plugin", - "extra": { - "psalm": { - "pluginClass": "Psalm\\PhpUnitPlugin\\Plugin" - } - }, - "autoload": { - "psr-4": { - "Psalm\\PhpUnitPlugin\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matt Brown", - "email": "github@muglug.com" - } - ], - "description": "Psalm plugin for PHPUnit", - "support": { - "issues": "https://github.com/psalm/psalm-plugin-phpunit/issues", - "source": "https://github.com/psalm/psalm-plugin-phpunit/tree/0.19.5" - }, - "time": "2025-03-31T18:49:55+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" - }, - "time": "2024-09-11T13:17:53+00:00" - }, - { - "name": "revolt/event-loop", - "version": "v1.0.8", - "source": { - "type": "git", - "url": "https://github.com/revoltphp/event-loop.git", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.15" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Revolt\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "ceesjank@gmail.com" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Rock-solid event loop for concurrent PHP applications.", - "keywords": [ - "async", - "asynchronous", - "concurrency", - "event", - "event-loop", - "non-blocking", - "scheduler" - ], - "support": { - "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" - }, - "time": "2025-08-27T21:33:23+00:00" - }, - { - "name": "sebastian/diff", - "version": "6.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:53:05+00:00" - }, - { - "name": "spatie/array-to-xml", - "version": "3.4.4", - "source": { - "type": "git", - "url": "https://github.com/spatie/array-to-xml.git", - "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/88b2f3852a922dd73177a68938f8eb2ec70c7224", - "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": "^8.0" - }, - "require-dev": { - "mockery/mockery": "^1.2", - "pestphp/pest": "^1.21", - "spatie/pest-plugin-snapshots": "^1.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Spatie\\ArrayToXml\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://freek.dev", - "role": "Developer" - } - ], - "description": "Convert an array to xml", - "homepage": "https://github.com/spatie/array-to-xml", - "keywords": [ - "array", - "convert", - "xml" - ], - "support": { - "source": "https://github.com/spatie/array-to-xml/tree/3.4.4" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-12-15T09:00:41+00:00" - }, - { - "name": "symfony/console", - "version": "v7.4.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2|^8.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/lock": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v7.4.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-01-13T11:36:38+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:21:43+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v7.4.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" - }, - "require-dev": { - "symfony/process": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-11-27T13:27:24+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-06-27T09:58:17+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-23T08:48:59+00:00" - }, - { - "name": "symfony/polyfill-php84", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php84\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-06-24T13:30:11+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.6.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-07-15T11:30:57+00:00" - }, - { - "name": "symfony/string", - "version": "v7.4.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.33", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" - }, - "require-dev": { - "symfony/emoji": "^7.1|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v7.4.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-01-12T10:54:30+00:00" - }, - { - "name": "vimeo/psalm", - "version": "6.15.1", - "source": { - "type": "git", - "url": "https://github.com/vimeo/psalm.git", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/28dc127af1b5aecd52314f6f645bafc10d0e11f9", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/parallel": "^2.3", - "composer-runtime-api": "^2", - "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^2.0 || ^3.0", - "danog/advanced-json-rpc": "^3.1", - "dnoegel/php-xdg-base-dir": "^0.1.1", - "ext-ctype": "*", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "ext-tokenizer": "*", - "felixfbecker/language-server-protocol": "^1.5.3", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", - "netresearch/jsonmapper": "^5.0", - "nikic/php-parser": "^5.0.0", - "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0", - "spatie/array-to-xml": "^2.17.0 || ^3.0", - "symfony/console": "^6.0 || ^7.0 || ^8.0", - "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3 || ^8.0", - "symfony/polyfill-php84": "^1.31.0" - }, - "provide": { - "psalm/psalm": "self.version" - }, - "require-dev": { - "amphp/phpunit-util": "^3", - "bamarni/composer-bin-plugin": "^1.4", - "brianium/paratest": "^6.9", - "danog/class-finder": "^0.4.8", - "dg/bypass-finals": "^1.5", - "ext-curl": "*", - "mockery/mockery": "^1.5", - "nunomaduro/mock-final-classes": "^1.1", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpdoc-parser": "^1.6", - "phpunit/phpunit": "^9.6", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.19", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^6.0 || ^7.0 || ^8.0" - }, - "suggest": { - "ext-curl": "In order to send data to shepherd", - "ext-igbinary": "^2.0.5 is required, used to serialize caching data" - }, - "bin": [ - "psalm", - "psalm-language-server", - "psalm-plugin", - "psalm-refactor", - "psalm-review", - "psalter" - ], - "type": "project", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev", - "dev-2.x": "2.x-dev", - "dev-3.x": "3.x-dev", - "dev-4.x": "4.x-dev", - "dev-5.x": "5.x-dev", - "dev-6.x": "6.x-dev", - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psalm\\": "src/Psalm/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matthew Brown" - }, - { - "name": "Daniil Gentili", - "email": "daniil@daniil.it" - } - ], - "description": "A static analysis tool for finding errors in PHP applications", - "keywords": [ - "code", - "inspection", - "php", - "static analysis" - ], - "support": { - "docs": "https://psalm.dev/docs", - "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm" - }, - "time": "2026-02-07T19:27:16+00:00" - }, - { - "name": "webmozart/assert", - "version": "2.1.2", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", - "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-filter": "*", - "php": "^8.2" - }, - "suggest": { - "ext-intl": "", - "ext-simplexml": "", - "ext-spl": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-feature/2-0": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - }, - { - "name": "Woody Gilk", - "email": "woody.gilk@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.2" - }, - "time": "2026-01-13T14:02:24+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "platform-overrides": { - "php": "8.2.27" - }, - "plugin-api-version": "2.6.0" -} From ad7483a1a20b465dbd797b16711a02f0030b8f55 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 5 Feb 2026 08:57:53 +0100 Subject: [PATCH 3/3] feat(phpstan): Setup Signed-off-by: provokateurin --- .github/dependabot.yml | 1 + .github/workflows/phpstan.yml | 62 ++++ composer.json | 3 +- lib/ACL/ACLCacheWrapper.php | 11 +- lib/ACL/ACLStorageWrapper.php | 17 +- lib/ACL/Rule.php | 43 ++- lib/ACL/RuleManager.php | 19 ++ lib/ACL/UserMapping/UserMappingManager.php | 3 +- lib/AppInfo/Application.php | 15 +- lib/AuthorizedAdminSettingMiddleware.php | 3 + lib/Command/ACL.php | 49 ++- lib/Command/Create.php | 4 +- lib/Command/Delete.php | 2 + lib/Command/FolderCommand.php | 8 +- lib/Command/Group.php | 9 +- lib/Command/ListCommand.php | 3 +- lib/Command/Rename.php | 7 +- lib/Command/Scan.php | 27 +- lib/Command/Trashbin/Cleanup.php | 6 +- lib/Controller/DelegationController.php | 9 +- lib/Controller/FolderController.php | 78 +++-- lib/DAV/ACLPlugin.php | 17 ++ lib/DAV/GroupFoldersHome.php | 4 + lib/DAV/RootCollection.php | 1 + lib/Folder/FolderDefinition.php | 3 + lib/Folder/FolderDefinitionWithMappings.php | 26 +- .../FolderDefinitionWithPermissions.php | 21 +- lib/Folder/FolderManager.php | 55 ++-- lib/Folder/FolderWithMappingsAndCache.php | 27 +- lib/{ => Listeners}/CacheListener.php | 32 +- .../CircleDestroyedEventListener.php | 1 + .../LoadAdditionalScriptsListener.php | 1 + lib/Listeners/NodeRenamedListener.php | 1 + .../Version103000Date20180806161724.php | 7 +- .../Version19000Date20241029123147.php | 3 - .../Version20001Date20250612140256.php | 4 +- .../Version2100000Date20251104151854.php | 6 - .../Version21000Date20250925152053.php | 4 - lib/Mount/CacheRootPermissionsMask.php | 1 + lib/Mount/FolderStorageManager.php | 53 +++- lib/Mount/GroupFolderStorage.php | 4 + lib/Mount/GroupMountPoint.php | 4 + lib/Mount/MountProvider.php | 20 +- lib/Mount/RootEntryCache.php | 7 + lib/Mount/RootPermissionsMask.php | 9 +- lib/ResponseDefinitions.php | 18 +- lib/Service/DelegationService.php | 3 + lib/Service/FoldersFilter.php | 10 +- lib/Settings/Admin.php | 7 + lib/Trash/TrashBackend.php | 105 +++---- lib/Trash/TrashManager.php | 37 +-- lib/Versions/ExpireManager.php | 5 +- lib/Versions/GroupVersion.php | 3 + lib/Versions/GroupVersionEntity.php | 10 + lib/Versions/GroupVersionsExpireManager.php | 2 - lib/Versions/VersionsBackend.php | 113 +++++-- openapi.json | 3 +- phpstan.neon | 127 ++++++++ templates/index.php | 9 +- tests/ACL/ACLCacheWrapperTest.php | 1 + tests/ACL/ACLScannerTest.php | 13 +- tests/ACL/ACLStorageWrapperTest.php | 3 +- tests/ACL/RuleManagerTest.php | 5 +- tests/ACL/RuleTest.php | 7 + tests/Folder/FolderManagerTest.php | 2 +- .../CircleDestroyedEventListenerTest.php | 3 + .../LoadAdditionalScriptsListenerTest.php | 4 + tests/Listeners/NodeRenamedListenerTest.php | 3 + tests/Trash/TrashBackendTest.php | 7 - .../doctrine_dbal_arrayparametertype.php | 37 +++ tests/stubs/oc_app.php | 281 ++++++++++++++++++ ...n_exceptions_decryptionfailedexception.php | 15 + tests/stubs/oc_files_fileinfo.php | 273 +++++++++++++++++ .../oc_files_storage_wrapper_encryption.php | 230 ++++++++++++++ tests/stubs/oc_hook.php | 65 ++++ ...av_connector_sabre_exception_forbidden.php | 33 ++ tests/stubs/oca_files_trashbin_storage.php | 82 +++++ tests/stubs/oca_richdocuments_db_wopi.php | 161 ++++++++++ .../stubs/oca_richdocuments_db_wopimapper.php | 85 ++++++ ...ole_exception_invalidargumentexception.php | 19 ++ ...omponent_console_helper_questionhelper.php | 85 ++++++ vendor-bin/phpstan/composer.json | 5 + vendor-bin/phpstan/composer.lock | 72 +++++ 83 files changed, 2310 insertions(+), 323 deletions(-) create mode 100644 .github/workflows/phpstan.yml rename lib/{ => Listeners}/CacheListener.php (51%) create mode 100644 phpstan.neon create mode 100644 tests/stubs/doctrine_dbal_arrayparametertype.php create mode 100644 tests/stubs/oc_app.php create mode 100644 tests/stubs/oc_encryption_exceptions_decryptionfailedexception.php create mode 100644 tests/stubs/oc_files_fileinfo.php create mode 100644 tests/stubs/oc_files_storage_wrapper_encryption.php create mode 100644 tests/stubs/oc_hook.php create mode 100644 tests/stubs/oca_dav_connector_sabre_exception_forbidden.php create mode 100644 tests/stubs/oca_files_trashbin_storage.php create mode 100644 tests/stubs/oca_richdocuments_db_wopi.php create mode 100644 tests/stubs/oca_richdocuments_db_wopimapper.php create mode 100644 tests/stubs/symfony_component_console_exception_invalidargumentexception.php create mode 100644 tests/stubs/symfony_component_console_helper_questionhelper.php create mode 100644 vendor-bin/phpstan/composer.json create mode 100644 vendor-bin/phpstan/composer.lock diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e548184cf..faf3f9a33 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,6 +17,7 @@ updates: - /vendor-bin/openapi-extractor - /vendor-bin/phpunit - /vendor-bin/rector + - /vendor-bin/phpstan schedule: interval: weekly day: saturday diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 000000000..26bd16e21 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,62 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Static analysis + +on: pull_request + +concurrency: + group: phpstan-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + static-analysis: + runs-on: ubuntu-latest + + name: static-phpstan-analysis + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Get php version + id: versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2 + + - name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min-id }} in phpstan.neon + run: "grep 'min: ${{ steps.versions.outputs.php-min-id }}' phpstan.neon" + + - name: Set up php${{ steps.versions.outputs.php-available }} + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0 + with: + php-version: ${{ steps.versions.outputs.php-available }} + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite + coverage: none + ini-file: development + # Temporary workaround for missing pcntl_* in PHP 8.3 + ini-values: disable_functions= + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: | + composer remove nextcloud/ocp --dev --no-scripts + composer i + + - name: Check for vulnerable PHP dependencies + run: composer require --dev roave/security-advisories:dev-latest + + - name: Install nextcloud/ocp + run: composer require --dev nextcloud/ocp:dev-${{ steps.versions.outputs.branches-max }} --ignore-platform-reqs --with-dependencies + + - name: Run coding standards check + run: composer run phpstan -- --no-progress diff --git a/composer.json b/composer.json index d20dafc47..065f18f00 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "test:unit": "phpunit -c tests/phpunit.xml", "test:unit:coverage": "XDEBUG_MODE=coverage phpunit -c tests/phpunit.xml", "rector": "rector && composer cs:fix", - "openapi": "generate-spec && npm run typescript:generate" + "openapi": "generate-spec && npm run typescript:generate", + "phpstan": "phpstan analyse -c phpstan.neon" }, "config": { "allow-plugins": { diff --git a/lib/ACL/ACLCacheWrapper.php b/lib/ACL/ACLCacheWrapper.php index e474a8eb4..0f88b63a7 100644 --- a/lib/ACL/ACLCacheWrapper.php +++ b/lib/ACL/ACLCacheWrapper.php @@ -24,6 +24,9 @@ public function __construct( parent::__construct($cache); } + /** + * @param array $rules + */ private function getACLPermissionsForPath(string $path, array $rules = []): int { if ($rules) { $permissions = $this->aclManager->getPermissionsForPathFromRules($this->folderId, $path, $rules); @@ -43,9 +46,12 @@ private function getACLPermissionsForPath(string $path, array $rules = []): int return $canRead ? $permissions : 0; } + /** + * @param array $rules + */ #[\Override] protected function formatCacheEntry($entry, array $rules = []): ICacheEntry|false { - if (isset($entry['permissions'])) { + if (isset($entry['permissions']) && is_int($entry['permissions']) && is_string($entry['path'])) { $entry['scan_permissions'] ??= $entry['permissions']; $entry['permissions'] &= $this->getACLPermissionsForPath($entry['path'], $rules); if (!$entry['permissions']) { @@ -56,6 +62,9 @@ protected function formatCacheEntry($entry, array $rules = []): ICacheEntry|fals return $entry; } + /** + * @return array + */ #[\Override] public function getFolderContentsById($fileId, ?string $mimeTypeFilter = null): array { $results = $this->getCache()->getFolderContentsById($fileId, $mimeTypeFilter); diff --git a/lib/ACL/ACLStorageWrapper.php b/lib/ACL/ACLStorageWrapper.php index 796203ae3..237b7a43d 100644 --- a/lib/ACL/ACLStorageWrapper.php +++ b/lib/ACL/ACLStorageWrapper.php @@ -9,6 +9,7 @@ namespace OCA\GroupFolders\ACL; use Icewind\Streams\IteratorDirectory; +use OC\Files\Storage\Storage; use OC\Files\Storage\Wrapper\Wrapper; use OCP\Constants; use OCP\Files\Cache\ICache; @@ -22,6 +23,9 @@ class ACLStorageWrapper extends Wrapper implements IConstructableStorage { private readonly int $folderId; private readonly int $storageId; + /** + * @param array{storage: Storage, acl_manager: ACLManager, in_share: bool, folder_id: int, storage_id: int} $arguments + */ public function __construct(array $arguments) { parent::__construct($arguments); $this->aclManager = $arguments['acl_manager']; @@ -212,7 +216,7 @@ public function getCache(string $path = '', ?IStorage $storage = null): ICache { public function getMetaData(string $path): ?array { $data = parent::getMetaData($path); - if (is_array($data) && isset($data['permissions'])) { + if (is_array($data) && isset($data['permissions']) && is_int($data['permissions'])) { $data['scan_permissions'] ??= $data['permissions']; $data['permissions'] &= $this->getACLPermissionsForPath($path); } @@ -331,10 +335,21 @@ public function getDirectDownload(string $path): array|false { return parent::getDirectDownload($path); } + /** + * @return \Traversable> + */ #[\Override] public function getDirectoryContent(string $directory): \Traversable { $content = $this->getWrapperStorage()->getDirectoryContent($directory); foreach ($content as $data) { + if (!isset($data['permissions']) || !is_int($data['permissions'])) { + throw new \RuntimeException('permissions is not an integer.'); + } + + if (!isset($data['name']) || !is_string($data['name'])) { + throw new \RuntimeException('name is not a string.'); + } + $data['scan_permissions'] ??= $data['permissions']; $data['permissions'] &= $this->getACLPermissionsForPath(rtrim($directory, '/') . '/' . $data['name']); diff --git a/lib/ACL/Rule.php b/lib/ACL/Rule.php index c82613466..874be5a8d 100644 --- a/lib/ACL/Rule.php +++ b/lib/ACL/Rule.php @@ -109,6 +109,16 @@ public function xmlSerialize(Writer $writer): void { $writer->write($data); } + /** + * @return array{ + * mapping: array{ + * type: 'user'|'group'|'dummy'|'circle', + * id: string, + * }, + * mask: int, + * permissions: int, + * } + */ #[\Override] public function jsonSerialize(): array { return [ @@ -125,19 +135,44 @@ public function jsonSerialize(): array { public static function xmlDeserialize(Reader $reader): Rule { $elements = \Sabre\Xml\Deserializer\keyValue($reader); + $mappingType = $elements[self::MAPPING_TYPE]; + if (!is_string($mappingType)) { + throw new \RuntimeException(self::MAPPING_TYPE . ' is not a string.'); + } + + if (!in_array($mappingType, ['user', 'group', 'circle'])) { + throw new \RuntimeException(self::MAPPING_TYPE . ' does not have a valid value.'); + } + + $mappingId = $elements[self::MAPPING_ID]; + if (!is_string($mappingId)) { + throw new \RuntimeException(self::MAPPING_ID . ' is not a string.'); + } + + $mask = $elements[self::MASK]; + if (!is_string($mask)) { + throw new \RuntimeException(self::MASK . ' is not a string.'); + } + + $permission = $elements[self::PERMISSIONS]; + if (!is_string($permission)) { + throw new \RuntimeException(self::PERMISSIONS . ' is not a string.'); + } + return new Rule( new UserMapping( - $elements[self::MAPPING_TYPE], - $elements[self::MAPPING_ID] + $mappingType, + $mappingId, ), -1, - (int)$elements[self::MASK], - (int)$elements[self::PERMISSIONS] + (int)$mask, + (int)$permission, ); } /** * merge multiple rules that apply on the same file where allow overwrites deny + * @param Rule[] $rules */ public static function mergeRules(array $rules): Rule { // or'ing the masks to get a new mask that masks all set permissions diff --git a/lib/ACL/RuleManager.php b/lib/ACL/RuleManager.php index 5787e5313..279d10077 100644 --- a/lib/ACL/RuleManager.php +++ b/lib/ACL/RuleManager.php @@ -25,6 +25,9 @@ public function __construct( ) { } + /** + * @param array{mapping_type?: 'user'|'group'|'dummy'|'circle', mapping_id: string, fileid: int, mask: int, permissions: int} $data + */ private function createRule(array $data): ?Rule { if (!isset($data['mapping_type'])) { return null; @@ -58,6 +61,7 @@ public function getRulesForFilesById(IUser $user, array $fileIds): array { $query->expr()->eq('mapping_id', $query->createNamedParameter($userMapping->getId())) ), $userMappings))); + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); $result = []; @@ -94,6 +98,7 @@ public function getRulesForFilesByPath(IUser $user, int $storageId, array $fileP $query->expr()->eq('mapping_id', $query->createNamedParameter($userMapping->getId())) ), $userMappings))); + /** @var list $rows */ $rows = array_merge($rows, $query->executeQuery()->fetchAll()); } @@ -127,6 +132,7 @@ public function getRulesForFilesByIds(IUser $user, array $fileIds): array { $query->expr()->eq('a.mapping_id', $query->createNamedParameter($userMapping->getId())) ), $userMappings))); + /** @var list $rows */ $rows = array_merge($rows, $query->executeQuery()->fetchAll()); } @@ -161,6 +167,7 @@ public function getRulesForFilesByParent(IUser $user, int $storageId, int $paren ) ); + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); $result = []; @@ -193,9 +200,15 @@ public function getAllRulesForPaths(int $storageId, array $filePaths): array { $rows = $query->executeQuery()->fetchAll(); + /** @var list $rows */ return $this->rulesByPath($rows); } + /** + * @param list $rows + * @param array> $result + * @return array> + */ private function rulesByPath(array $rows, array $result = []): array { foreach ($rows as $row) { $rule = $this->createRule($row); @@ -210,6 +223,10 @@ private function rulesByPath(array $rows, array $result = []): array { return $result; } + /** + * @param list $rows + * @return array>> + */ private function rulesByFileId(array $rows): array { $result = []; foreach ($rows as $row) { @@ -244,6 +261,7 @@ public function getAllRulesForPrefix(int $storageId, string $prefix): array { $rows = $query->executeQuery()->fetchAll(); + /** @var list $rows */ return $this->rulesByPath($rows); } @@ -272,6 +290,7 @@ public function getRulesForPrefix(IUser $user, int $storageId, string $prefix): $rows = $query->executeQuery()->fetchAll(); + /** @var list $rows */ return $this->rulesByPath($rows); } diff --git a/lib/ACL/UserMapping/UserMappingManager.php b/lib/ACL/UserMapping/UserMappingManager.php index c22c4e2ff..381a8ba37 100644 --- a/lib/ACL/UserMapping/UserMappingManager.php +++ b/lib/ACL/UserMapping/UserMappingManager.php @@ -33,7 +33,7 @@ public function __construct( #[\Override] public function getMappingsForUser(IUser $user, bool $userAssignable = true): array { $groupMappings = array_values(array_map(fn (IGroup $group): UserMapping => new UserMapping('group', $group->getGID(), $group->getDisplayName()), $this->groupManager->getUserGroups($user))); - $circleMappings = array_values(array_map(fn (Circle $circle): UserMapping => new UserMapping('circle', $circle->getSingleId(), $circle->getDisplayName()), $this->getUserCircles($user->getUID()))); + $circleMappings = array_map(fn (Circle $circle): UserMapping => new UserMapping('circle', $circle->getSingleId(), $circle->getDisplayName()), $this->getUserCircles($user->getUID())); return array_merge([ new UserMapping('user', $user->getUID(), $user->getDisplayName()), @@ -91,6 +91,7 @@ private function getCircle(string $groupId): ?Circle { /** * returns list of circles a user is member of + * @return list */ private function getUserCircles(string $userId): array { $circlesManager = $this->getCirclesManager(); diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d963acb7b..2c5d70e80 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -20,12 +20,12 @@ use OCA\GroupFolders\BackgroundJob\ExpireGroupPlaceholder; use OCA\GroupFolders\BackgroundJob\ExpireGroupTrash as ExpireGroupTrashJob; use OCA\GroupFolders\BackgroundJob\ExpireGroupVersions as ExpireGroupVersionsJob; -use OCA\GroupFolders\CacheListener; use OCA\GroupFolders\Command\ExpireGroup\ExpireGroupBase; use OCA\GroupFolders\Command\ExpireGroup\ExpireGroupTrash; use OCA\GroupFolders\Command\ExpireGroup\ExpireGroupVersions; use OCA\GroupFolders\Command\ExpireGroup\ExpireGroupVersionsTrash; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Listeners\CacheListener; use OCA\GroupFolders\Listeners\CircleDestroyedEventListener; use OCA\GroupFolders\Listeners\LoadAdditionalScriptsListener; use OCA\GroupFolders\Listeners\NodeRenamedListener; @@ -42,6 +42,8 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\CacheEntryInsertedEvent; +use OCP\Files\Cache\CacheEntryUpdatedEvent; use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Events\Node\NodeRenamedEvent; use OCP\Files\IRootFolder; @@ -49,6 +51,7 @@ use OCP\Files\Storage\IStorageFactory; use OCP\Group\Events\GroupDeletedEvent; use OCP\IAppConfig; +use OCP\IContainer; use OCP\IDBConnection; use OCP\IRequest; use OCP\IUserManager; @@ -60,6 +63,9 @@ class Application extends App implements IBootstrap { public const APP_ID = 'groupfolders'; + /** + * @param array $urlParams + */ public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); } @@ -79,6 +85,8 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadAdditionalScriptsListener::class); $context->registerEventListener(CircleDestroyedEvent::class, CircleDestroyedEventListener::class); $context->registerEventListener(NodeRenamedEvent::class, NodeRenamedListener::class); + $context->registerEventListener(CacheEntryInsertedEvent::class, CacheListener::class, 99999); + $context->registerEventListener(CacheEntryUpdatedEvent::class, CacheListener::class, 99999); $context->registerService(MountProvider::class, function (ContainerInterface $c): MountProvider { /** @var IAppConfig $config */ @@ -167,7 +175,7 @@ public function register(IRegistrationContext $context): void { return new ExpireGroupPlaceholder($c->get(ITimeFactory::class)); }); - $context->registerService(\OCA\GroupFolders\BackgroundJob\ExpireGroupTrash::class, function (ContainerInterface $c): TimedJob { + $context->registerService(\OCA\GroupFolders\BackgroundJob\ExpireGroupTrash::class, function (IContainer $c): TimedJob { if (interface_exists(\OCA\Files_Trashbin\Trash\ITrashBackend::class)) { return new ExpireGroupTrashJob( $c->get(TrashBackend::class), @@ -187,13 +195,12 @@ public function register(IRegistrationContext $context): void { #[\Override] public function boot(IBootContext $context): void { - $context->injectFn(function (IMountProviderCollection $mountProviderCollection, CacheListener $cacheListener, IEventDispatcher $eventDispatcher): void { + $context->injectFn(function (IMountProviderCollection $mountProviderCollection, IEventDispatcher $eventDispatcher): void { $mountProviderCollection->registerProvider(Server::get(MountProvider::class)); $eventDispatcher->addListener(GroupDeletedEvent::class, function (GroupDeletedEvent $event): void { Server::get(FolderManager::class)->deleteGroup($event->getGroup()->getGID()); }); - $cacheListener->listen(); }); } } diff --git a/lib/AuthorizedAdminSettingMiddleware.php b/lib/AuthorizedAdminSettingMiddleware.php index 30464e726..5184459cc 100644 --- a/lib/AuthorizedAdminSettingMiddleware.php +++ b/lib/AuthorizedAdminSettingMiddleware.php @@ -38,6 +38,9 @@ public function beforeController(Controller $controller, string $methodName): vo } } + /** + * @return JSONResponse|TemplateResponse + */ #[\Override] public function afterException(Controller $controller, string $methodName, Exception $exception): Response { /** @var Http::STATUS_* $code */ diff --git a/lib/Command/ACL.php b/lib/Command/ACL.php index e6189283e..f7ced3219 100644 --- a/lib/Command/ACL.php +++ b/lib/Command/ACL.php @@ -20,6 +20,7 @@ use OCP\Constants; use OCP\Files\IRootFolder; use OCP\IUserManager; +use RuntimeException; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; @@ -70,6 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->folderManager->setFolderACL($folder->id, false); } elseif ($input->getOption('test')) { if ($input->getOption('user') && ($input->getArgument('path'))) { + /** @var string $mappingId */ $mappingId = $input->getOption('user'); $user = $this->userManager->get($mappingId); if (!$user) { @@ -77,6 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return -1; } + /** @var string $path */ $path = $input->getArgument('path'); $aclManager = $this->aclManagerFactory->getACLManager($user); if ($this->folderManager->getFolderPermissionsForUser($user, $folder->id) === 0) { @@ -134,12 +137,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln(' argument has to be an array'); return -3; } + /** @var list $permissionStrings */ $mount = $this->mountProvider->getMount( FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL), '/dummy/files/' . $folder->mountPoint, ); - $id = $mount->getStorage()->getCache()->getId($path); + + $storage = $mount->getStorage(); + if ($storage === null) { + throw new RuntimeException('Failed to get storage for mount.'); + } + + $id = $storage->getCache()->getId($path); if ($id === -1) { $output->writeln('Path not found in folder: ' . $path . ''); return -1; @@ -157,11 +167,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } foreach ($permissionStrings as $permission) { - if (!is_string($permission)) { - $output->writeln(' argument has to be an array of strings'); - return -3; - } - if ($permission[0] !== '+' && $permission[0] !== '-') { $output->writeln('incorrect format for permissions "' . $permission . '"'); return -3; @@ -189,8 +194,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function printPermissions(InputInterface $input, OutputInterface $output, FolderWithMappingsAndCache $folder): void { $rootPath = $folder->rootCacheEntry->getPath(); + $numericStorageId = $this->rootFolder->getMountPoint()->getNumericStorageId(); + if ($numericStorageId === null) { + throw new RuntimeException('Failed to get numeric storage id for mount.'); + } $rules = $this->ruleManager->getAllRulesForPrefix( - $this->rootFolder->getMountPoint()->getNumericStorageId(), + $numericStorageId, $rootPath ); $jailPathLength = strlen($rootPath) + 1; @@ -206,7 +215,7 @@ private function printPermissions(InputInterface $input, OutputInterface $output $items = array_combine($paths, $rules); ksort($items); - $output->writeln(json_encode($items, $outputFormat === parent::OUTPUT_FORMAT_JSON_PRETTY ? JSON_PRETTY_PRINT : 0)); + $output->writeln(json_encode($items, JSON_THROW_ON_ERROR | ($outputFormat === parent::OUTPUT_FORMAT_JSON_PRETTY ? JSON_PRETTY_PRINT : 0))); break; default: $items = array_map(function (array $rulesForPath, string $path) use ($jailPathLength): array { @@ -236,15 +245,26 @@ private function printPermissions(InputInterface $input, OutputInterface $output } } + /** + * @return array{'user'|'group'|'circle', string} + */ private function convertMappingOptions(InputInterface $input): array { - if ($input->getOption('user')) { - return ['user', $input->getOption('user')]; + /** @var ?string $user */ + $user = $input->getOption('user'); + if ($user !== null) { + return ['user', $user]; } - if ($input->getOption('group')) { - return ['group', $input->getOption('group')]; + + /** @var ?string $group */ + $group = $input->getOption('group'); + if ($group !== null) { + return ['group', $group]; } - if ($input->getOption('team')) { - return ['circle', $input->getOption('team')]; + + /** @var ?string $team */ + $team = $input->getOption('team'); + if ($team !== null) { + return ['circle', $team]; } throw new InvalidArgumentException('invalid mapping options'); @@ -252,6 +272,7 @@ private function convertMappingOptions(InputInterface $input): array { /** * @param list $permissions + * @return array{int, int} */ private function parsePermissions(array $permissions): array { $mask = 0; diff --git a/lib/Command/Create.php b/lib/Command/Create.php index 9ac57105d..a8c56c029 100644 --- a/lib/Command/Create.php +++ b/lib/Command/Create.php @@ -35,7 +35,9 @@ protected function configure(): void { #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { - $name = $this->folderManager->trimMountpoint((string)$input->getArgument('name')); + /** @var string $name */ + $name = $input->getArgument('name'); + $name = $this->folderManager->trimMountpoint($name); // Check if the folder name is valid if (empty($name)) { diff --git a/lib/Command/Delete.php b/lib/Command/Delete.php index db68c3154..e01f03e5b 100644 --- a/lib/Command/Delete.php +++ b/lib/Command/Delete.php @@ -8,6 +8,7 @@ namespace OCA\GroupFolders\Command; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -30,6 +31,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return -1; } + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Are you sure you want to delete the Team folder ' . $folder->mountPoint . ' and all files within, this cannot be undone (y/N).', false); if ($input->getOption('force') || $helper->ask($input, $output, $question)) { diff --git a/lib/Command/FolderCommand.php b/lib/Command/FolderCommand.php index b2adff85d..f3058063f 100644 --- a/lib/Command/FolderCommand.php +++ b/lib/Command/FolderCommand.php @@ -32,10 +32,12 @@ public function __construct( } protected function getFolder(InputInterface $input, OutputInterface $output): ?FolderWithMappingsAndCache { - $folderId = (int)$input->getArgument('folder_id'); - if ((string)$folderId !== $input->getArgument('folder_id')) { + /** @var string $folderIdString */ + $folderIdString = $input->getArgument('folder_id'); + $folderId = (int)$folderIdString; + if ((string)$folderId !== $folderIdString) { // Protect against removing folderId === 0 when typing a string (e.g. folder name instead of folder id) - $output->writeln('Folder id argument is not an integer. Got ' . $input->getArgument('folder_id') . ''); + $output->writeln('Folder id argument is not an integer. Got ' . $folderIdString . ''); return null; } diff --git a/lib/Command/Group.php b/lib/Command/Group.php index ab40c944f..8eb3ff847 100644 --- a/lib/Command/Group.php +++ b/lib/Command/Group.php @@ -55,15 +55,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int return -1; } + /** @var string $groupString */ $groupString = $input->getArgument('group'); $group = $this->groupManager->get($groupString); if ($input->getOption('delete')) { $this->folderManager->removeApplicableGroup($folder->id, $groupString); return 0; } elseif ($group || $this->folderManager->isACircle($groupString)) { - $permissionsString = $input->getArgument('permissions'); + /** @var list $permissionsString */ + $permissionsString = (array)$input->getArgument('permissions'); $permissions = $this->getNewPermissions($permissionsString); - if ($permissions) { + if ($permissions > 0) { if (!isset($folder->groups[$groupString])) { $this->folderManager->addApplicableGroup($folder->id, $groupString); } @@ -83,6 +85,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int return -1; } + /** + * @param list $input + */ private function getNewPermissions(array $input): int { $permissions = 1; $values = self::PERMISSION_VALUES; diff --git a/lib/Command/ListCommand.php b/lib/Command/ListCommand.php index 1dd64f7e1..5102ff916 100644 --- a/lib/Command/ListCommand.php +++ b/lib/Command/ListCommand.php @@ -49,6 +49,7 @@ protected function configure(): void { #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { + /** @var ?string $userId */ $userId = $input->getOption('user'); $groups = $this->groupManager->search(''); $groupNames = []; @@ -128,6 +129,6 @@ private function permissionsToString(int $permissions): string { return 'none'; } - return implode(', ', array_filter(self::PERMISSION_NAMES, fn (int $possiblePermission): int => $possiblePermission & $permissions, ARRAY_FILTER_USE_KEY)); + return implode(', ', array_filter(self::PERMISSION_NAMES, fn (int $possiblePermission): bool => ($possiblePermission & $permissions) === $permissions, ARRAY_FILTER_USE_KEY)); } } diff --git a/lib/Command/Rename.php b/lib/Command/Rename.php index 00efabab1..0ec5d97a7 100644 --- a/lib/Command/Rename.php +++ b/lib/Command/Rename.php @@ -28,8 +28,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return -1; } + /** @var string $name */ + $name = $input->getArgument('name'); + // Check if the new name is valid - $name = $this->folderManager->trimMountpoint((string)$input->getArgument('name')); + $name = $this->folderManager->trimMountpoint($name); if (empty($name)) { $output->writeln('Folder name cannot be empty'); return 1; @@ -46,7 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $this->folderManager->renameFolder($folder->id, $input->getArgument('name')); + $this->folderManager->renameFolder($folder->id, $name); return 0; } diff --git a/lib/Command/Scan.php b/lib/Command/Scan.php index b7d4b329e..5be722281 100644 --- a/lib/Command/Scan.php +++ b/lib/Command/Scan.php @@ -8,15 +8,16 @@ namespace OCA\GroupFolders\Command; +use OC\Files\Cache\Scanner; use OC\Files\ObjectStore\ObjectStoreScanner; use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupFolders\Mount\FolderStorageManager; use OCA\GroupFolders\Mount\MountProvider; use OCP\Constants; -use OCP\Files\Cache\IScanner; use OCP\Files\IRootFolder; use OCP\Files\Storage\IStorageFactory; +use RuntimeException; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -117,8 +118,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } foreach ($mounts as $type => $mount) { $statsRow = ["$folderId - $type", 0, 0, 0, 0]; - /** @var IScanner&\OC\Hooks\BasicEmitter $scanner */ - $scanner = $mount->getStorage()->getScanner(); + $storage = $mount->getStorage(); + if ($storage === null) { + throw new RuntimeException('Failed to get storage for mount.'); + } + /** @var Scanner&\OC\Hooks\BasicEmitter $scanner */ + $scanner = $storage->getScanner(); $output->writeln("Scanning Team folder with id\t$folderId - $type", OutputInterface::VERBOSITY_VERBOSE); if ($scanner instanceof ObjectStoreScanner) { @@ -129,22 +134,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function (string $path) use ($output, &$statsRow): void { $output->writeln("\tFile\t/$path", OutputInterface::VERBOSITY_VERBOSE); $statsRow[2]++; - // abortIfInterrupted doesn't exist in nc14 - if (method_exists($this, 'abortIfInterrupted')) { - $this->abortIfInterrupted(); - } + $this->abortIfInterrupted(); }); $scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function (string $path) use ($output, &$statsRow): void { $output->writeln("\tFolder\t/$path", OutputInterface::VERBOSITY_VERBOSE); $statsRow[1]++; - // abortIfInterrupted doesn't exist in nc14 - if (method_exists($this, 'abortIfInterrupted')) { - $this->abortIfInterrupted(); - } + $this->abortIfInterrupted(); }); - $scanner->listen('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output, &$statsRow): void { + $scanner->listen('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', function (string $fullPath) use ($output, &$statsRow): void { $output->writeln("\tEntry \"" . $fullPath . '" will not be accessible due to incompatible encoding'); $statsRow[3]++; }); @@ -170,6 +169,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + /** + * @param list $headers + * @param list $rows + */ protected function showSummary(array $headers, array $rows, OutputInterface $output): void { $table = new Table($output); $table diff --git a/lib/Command/Trashbin/Cleanup.php b/lib/Command/Trashbin/Cleanup.php index 6d7625312..db3abd446 100644 --- a/lib/Command/Trashbin/Cleanup.php +++ b/lib/Command/Trashbin/Cleanup.php @@ -57,8 +57,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $folders = $this->folderManager->getAllFoldersWithSize(); $folders = array_map(fn (FolderWithMappingsAndCache $folder): FolderDefinitionWithPermissions => FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL), $folders); - if ($input->getArgument('folder_id') !== null) { - $folderId = (int)$input->getArgument('folder_id'); + /** @var ?string $folderId */ + $folderId = $input->getArgument('folder_id'); + if ($folderId !== null) { + $folderId = (int)$folderId; foreach ($folders as $folder) { if ($folder->id === $folderId) { diff --git a/lib/Controller/DelegationController.php b/lib/Controller/DelegationController.php index 3f072ef43..8279f3774 100644 --- a/lib/Controller/DelegationController.php +++ b/lib/Controller/DelegationController.php @@ -22,14 +22,15 @@ use OCP\IConfig; use OCP\IGroupManager; use OCP\IRequest; +use OCP\IUser; use OCP\IUserSession; use OCP\Server; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; /** - * @psalm-import-type GroupFoldersDelegationGroup from ResponseDefinitions - * @psalm-import-type GroupFoldersDelegationCircle from ResponseDefinitions + * @phpstan-import-type GroupFoldersDelegationGroup from ResponseDefinitions + * @phpstan-import-type GroupFoldersDelegationCircle from ResponseDefinitions */ class DelegationController extends OCSController { public function __construct( @@ -96,7 +97,9 @@ public function getAllCircles(): DataResponse { // As admin, get all circles, // As non-admin, only returns circles current user is members of. - if ($this->groupManager->isAdmin($this->userSession->getUser()->getUID())) { + /** @var IUser $user Not a public route, so a user must be present. */ + $user = $this->userSession->getUser(); + if ($this->groupManager->isAdmin($user->getUID())) { $circlesManager->startSuperSession(); } else { $circlesManager->startSession(); diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index 151c68606..67d8a6760 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -33,10 +33,27 @@ use OCP\IUserSession; /** - * @psalm-import-type GroupFoldersGroup from ResponseDefinitions - * @psalm-import-type GroupFoldersCircle from ResponseDefinitions - * @psalm-import-type GroupFoldersUser from ResponseDefinitions - * @psalm-import-type GroupFoldersFolder from ResponseDefinitions + * @phpstan-import-type GroupFoldersGroup from ResponseDefinitions + * @phpstan-import-type GroupFoldersCircle from ResponseDefinitions + * @phpstan-import-type GroupFoldersUser from ResponseDefinitions + * @phpstan-import-type GroupFoldersFolder from ResponseDefinitions + * @phpstan-import-type GroupFoldersAclManage from ResponseDefinitions + * @phpstan-type GroupFoldersFolderXML = array{ + * id: int, + * mount_point: string, + * groups: list, + * quota: int, + * size: int, + * acl: bool, + * acl_default_no_permission: bool, + * manage: list, + * sortIndex?: non-negative-int, + * } */ class FolderController extends OCSController { private readonly ?IUser $user; @@ -113,6 +130,9 @@ private function formatFolder(FolderWithMappingsAndCache $folder): array { #[NoAdminRequired] #[FrontpageRoute(verb: 'GET', url: '/folders')] public function getFolders(bool $applicable = false, int $offset = 0, ?int $limit = null, string $orderBy = 'mount_point', string $order = 'asc'): DataResponse { + /** + * @phpstan-ignore smallerOrEqual.alwaysFalse, booleanAnd.alwaysFalse + */ if ($limit !== null && $limit <= 0) { throw new OCSBadRequestException('The limit must be greater than 0.'); } @@ -132,6 +152,7 @@ public function getFolders(bool $applicable = false, int $offset = 0, ?int $limi $folders = []; $i = 0; + /** @var string $id */ foreach ($this->manager->getAllFoldersWithSize($offset, $limit, $orderBy, $order) as $id => $folder) { // Make them string-indexed for OpenAPI JSON output // JavaScript doesn't preserve JSON object key orders, so we need to manually add this information. @@ -198,7 +219,7 @@ private function checkedGetFolder(int $id): FolderWithMappingsAndCache { return $folder; } - private function checkMountPointExists(string $mountpoint): ?DataResponse { + private function checkMountPointExists(string $mountpoint): void { $storageId = $this->getRootFolderStorageId(); if ($storageId === null) { throw new OCSNotFoundException('Groupfolder not found'); @@ -207,8 +228,6 @@ private function checkMountPointExists(string $mountpoint): ?DataResponse { if ($this->manager->mountPointExists($mountpoint)) { throw new OCSBadRequestException('Mount point already exists'); } - - return null; } private function getRootFolderStorageId(): ?int { @@ -484,28 +503,42 @@ public function renameFolder(int $id, string $mountpoint): DataResponse { /** * Overwrite response builder to customize xml handling to deal with spaces in folder names + * @param DataResponse, array{}> $data + * @return V1Response> */ private function buildOCSResponseXML(string $format, DataResponse $data): V1Response { - /** @var array $folderData */ $folderData = $data->getData(); if (isset($folderData['id'])) { // single folder response - $folderData = $this->folderDataForXML($folderData); + /** @var GroupFoldersFolder $folderDataIn */ + $folderDataIn = $folderData; + $folderData = $this->folderDataForXML($folderDataIn); + $data->setData($folderData); } elseif (isset($folderData['folder'])) { // single folder response - $folderData['folder'] = $this->folderDataForXML(['folder']); + /** @var array{folder: GroupFoldersFolder} $folderDataIn */ + $folderDataIn = $folderData; + $folderData['folder'] = $this->folderDataForXML($folderDataIn['folder']); + /** @var DataResponse $data */ + $data->setData($folderData); } elseif (count($folderData) && isset(current($folderData)['id'])) { // folder list - $folderData = array_map($this->folderDataForXML(...), $folderData); + /** @var list $folderDataIn */ + $folderDataIn = $folderData; + $folderData = array_map($this->folderDataForXML(...), $folderDataIn); + /** @var DataResponse, array{}> $data */ + $data->setData($folderData); } - $data->setData($folderData); - return new V1Response($data, $format); } + /** + * @param GroupFoldersFolder $data + * @return GroupFoldersFolderXML + */ private function folderDataForXML(array $data): array { - $groups = $data['group_details'] ?? []; + $groups = $data['group_details']; unset($data['group_details']); $data['groups'] = []; foreach ($groups as $id => $group) { @@ -551,21 +584,4 @@ public function aclMappingSearch(int $id, string $search = ''): DataResponse { 'circles' => $circles ]); } - - private function compareFolderNames(string $a, string $b): int { - if (($value = strnatcmp($a, $b)) === 0) { - return $value; - } - - // Folder names starting with '_' get pushed to the end while they are brought to the - // beginning in the frontend. Do the same here to keep it consistent with the frontend - if (str_starts_with($a, '_') && !str_starts_with($b, '_')) { - return -1; - } - if (!str_starts_with($a, '_') && str_starts_with($b, '_')) { - return 1; - } - - return $value; - } } diff --git a/lib/DAV/ACLPlugin.php b/lib/DAV/ACLPlugin.php index 652201421..872fb6cdf 100644 --- a/lib/DAV/ACLPlugin.php +++ b/lib/DAV/ACLPlugin.php @@ -22,6 +22,7 @@ use OCP\IUser; use OCP\IUserSession; use OCP\Log\Audit\CriticalActionPerformedEvent; +use RuntimeException; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\INode; use Sabre\DAV\PropFind; @@ -164,6 +165,11 @@ public function propFind(PropFind $propFind, INode $node): void { } } + $fileInfoId = $fileInfo->getId(); + if ($fileInfoId === null) { + throw new RuntimeException('Failed to get id of fileinfo.'); + } + return array_map(fn (IUserMapping $mapping, int $permissions, int $mask): Rule => new Rule( $mapping, $fileInfoId, @@ -222,6 +228,10 @@ public function propPatch(string $path, PropPatch $propPatch): void { // Mapping the old property to the new property. $propPatch->handle(self::ACL_LIST, function (array $rawRules) use ($path): bool { + if ($this->server === null) { + return false; + } + $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof Node) { return false; @@ -245,7 +255,13 @@ public function propPatch(string $path, PropPatch $propPatch): void { $path = trim($mount->getSourcePath() . '/' . $fileInfo->getInternalPath(), '/'); + $fileInfoId = $fileInfo->getId(); + if ($fileInfoId === null) { + throw new RuntimeException('Failed to get id of fileinfo.'); + } + // populate fileid in rules + /** @var Rule[] $rawRules */ $rules = array_values(array_map(fn (Rule $rule): Rule => new Rule( $rule->getUserMapping(), $fileInfoId, @@ -274,6 +290,7 @@ public function propPatch(string $path, PropPatch $propPatch): void { throw new BadRequest($this->l10n->t('You cannot remove your own read permission.')); } + /** @var list $existingRules */ $existingRules = array_reduce( $this->ruleManager->getAllRulesForPaths($mount->getNumericStorageId(), [$path]), array_merge(...), diff --git a/lib/DAV/GroupFoldersHome.php b/lib/DAV/GroupFoldersHome.php index 63b80f1f0..ac699f627 100644 --- a/lib/DAV/GroupFoldersHome.php +++ b/lib/DAV/GroupFoldersHome.php @@ -19,6 +19,9 @@ use Sabre\DAV\ICollection; class GroupFoldersHome implements ICollection { + /** + * @param array{uri: string} $principalInfo + */ public function __construct( private array $principalInfo, private readonly FolderManager $folderManager, @@ -34,6 +37,7 @@ public function delete(): never { #[\Override] public function getName(): string { + /** @var string $name */ [, $name] = \Sabre\Uri\split($this->principalInfo['uri']); return $name; } diff --git a/lib/DAV/RootCollection.php b/lib/DAV/RootCollection.php index 6b814c5d9..42e81321a 100644 --- a/lib/DAV/RootCollection.php +++ b/lib/DAV/RootCollection.php @@ -30,6 +30,7 @@ public function __construct( * The passed array contains principal information, and is guaranteed to * at least contain a uri item. Other properties may or may not be * supplied by the authentication backend. + * @param array{uri: string} $principalInfo */ #[\Override] public function getChildForPrincipal(array $principalInfo): GroupFoldersHome { diff --git a/lib/Folder/FolderDefinition.php b/lib/Folder/FolderDefinition.php index 61a27aaaf..6a821e40e 100644 --- a/lib/Folder/FolderDefinition.php +++ b/lib/Folder/FolderDefinition.php @@ -9,6 +9,9 @@ namespace OCA\GroupFolders\Folder; class FolderDefinition { + /** + * @param array{separate-storage?: bool} $options + */ public function __construct( public readonly int $id, public readonly string $mountPoint, diff --git a/lib/Folder/FolderDefinitionWithMappings.php b/lib/Folder/FolderDefinitionWithMappings.php index 1322a8d44..eb08b6a45 100644 --- a/lib/Folder/FolderDefinitionWithMappings.php +++ b/lib/Folder/FolderDefinitionWithMappings.php @@ -11,13 +11,13 @@ use OCA\GroupFolders\ResponseDefinitions; /** - * @psalm-import-type GroupFoldersApplicable from ResponseDefinitions - * @psalm-import-type GroupFoldersAclManage from ResponseDefinitions + * @phpstan-import-type GroupFoldersApplicable from ResponseDefinitions + * @phpstan-import-type GroupFoldersAclManage from ResponseDefinitions */ class FolderDefinitionWithMappings extends FolderDefinition { /** - * @psalm-param array $groups - * @psalm-param list $manage + * @param array $groups + * @param list $manage */ public function __construct( int $id, @@ -35,8 +35,8 @@ public function __construct( } /** - * @psalm-param array $groups - * @psalm-param list $manage + * @param array $groups + * @param list $manage */ public static function fromFolder(FolderDefinition $folder, array $groups, array $manage): FolderDefinitionWithMappings { return new FolderDefinitionWithMappings( @@ -53,12 +53,26 @@ public static function fromFolder(FolderDefinition $folder, array $groups, array ); } + /** + * @return array{ + * id: int, + * mount_point: string, + * quota: int, + * acl: bool, + * acl_default_no_permission: bool, + * storage_id: int, + * root_id: int, + * groups: array, + * manage: list, + * } + */ public function toArray(): array { return [ 'id' => $this->id, 'mount_point' => $this->mountPoint, 'quota' => $this->quota, 'acl' => $this->acl, + 'acl_default_no_permission' => $this->aclDefaultNoPermission, 'storage_id' => $this->storageId, 'root_id' => $this->rootId, 'groups' => $this->groups, diff --git a/lib/Folder/FolderDefinitionWithPermissions.php b/lib/Folder/FolderDefinitionWithPermissions.php index ca69832f7..6327d6311 100644 --- a/lib/Folder/FolderDefinitionWithPermissions.php +++ b/lib/Folder/FolderDefinitionWithPermissions.php @@ -12,8 +12,8 @@ use OCP\Files\Cache\ICacheEntry; /** - * @psalm-import-type GroupFoldersApplicable from ResponseDefinitions - * @psalm-import-type GroupFoldersAclManage from ResponseDefinitions + * @phpstan-import-type GroupFoldersApplicable from ResponseDefinitions + * @phpstan-import-type GroupFoldersAclManage from ResponseDefinitions */ class FolderDefinitionWithPermissions extends FolderDefinition { public function __construct( @@ -31,10 +31,6 @@ public function __construct( parent::__construct($id, $mountPoint, $quota, $acl, $aclDefaultNoPermission, $storageId, $rootId, $options); } - /** - * @psalm-param array $groups - * @psalm-param list $manage - */ public static function fromFolder(FolderDefinition $folder, ICacheEntry $rootCacheEntry, int $permissions): FolderDefinitionWithPermissions { return new FolderDefinitionWithPermissions( $folder->id, @@ -50,6 +46,19 @@ public static function fromFolder(FolderDefinition $folder, ICacheEntry $rootCac ); } + /** + * @return array{ + * id: int, + * mount_point: string, + * permissions: int, + * quota: int, + * acl: bool, + * acl_default_no_permission: bool, + * storage_id: int, + * root_id: int, + * root_cache_entry: ICacheEntry, + * } + */ public function toArray(): array { return [ 'id' => $this->id, diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index cd3d82444..147470eab 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -45,12 +45,12 @@ use Psr\Log\LoggerInterface; /** - * @psalm-import-type GroupFoldersGroup from ResponseDefinitions - * @psalm-import-type GroupFoldersCircle from ResponseDefinitions - * @psalm-import-type GroupFoldersUser from ResponseDefinitions - * @psalm-import-type GroupFoldersAclManage from ResponseDefinitions - * @psalm-import-type GroupFoldersApplicable from ResponseDefinitions - * @psalm-type InternalFolderMapping = array{ + * @phpstan-import-type GroupFoldersGroup from ResponseDefinitions + * @phpstan-import-type GroupFoldersCircle from ResponseDefinitions + * @phpstan-import-type GroupFoldersUser from ResponseDefinitions + * @phpstan-import-type GroupFoldersAclManage from ResponseDefinitions + * @phpstan-import-type GroupFoldersApplicable from ResponseDefinitions + * @phpstan-type InternalFolderMapping = array{ * folder_id: int, * mapping_type: 'user'|'group'|'circle', * mapping_id: string, @@ -85,6 +85,7 @@ public function getAllFolders(): array { $query->select('folder_id', 'mount_point', 'quota', 'acl', 'acl_default_no_permission', 'storage_id', 'root_id', 'options') ->from('group_folders', 'f'); + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); $folderMap = []; @@ -155,6 +156,7 @@ public function getAllFoldersWithSize(int $offset = 0, ?int $limit = null, strin $query->addOrderBy('mount_point', 'ASC'); } + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); $folderIds = array_map(static fn (array $row): int => (int)$row['folder_id'], $rows); @@ -195,6 +197,7 @@ public function getAllFoldersForUserWithSize(IUser $user): array { ->selectAlias('a.permissions', 'group_permissions') ->where($query->expr()->in('a.group_id', $query->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))); + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); $folderIds = array_map(static fn (array $row): int => (int)$row['folder_id'], $rows); @@ -219,6 +222,7 @@ public function getAllFoldersForUserWithSize(IUser $user): array { } /** + * @param list $folderIds * @return array> * @throws Exception */ @@ -305,8 +309,6 @@ private function getManageAcl(array $mappings): array { 'displayname' => $circle->getDisplayName(), ]; } - - return null; }, $mappings))); } @@ -316,6 +318,7 @@ public function getFolder(int $id): ?FolderWithMappingsAndCache { $query->where($query->expr()->eq('f.folder_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); $result = $query->executeQuery(); + /** @var array{folder_id: int, mount_point: string, quota: int, acl: bool, acl_default_no_permission: bool, storage_id: int, root_id: int, options: string}|false $row */ $row = $result->fetch(); $result->closeCursor(); if ($row === false) { @@ -326,7 +329,6 @@ public function getFolder(int $id): ?FolderWithMappingsAndCache { $folderMappings = $this->getFolderMappings($id); $folder = $this->rowToFolder($row); - $id = $folder->id; return FolderWithMappingsAndCache::fromFolderWithMapping( FolderDefinitionWithMappings::fromFolder( $folder, @@ -348,10 +350,11 @@ public function getFolderAclEnabled(int $id): bool { ->from('group_folders', 'f') ->where($query->expr()->eq('folder_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); $result = $query->executeQuery(); + /** @var array{acl: int} $row */ $row = $result->fetch(); $result->closeCursor(); - return (bool)($row['acl'] ?? false); + return $row['acl'] === 1; } public function getFolderByPath(string $path): int { @@ -364,6 +367,7 @@ public function getFolderByPath(string $path): int { } /** + * @param list $folderIds * @return array> * @throws Exception */ @@ -383,6 +387,7 @@ private function getAllApplicable(?array $folderIds = null): array { $queryHelper?->addCircleDetails('g', 'circle_id'); + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); $applicableMap = []; @@ -411,7 +416,7 @@ private function getAllApplicable(?array $folderIds = null): array { } $entry = [ - 'displayName' => $circle?->getDisplayName() ?? $row['circle_id'], + 'displayName' => $circle?->getDisplayName() ?? $entityId, 'permissions' => (int)$row['permissions'], 'type' => 'circle', ]; @@ -502,10 +507,11 @@ public function mountPointExists(string $mountPoint): bool { ->setMaxResults(1); $result = $query->executeQuery(); - $exists = (int)$result->fetchOne() > 0; + /** @var int $count */ + $count = $result->fetchOne(); $result->closeCursor(); - return $exists; + return $count > 0; } /** @@ -519,6 +525,7 @@ private function getManagerMappings(int $folderId): array { ->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))); $managerMappings = []; + /** @var list $rows */ $rows = $query->executeQuery()->fetchAll(); foreach ($rows as $manageRule) { $managerMappings[] = new UserMapping($manageRule['mapping_type'], $manageRule['mapping_id']); @@ -599,6 +606,10 @@ public function searchUsers(int $id, string $search = '', int $limit = 10, int $ return array_values($users); } + /** + * @param array{folder_id?: int, options?: string} $row + * @return array{separate-storage?: bool} + */ private function getFolderOptions(array $row): array { if (!isset($row['options'])) { return []; @@ -615,9 +626,13 @@ private function getFolderOptions(array $row): array { return []; } + /** @var array{separate-storage?: bool} $options */ return $options; } + /** + * @param array{folder_id: int, mount_point: string, quota: int, acl: bool, acl_default_no_permission: bool, storage_id: int, root_id: int, options?: string} $row + */ private function rowToFolder(array $row): FolderDefinition { return new FolderDefinition( (int)$row['folder_id'], @@ -633,6 +648,7 @@ private function rowToFolder(array $row): FolderDefinition { /** * @param string[] $groupIds + * @param list $paths * @return list * @throws Exception */ @@ -666,6 +682,7 @@ public function getFoldersForGroups(array $groupIds, ?int $folderId = null, ?arr $query->setParameter('groupIds', $chunk, IQueryBuilder::PARAM_STR_ARRAY); if ($paths === null) { + /** @var list $result */ $result = array_merge($result, $query->executeQuery()->fetchAll()); continue; } @@ -673,6 +690,7 @@ public function getFoldersForGroups(array $groupIds, ?int $folderId = null, ?arr // When paths are set, we need to chunk these as well foreach (array_chunk($paths, 1000) as $pathChunk) { $query->setParameter('path', $pathChunk, IQueryBuilder::PARAM_STR_ARRAY); + /** @var list $result */ $result = array_merge($result, $query->executeQuery()->fetchAll()); } } @@ -688,6 +706,7 @@ public function getFoldersForGroups(array $groupIds, ?int $folderId = null, ?arr } /** + * @param list $paths * @return list * @throws Exception */ @@ -719,11 +738,7 @@ public function getFoldersFromCircleMemberships(IUser $user, ?int $folderId = nu $query->andWhere($query->expr()->eq('f.folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))); } - if (method_exists($queryHelper, 'limitToMemberships')) { - $queryHelper->limitToMemberships('a', 'circle_id', $federatedUser); - } else { - $queryHelper->limitToInheritedMembers('a', 'circle_id', $federatedUser); - } + $queryHelper->limitToMemberships('a', 'circle_id', $federatedUser); // add chunking because Oracle can't deal with more than 1000 values in an expression list for in queries. $result = []; @@ -734,9 +749,11 @@ public function getFoldersFromCircleMemberships(IUser $user, ?int $folderId = nu // When paths are set, we need to chunk these as well foreach (array_chunk($paths, 1000) as $pathChunk) { $query->setParameter('path', $pathChunk, IQueryBuilder::PARAM_STR_ARRAY); + /** @var list $result */ $result = array_merge($result, $query->executeQuery()->fetchAll()); } } else { + /** @var list $result */ $result = $query->executeQuery()->fetchAll(); } @@ -777,6 +794,7 @@ public function trimMountpoint(string $mountpoint): string { } /** + * @param array{separate-storage?: bool} $options * @throws Exception */ public function createFolder(string $mountPoint, array $options = [], bool $aclDefaultNoPermission = false): int { @@ -1016,6 +1034,7 @@ public function setFolderACL(int $folderId, bool $acl): void { } /** + * @param list $paths * @return list * @throws Exception */ diff --git a/lib/Folder/FolderWithMappingsAndCache.php b/lib/Folder/FolderWithMappingsAndCache.php index 025f6206f..b78dbfc4b 100644 --- a/lib/Folder/FolderWithMappingsAndCache.php +++ b/lib/Folder/FolderWithMappingsAndCache.php @@ -12,13 +12,13 @@ use OCP\Files\Cache\ICacheEntry; /** - * @psalm-import-type GroupFoldersApplicable from ResponseDefinitions - * @psalm-import-type GroupFoldersAclManage from ResponseDefinitions + * @phpstan-import-type GroupFoldersApplicable from ResponseDefinitions + * @phpstan-import-type GroupFoldersAclManage from ResponseDefinitions */ class FolderWithMappingsAndCache extends FolderDefinitionWithMappings { /** - * @psalm-param array $groups - * @psalm-param list $manage + * @param array $groups + * @param list $manage */ public function __construct( int $id, @@ -36,10 +36,6 @@ public function __construct( parent::__construct($id, $mountPoint, $quota, $acl, $aclDefaultNoPermission, $storageId, $rootId, $options, $groups, $manage); } - /** - * @psalm-param array $groups - * @psalm-param list $manage - */ public static function fromFolderWithMapping(FolderDefinitionWithMappings $folder, ICacheEntry $rootCacheEntry): FolderWithMappingsAndCache { return new FolderWithMappingsAndCache( $folder->id, @@ -56,6 +52,21 @@ public static function fromFolderWithMapping(FolderDefinitionWithMappings $folde ); } + /** + * @return array{ + * id: int, + * mount_point: string, + * quota: int, + * acl: bool, + * acl_default_no_permission: bool, + * storage_id: int, + * root_id: int, + * root_cache_entry: ICacheEntry, + * groups: array, + * manage: list, + * options: array{separate-storage?: bool}, + * } + */ #[\Override] public function toArray(): array { return [ diff --git a/lib/CacheListener.php b/lib/Listeners/CacheListener.php similarity index 51% rename from lib/CacheListener.php rename to lib/Listeners/CacheListener.php index 967126851..9cb18c542 100644 --- a/lib/CacheListener.php +++ b/lib/Listeners/CacheListener.php @@ -1,40 +1,42 @@ eventDispatcher->addListener(CacheEntryInsertedEvent::class, $this->onCacheEvent(...), 99999); - $this->eventDispatcher->addListener(CacheEntryUpdatedEvent::class, $this->onCacheEvent(...), 99999); - } +/** + * @template-implements IEventListener + */ +class CacheListener implements IEventListener { + #[\Override] + public function handle(Event $event): void { + /** @phpstan-ignore instanceof.alwaysTrue, booleanAnd.alwaysFalse */ + if (!$event instanceof CacheEntryInsertedEvent && !$event instanceof CacheEntryUpdatedEvent) { + return; + } - public function onCacheEvent(ICacheEvent $event): void { $storage = $event->getStorage(); if (!$storage->instanceOfStorage(GroupFolderStorage::class)) { return; } + /** @phpstan-ignore method.impossibleType */ if (!$storage->instanceOfStorage(Jail::class)) { return; } - /** @var Jail $storage */ + /** @var Jail $storage @phpstan-ignore varTag.nativeType */ $path = $storage->getJailedPath($event->getPath()); if ($path !== null) { $event->setPath($path); diff --git a/lib/Listeners/CircleDestroyedEventListener.php b/lib/Listeners/CircleDestroyedEventListener.php index 88c9236bd..fd19c9ba4 100644 --- a/lib/Listeners/CircleDestroyedEventListener.php +++ b/lib/Listeners/CircleDestroyedEventListener.php @@ -26,6 +26,7 @@ public function __construct( #[\Override] public function handle(Event $event): void { + /** @phpstan-ignore instanceof.alwaysTrue */ if (!$event instanceof CircleDestroyedEvent) { return; } diff --git a/lib/Listeners/LoadAdditionalScriptsListener.php b/lib/Listeners/LoadAdditionalScriptsListener.php index 29bca0b81..c7ebbf8d1 100644 --- a/lib/Listeners/LoadAdditionalScriptsListener.php +++ b/lib/Listeners/LoadAdditionalScriptsListener.php @@ -22,6 +22,7 @@ class LoadAdditionalScriptsListener implements IEventListener { #[\Override] public function handle(Event $event): void { + /** @phpstan-ignore instanceof.alwaysTrue, booleanAnd.alwaysFalse */ if (!$event instanceof LoadAdditionalScriptsEvent && !$event instanceof BeforeTemplateRenderedEvent) { return; } diff --git a/lib/Listeners/NodeRenamedListener.php b/lib/Listeners/NodeRenamedListener.php index 7727c50f8..b97a27aca 100644 --- a/lib/Listeners/NodeRenamedListener.php +++ b/lib/Listeners/NodeRenamedListener.php @@ -27,6 +27,7 @@ public function __construct( #[\Override] public function handle(Event $event): void { + /** @phpstan-ignore instanceof.alwaysTrue */ if (!$event instanceof NodeRenamedEvent) { return; } diff --git a/lib/Migration/Version103000Date20180806161724.php b/lib/Migration/Version103000Date20180806161724.php index 7221ff465..54012c770 100644 --- a/lib/Migration/Version103000Date20180806161724.php +++ b/lib/Migration/Version103000Date20180806161724.php @@ -14,6 +14,9 @@ use OCP\Migration\SimpleMigrationStep; class Version103000Date20180806161724 extends SimpleMigrationStep { + /** + * @var list + */ private array $applicableData = []; public function __construct( @@ -32,7 +35,9 @@ public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $query->select(['folder_id', 'permissions', 'group_id']) ->from('group_folders_applicable'); $result = $query->executeQuery(); - $this->applicableData = $result->fetchAll(\PDO::FETCH_ASSOC); + /** @var list $data */ + $data = $result->fetchAll(\PDO::FETCH_ASSOC); + $this->applicableData = $data; } } diff --git a/lib/Migration/Version19000Date20241029123147.php b/lib/Migration/Version19000Date20241029123147.php index ca7e49546..ae8fdb10c 100644 --- a/lib/Migration/Version19000Date20241029123147.php +++ b/lib/Migration/Version19000Date20241029123147.php @@ -15,9 +15,6 @@ use OCP\Migration\SimpleMigrationStep; class Version19000Date20241029123147 extends SimpleMigrationStep { - /** - * @param Closure(): ISchemaWrapper $schemaClosure - */ #[\Override] public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { /** @var ISchemaWrapper $schema */ diff --git a/lib/Migration/Version20001Date20250612140256.php b/lib/Migration/Version20001Date20250612140256.php index 2c905fb02..2b91cdbcb 100644 --- a/lib/Migration/Version20001Date20250612140256.php +++ b/lib/Migration/Version20001Date20250612140256.php @@ -108,6 +108,7 @@ private function getJailedRootIds(int $storageId): array { $rootIds = []; while ($row = $result->fetch()) { + /** @var array{name: string, fileid: int} $row */ if (is_numeric($row['name'])) { $rootIds[(int)$row['name']] = (int)$row['fileid']; } @@ -122,11 +123,12 @@ private function getJailedGroupFolderRootId(int $storageId): ?int { ->where($query->expr()->eq('path_hash', $query->createNamedParameter(md5('__groupfolders')))) ->andWhere($query->expr()->eq('storage', $query->createNamedParameter($storageId))); + /** @var int|false $id */ $id = $query->executeQuery()->fetchOne(); if ($id === false) { return null; } else { - return (int)$id; + return $id; } } diff --git a/lib/Migration/Version2100000Date20251104151854.php b/lib/Migration/Version2100000Date20251104151854.php index 64786caa2..bbf8b8b30 100644 --- a/lib/Migration/Version2100000Date20251104151854.php +++ b/lib/Migration/Version2100000Date20251104151854.php @@ -17,12 +17,6 @@ use Override; class Version2100000Date20251104151854 extends SimpleMigrationStep { - /** - * @param IOutput $output - * @param Closure(): ISchemaWrapper $schemaClosure - * @param array $options - * @return null|ISchemaWrapper - */ #[Override] public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { /** @var ISchemaWrapper $schema */ diff --git a/lib/Migration/Version21000Date20250925152053.php b/lib/Migration/Version21000Date20250925152053.php index 4dc0f189c..4ea66faf2 100644 --- a/lib/Migration/Version21000Date20250925152053.php +++ b/lib/Migration/Version21000Date20250925152053.php @@ -11,7 +11,6 @@ use Closure; use OCA\GroupFolders\Folder\FolderManager; -use OCP\DB\ISchemaWrapper; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; use Override; @@ -25,9 +24,6 @@ public function __construct( ) { } - /** - * @param Closure(): ISchemaWrapper $schemaClosure - */ #[Override] public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { $this->folderManager->updateOverwriteHomeFolders(); diff --git a/lib/Mount/CacheRootPermissionsMask.php b/lib/Mount/CacheRootPermissionsMask.php index 3d0fc95d4..78133cef2 100644 --- a/lib/Mount/CacheRootPermissionsMask.php +++ b/lib/Mount/CacheRootPermissionsMask.php @@ -26,6 +26,7 @@ protected function formatCacheEntry($entry): ICacheEntry|false { $isRoot = $entry->getId() === $this->rootId; if (isset($entry['permissions']) && $isRoot) { $entry['scan_permissions'] ??= $entry['permissions']; + /** @phpstan-ignore assignOp.invalid */ $entry['permissions'] &= $this->mask; } diff --git a/lib/Mount/FolderStorageManager.php b/lib/Mount/FolderStorageManager.php index ded11a817..39b98a703 100644 --- a/lib/Mount/FolderStorageManager.php +++ b/lib/Mount/FolderStorageManager.php @@ -8,10 +8,12 @@ namespace OCA\GroupFolders\Mount; +use Exception; use OC\Files\Cache\Cache; use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\ObjectStore\PrimaryObjectStoreConfig; use OC\Files\Storage\Local; +use OC\Files\Storage\Storage; use OC\Files\Storage\Wrapper\Jail; use OCA\GroupFolders\ACL\ACLManagerFactory; use OCA\GroupFolders\ACL\ACLStorageWrapper; @@ -27,6 +29,7 @@ class FolderStorageManager { private readonly bool $enableEncryption; + /** @var array */ private array $cachedFolders = []; public function __construct( @@ -40,6 +43,7 @@ public function __construct( } /** + * @param array $options * @return array{storage_id: int, root_id: int} */ public function initRootAndStorageForFolder(int $folderId, bool $separateStorage, array $options): array { @@ -61,6 +65,7 @@ public function initRootAndStorageForFolder(int $folderId, bool $separateStorage /** * @param 'files'|'trash'|'versions'|'' $type + * @param array $options */ public function getBaseStorageForFolder( int $folderId, @@ -81,6 +86,7 @@ public function getBaseStorageForFolder( /** * @param 'files'|'trash'|'versions'|'' $type + * @param array $options */ private function getBaseStorageForFolderSeparate( int $folderId, @@ -92,8 +98,14 @@ private function getBaseStorageForFolderSeparate( array $options = [], ): IStorage { if ($this->primaryObjectStoreConfig->hasObjectStore()) { - $storage = $this->getBaseStorageForFolderSeparateStorageObject($folderId, $init, $options['bucket'] ?? null); + $bucket = $options['bucket'] ?? null; + if ($bucket !== null && !is_string($bucket)) { + throw new Exception('bucket is not a string.'); + } + /** @var Storage $storage */ + $storage = $this->getBaseStorageForFolderSeparateStorageObject($folderId, $init, $bucket); } else { + /** @var Storage $storage */ $storage = $this->getBaseStorageForFolderSeparateStorageLocal($folderId, $init); } @@ -125,7 +137,7 @@ private function getBaseStorageForFolderSeparateStorageLocal( int $folderId, bool $init = false, ): IStorage { - $dataDirectory = $this->config->getSystemValue('datadirectory'); + $dataDirectory = $this->config->getSystemValueString('datadirectory'); $rootPath = $dataDirectory . '/__groupfolders/' . $folderId; if ($init) { $result = mkdir($rootPath . '/files', recursive: true); @@ -226,6 +238,7 @@ private function getBaseStorageForFolderRootJail( } catch (NotFoundException) { $storageFolder = $parentFolder->newFolder((string)$folderId); } + /** @var Storage $rootStorage */ $rootStorage = $storageFolder->getStorage(); $rootPath = $storageFolder->getInternalPath(); @@ -279,6 +292,9 @@ private function getObjectStorageKey(int $folderId): string { return 'default'; } + /** + * @param array $objectStoreConfig + */ private function getObjectStorageBucket(int $folderId, array $objectStoreConfig, ?string $overwriteBucket = null): string { $bucketKey = 'object_store_bucket_' . $folderId; $bucket = $this->appConfig->getValueString(Application::APP_ID, $bucketKey); @@ -286,8 +302,18 @@ private function getObjectStorageBucket(int $folderId, array $objectStoreConfig, if ($overwriteBucket !== null) { $bucket = $overwriteBucket; } else { - $bucketBase = $objectStoreConfig['arguments']['bucket'] ?? ''; - $bucket = $bucketBase . $this->calculateBucketNum((string)$folderId, $objectStoreConfig); + $arguments = $objectStoreConfig['arguments']; + if (!is_array($arguments)) { + throw new Exception('arguments is not an array.'); + } + + $bucketBase = $arguments['bucket']; + if ($bucketBase !== null && !is_string($bucketBase)) { + throw new Exception('bucket is not a string.'); + } + $bucketBase ??= ''; + + $bucket = $bucketBase . $this->calculateBucketNum((string)$folderId, $arguments); } $this->appConfig->setValueString(Application::APP_ID, $bucketKey, $bucket); @@ -296,13 +322,24 @@ private function getObjectStorageBucket(int $folderId, array $objectStoreConfig, return $bucket; } - // logic taken from OC\Files\ObjectStore\Mapper which we can't use because it requires an IUser - private function calculateBucketNum(string $key, array $objectStoreConfig): string { - $numBuckets = $objectStoreConfig['arguments']['num_buckets'] ?? 64; + /** + * logic taken from OC\Files\ObjectStore\Mapper which we can't use because it requires an IUser + * @param array $arguments + */ + private function calculateBucketNum(string $key, array $arguments): string { + $numBuckets = $arguments['num_buckets']; + if ($numBuckets !== null && !is_int($numBuckets)) { + throw new Exception('num_buckets is not an integer.'); + } + $numBuckets ??= 64; // Get the bucket config and shift if provided. // Allow us to prevent writing in old filled buckets - $minBucket = (int)($objectStoreConfig['arguments']['min_bucket'] ?? 0); + $minBucket = $arguments['min_bucket']; + if ($minBucket !== null && !is_int($minBucket)) { + throw new Exception('min_bucket is not an integer.'); + } + $minBucket ??= 0; $hash = md5($key); $num = hexdec(substr($hash, 0, 4)); diff --git a/lib/Mount/GroupFolderStorage.php b/lib/Mount/GroupFolderStorage.php index f3ca4ff1b..e5d149b1a 100644 --- a/lib/Mount/GroupFolderStorage.php +++ b/lib/Mount/GroupFolderStorage.php @@ -27,6 +27,9 @@ class GroupFolderStorage extends Quota implements IConstructableStorage { private readonly IUserSession $userSession; private readonly ?IUser $mountOwner; + /** + * @param array{folder: FolderDefinition, rootCacheEntry: ?ICacheEntry, userSession: IUserSession, mountOwner: ?IUser} $parameters + */ public function __construct(array $parameters) { parent::__construct($parameters); $this->folder = $parameters['folder']; @@ -95,6 +98,7 @@ public function getScanner($path = '', $storage = null): IScanner { $storage = $this; } + /** @phpstan-ignore method.impossibleType */ if ($storage->instanceOfStorage(ObjectStoreStorage::class)) { $storage->scanner = new ObjectStoreScanner($storage); } elseif (!isset($storage->scanner)) { diff --git a/lib/Mount/GroupMountPoint.php b/lib/Mount/GroupMountPoint.php index 2588a6b7d..c3493be4d 100644 --- a/lib/Mount/GroupMountPoint.php +++ b/lib/Mount/GroupMountPoint.php @@ -18,6 +18,10 @@ use OCP\Files\Storage\IStorageFactory; class GroupMountPoint extends MountPoint implements ISystemMountPoint, IShareOwnerlessMount { + /** + * @param ?array $arguments + * @param ?array $mountOptions + */ public function __construct( private readonly FolderDefinition $folder, IStorage $storage, diff --git a/lib/Mount/MountProvider.php b/lib/Mount/MountProvider.php index 891f70438..fb35db77f 100644 --- a/lib/Mount/MountProvider.php +++ b/lib/Mount/MountProvider.php @@ -13,9 +13,11 @@ use OC\Files\Storage\Wrapper\PermissionsMask; use OCA\GroupFolders\ACL\ACLManager; use OCA\GroupFolders\ACL\ACLManagerFactory; +use OCA\GroupFolders\ACL\Rule; use OCA\GroupFolders\Folder\FolderDefinition; use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; +use OCA\Richdocuments\Db\WopiMapper; use OCP\Constants; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\ICacheEntry; @@ -94,8 +96,8 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader): array { private function getCurrentUID(): ?string { try { // wopi requests are not logged in, instead we need to get the editor user from the access token - if (str_contains($this->request->getRawPathInfo(), 'apps/richdocuments/wopi') && class_exists('OCA\Richdocuments\Db\WopiMapper')) { - $wopiMapper = Server::get('OCA\Richdocuments\Db\WopiMapper'); + if (str_contains($this->request->getRawPathInfo(), 'apps/richdocuments/wopi') && class_exists(WopiMapper::class)) { + $wopiMapper = Server::get(WopiMapper::class); $token = $this->request->getParam('access_token'); if ($token) { $wopi = $wopiMapper->getPathForToken($token); @@ -110,6 +112,9 @@ private function getCurrentUID(): ?string { return $user?->getUID(); } + /** + * @param array $rootRules + */ public function getMount( FolderDefinitionWithPermissions $folder, string $mountPoint, @@ -122,6 +127,7 @@ public function getMount( if ($aclManager && $folder->acl && $user) { $aclRootPermissions = $aclManager->getPermissionsForPathFromRules($folder->id, $cacheEntry->getPath(), $rootRules); + /** @phpstan-ignore assignOp.invalid */ $cacheEntry['permissions'] &= $aclRootPermissions; } @@ -151,7 +157,7 @@ public function getMount( } public function getTrashMount( - FolderDefinitionWithPermissions $folder, + FolderDefinition $folder, string $mountPoint, IStorageFactory $loader, ?IUser $user, @@ -212,7 +218,7 @@ public function getVersionsMount( * @param 'files'|'trash'|'versions' $type */ public function getGroupFolderStorage( - FolderDefinitionWithPermissions $folder, + FolderDefinition $folder, ?IUser $user, ?ICacheEntry $rootCacheEntry, string $type = 'files', @@ -257,7 +263,9 @@ private function findConflictsForUser(IUser $user, array $mountPoints): array { $paths = []; foreach (array_chunk($pathHashes, 1000) as $chunk) { $query->setParameter('chunk', $chunk, IQueryBuilder::PARAM_STR_ARRAY); - array_push($paths, ...$query->executeQuery()->fetchAll(PDO::FETCH_COLUMN)); + /** @var array $rows */ + $rows = $query->executeQuery()->fetchAll(PDO::FETCH_COLUMN); + array_push($paths, ...$rows); } return array_map(function (string $path): string { @@ -307,7 +315,7 @@ public function getMountsForPath(string $setupPathHint, bool $forChildren, array $loader, $user, $aclManager, - $folder->acl ? $aclManager->getRulesByFileIds([$folder->rootId]) : [], + $folder->acl ? $aclManager->getRulesByFileIds([$folder->rootId])[$folder->storageId] ?? [] : [], ); } diff --git a/lib/Mount/RootEntryCache.php b/lib/Mount/RootEntryCache.php index 675d291f2..e78fb844f 100644 --- a/lib/Mount/RootEntryCache.php +++ b/lib/Mount/RootEntryCache.php @@ -38,12 +38,19 @@ public function getId($file): int { return parent::getId($file); } + + /** + * @param array $data + */ #[\Override] public function update($id, array $data): void { $this->rootEntry = null; parent::update($id, $data); } + /** + * @param array $data + */ #[\Override] public function insert($file, array $data): int { $this->rootEntry = null; diff --git a/lib/Mount/RootPermissionsMask.php b/lib/Mount/RootPermissionsMask.php index 39a397fd2..d5d097186 100644 --- a/lib/Mount/RootPermissionsMask.php +++ b/lib/Mount/RootPermissionsMask.php @@ -8,6 +8,7 @@ namespace OCA\GroupFolders\Mount; +use OC\Files\Storage\Storage; use OC\Files\Storage\Wrapper\Wrapper; use OCA\GroupFolders\Folder\FolderDefinition; use OCP\Constants; @@ -25,7 +26,7 @@ class RootPermissionsMask extends Wrapper { private readonly FolderDefinition $folder; /** - * @param array $arguments ['storage' => $storage, 'mask' => $mask] + * @param array{storage: Storage, mask: int, folder: FolderDefinition} $arguments * * $storage: The storage the permissions mask should be applied on * $mask: The permission bits that should be kept, a combination of the \OCP\Constant::PERMISSION_ constants @@ -85,11 +86,15 @@ public function getPermissions(string $path): int { return $this->getWrapperStorage()->getPermissions($path); } + /** + * @return ?array + */ #[\Override] public function getMetaData(string $path): ?array { + /** @var ?array{permissions?: int, scan_permissions?: int} $data */ $data = parent::getMetaData($path); - if (is_array($data) && $path === '' && isset($data['permissions'])) { + if ($data !== null && $path === '' && isset($data['permissions'])) { $data['scan_permissions'] ??= $data['permissions']; $data['permissions'] &= $this->mask; } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 0fbc62ce4..fdbe579a6 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -9,44 +9,44 @@ namespace OCA\GroupFolders; /** - * @psalm-type GroupFoldersDelegationGroup = array{ + * @phpstan-type GroupFoldersDelegationGroup = array{ * gid: string, * displayName: string, * } * - * @psalm-type GroupFoldersDelegationCircle = array{ + * @phpstan-type GroupFoldersDelegationCircle = array{ * singleId: string, * displayName: string, * } * - * @psalm-type GroupFoldersGroup = array{ + * @phpstan-type GroupFoldersGroup = array{ * gid: string, * displayname: string, * } * - * @psalm-type GroupFoldersCircle = array{ + * @phpstan-type GroupFoldersCircle = array{ * sid: string, * displayname: string, * } * - * @psalm-type GroupFoldersUser = array{ + * @phpstan-type GroupFoldersUser = array{ * uid: string, * displayname: string, * } * - * @psalm-type GroupFoldersAclManage = array{ + * @phpstan-type GroupFoldersAclManage = array{ * displayname: string, * id: string, * type: 'user'|'group'|'circle', * } * - * @psalm-type GroupFoldersApplicable = array{ + * @phpstan-type GroupFoldersApplicable = array{ * displayName: string, * permissions: int, * type: 'group'|'circle', * } * - * @psalm-type GroupFoldersFolder = array{ + * @phpstan-type GroupFoldersFolder = array{ * id: int, * mount_point: string, * group_details: array, @@ -56,7 +56,7 @@ * acl: bool, * acl_default_no_permission: bool, * manage: list, - * sortIndex?: int, + * sortIndex?: non-negative-int, * } */ class ResponseDefinitions { diff --git a/lib/Service/DelegationService.php b/lib/Service/DelegationService.php index 5e7722266..a170d7b56 100644 --- a/lib/Service/DelegationService.php +++ b/lib/Service/DelegationService.php @@ -65,6 +65,9 @@ public function hasOnlyApiAccess(): bool { ]); } + /** + * @param list $settingClasses + */ private function getAccessLevel(array $settingClasses): bool { $user = $this->userSession->getUser(); if ($user === null) { diff --git a/lib/Service/FoldersFilter.php b/lib/Service/FoldersFilter.php index 07d538d6b..6bfba5585 100644 --- a/lib/Service/FoldersFilter.php +++ b/lib/Service/FoldersFilter.php @@ -13,7 +13,7 @@ use OCP\IUserSession; /** - * @psalm-import-type GroupFoldersFolder from ResponseDefinitions + * @phpstan-import-type GroupFoldersFolder from ResponseDefinitions */ class FoldersFilter { public function __construct( @@ -23,8 +23,8 @@ public function __construct( } /** - * @param GroupFoldersFolder[] $folders List of all folders - * @return GroupFoldersFolder[] + * @param array $folders List of all folders + * @return array */ public function getForApiUser(array $folders): array { $user = $this->userSession->getUser(); @@ -32,7 +32,7 @@ public function getForApiUser(array $folders): array { return []; } - return array_values(array_filter($folders, function (array $folder) use ($user): bool { + return array_filter($folders, function (array $folder) use ($user): bool { foreach ($folder['manage'] as $manager) { if ($manager['type'] === 'group') { if ($this->groupManager->isInGroup($user->getUid(), $manager['id'])) { @@ -44,6 +44,6 @@ public function getForApiUser(array $folders): array { } return false; - })); + }); } } diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index e7ef5fe2b..169d69990 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -12,6 +12,7 @@ use OCA\GroupFolders\Service\ApplicationService; use OCA\GroupFolders\Service\DelegationService; use OCP\App\IAppManager; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\Settings\IDelegatedSettings; @@ -26,6 +27,9 @@ public function __construct( ) { } + /** + * @return TemplateResponse + */ #[\Override] public function getForm(): TemplateResponse { Util::addStyle(Application::APP_ID, Application::APP_ID . '-settings'); @@ -69,6 +73,9 @@ public function getName(): ?string { return null; } + /** + * @return array + */ #[\Override] public function getAuthorizedAppConfig(): array { return []; diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 0798bde8c..2707a242f 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -9,7 +9,6 @@ namespace OCA\GroupFolders\Trash; use OC\Encryption\Exceptions\DecryptionFailedException; -use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\Storage\Wrapper\Encryption; use OC\Files\Storage\Wrapper\Jail; use OCA\Files_Trashbin\Expiration; @@ -17,6 +16,7 @@ use OCA\Files_Trashbin\Trash\ITrashBackend; use OCA\Files_Trashbin\Trash\ITrashItem; use OCA\GroupFolders\ACL\ACLManagerFactory; +use OCA\GroupFolders\Folder\FolderDefinition; use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; @@ -37,6 +37,7 @@ use OCP\IUserManager; use OCP\IUserSession; use Psr\Log\LoggerInterface; +use RuntimeException; class TrashBackend implements ITrashBackend { private ?VersionsBackend $versionsBackend = null; @@ -83,7 +84,11 @@ public function listTrashFolder(ITrashItem $folder): array { } $content = $folderNode->getDirectoryListing(); - $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($folder->getGroupFolderStorageId(), $folder->getId()); + $folderId = $folder->getId(); + if ($folderId === null) { + throw new RuntimeException('Failed to get id of folder.'); + } + $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($folder->getGroupFolderStorageId(), $folderId); return array_values(array_filter(array_map(function (Node $node) use ($folder, $user): ?GroupTrashItem { $item = new GroupTrashItem( @@ -148,8 +153,8 @@ public function restoreItem(ITrashItem $item): void { $info = pathinfo($originalLocation); $i = 1; - $gen = function (array $info, int $i): string { - $target = $info['dirname']; + do { + $target = $info['dirname'] ?? ''; if ($target === '.') { $target = ''; } @@ -161,11 +166,7 @@ public function restoreItem(ITrashItem $item): void { $target .= '.' . $info['extension']; } - return $target; - }; - - do { - $originalLocation = $gen($info, $i); + $originalLocation = $target; $i++; } while ($targetFolder->nodeExists($originalLocation)); } @@ -195,6 +196,9 @@ public function restoreItem(ITrashItem $item): void { ); } + /** + * @return array{IStorage, string} + */ private function unwrapJails(IStorage $storage, string $internalPath): array { $unJailedInternalPath = $internalPath; $unJailedStorage = $storage; @@ -247,6 +251,10 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { /** @var GroupFolderStorage $storage */ $name = basename($internalPath); $fileEntry = $storage->getCache()->get($internalPath); + if ($fileEntry === false) { + throw new RuntimeException('Failed to get cache entry.'); + } + $folder = $storage->getFolder(); $folderId = $storage->getFolderId(); @@ -255,16 +263,13 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { $time = time(); $trashName = $name . '.d' . $time; $targetInternalPath = $trashFolder->getInternalPath() . '/' . $trashName; - // until the fix from https://github.com/nextcloud/server/pull/49262 is in all versions we support we need to manually disable the optimization - if ($storage->instanceOfStorage(Encryption::class)) { - $result = $this->moveFromEncryptedStorage($storage, $trashStorage, $internalPath, $targetInternalPath); - } else { - $result = $trashStorage->moveFromStorage($storage, $internalPath, $targetInternalPath); - } + $result = $trashStorage->moveFromStorage($storage, $internalPath, $targetInternalPath); if ($result) { $originalLocation = $internalPath; if ($storage->instanceOfStorage(ISharedStorage::class)) { - $originalLocation = $storage->getWrapperStorage()->getUnjailedPath($originalLocation); + /** @var Jail $jail */ + $jail = $storage->getWrapperStorage(); + $originalLocation = $jail->getUnjailedPath($originalLocation); } $deletedBy = $this->userSession->getUser(); @@ -289,48 +294,6 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { return false; } - /** - * move from storage when we can't just move within the storage - * - * This is copied from the fallback implementation from Common::moveFromStorage - */ - private function moveFromEncryptedStorage(IStorage $sourceStorage, IStorage $targetStorage, string $sourceInternalPath, string $targetInternalPath): bool { - if (!$sourceStorage->isDeletable($sourceInternalPath)) { - return false; - } - - // the trash should be the top wrapper, remove it to prevent recursive attempts to move to trash - if ($sourceStorage instanceof Storage) { - $sourceStorage = $sourceStorage->getWrapperStorage(); - } - - $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); - if ($result) { - if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { - /** @var ObjectStoreStorage $sourceStorage */ - $sourceStorage->setPreserveCacheOnDelete(true); - } - try { - if ($sourceStorage->is_dir($sourceInternalPath)) { - $result = $sourceStorage->rmdir($sourceInternalPath); - } else { - $result = $sourceStorage->unlink($sourceInternalPath); - } - } finally { - if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { - /** @var ObjectStoreStorage $sourceStorage */ - $sourceStorage->setPreserveCacheOnDelete(false); - } - } - } - - return $result; - } - - private function userHasAccessToFolder(IUser $user, int $folderId): bool { - return $this->folderManager->getFoldersForUser($user, $folderId) !== []; - } - private function userHasAccessToItem( GroupTrashItem $item, int $permission = Constants::PERMISSION_READ, @@ -377,13 +340,17 @@ private function getNodeForTrashItem(IUser $user, ITrashItem $trashItem): ?Node return null; } - private function setupTrashFolder(FolderDefinitionWithPermissions $folder, ?IUser $user = null): Folder { + private function setupTrashFolder(FolderDefinition $folder, ?IUser $user = null): Folder { $folderId = $folder->id; $uid = $user ? $user->getUID() : 'dummy'; $mountPoint = '/' . $uid . '/files_trashbin/groupfolders/' . $folderId; $mount = $this->mountManager->find($mountPoint); + if ($mount === null) { + throw new \RuntimeException('Failed to get mount for mountpoint.'); + } + if ($mount->getMountPoint() !== $mountPoint) { $trashMount = $this->mountProvider->getTrashMount( $folder, @@ -394,7 +361,12 @@ private function setupTrashFolder(FolderDefinitionWithPermissions $folder, ?IUse $this->mountManager->addMount($trashMount); } - return $this->rootFolder->get('/' . $uid . '/files_trashbin/groupfolders/' . $folderId); + $folder = $this->rootFolder->get('/' . $uid . '/files_trashbin/groupfolders/' . $folderId); + if (!$folder instanceof Folder) { + throw new RuntimeException('Trash folder was not a folder.'); + } + + return $folder; } private function getUnJailedPath(Node $node): string { @@ -434,12 +406,12 @@ private function getTrashForFolders(IUser $user, array $folders): array { $itemsForFolder = array_map(function (Node $item) use ($user, $folder, $indexedRows): \OCA\GroupFolders\Trash\GroupTrashItem { $pathParts = pathinfo($item->getName()); - $timestamp = (int)substr($pathParts['extension'], 1); + $timestamp = (int)substr($pathParts['extension'] ?? '', 1); $name = $pathParts['filename']; $key = $folder->id . '/' . $name . '/' . $timestamp; - $originalLocation = isset($indexedRows[$key]) ? $indexedRows[$key]['original_location'] : ''; - $deletedBy = isset($indexedRows[$key]) ? $indexedRows[$key]['deleted_by'] : ''; + $originalLocation = $indexedRows[$key]['original_location'] ?: ''; + $deletedBy = $indexedRows[$key]['deleted_by'] ?: ''; return new GroupTrashItem( $this, @@ -479,10 +451,10 @@ private function getTrashForFolders(IUser $user, array $folders): array { return true; }); } - $items = array_merge($items, $itemsForFolder); + $items[] = $itemsForFolder; } - return $items; + return array_values(array_merge(...$items)); } /** @@ -529,6 +501,9 @@ public function cleanTrashFolder(FolderDefinitionWithPermissions $folder): void $this->trashManager->emptyTrashbin($folder->id); } + /** + * @return array{int, int|float} + */ public function expire(Expiration $expiration): array { $size = 0; $count = 0; diff --git a/lib/Trash/TrashManager.php b/lib/Trash/TrashManager.php index 9934abcaf..b36a86c7f 100644 --- a/lib/Trash/TrashManager.php +++ b/lib/Trash/TrashManager.php @@ -29,6 +29,9 @@ public function listTrashForFolders(array $folderIds): array { ->orderBy('deleted_time') ->where($query->expr()->in('folder_id', $query->createNamedParameter($folderIds, IQueryBuilder::PARAM_INT_ARRAY))); + /** @var list $rows */ + $rows = $query->executeQuery()->fetchAll(); + return array_map(fn (array $row): array => [ 'trash_id' => (int)$row['trash_id'], 'name' => (string)$row['name'], @@ -37,7 +40,7 @@ public function listTrashForFolders(array $folderIds): array { 'folder_id' => (int)$row['folder_id'], 'file_id' => $row['file_id'] !== null ? (int)$row['file_id'] : null, 'deleted_by' => $row['deleted_by'] !== null ? (string)$row['deleted_by'] : null, - ], $query->executeQuery()->fetchAll()); + ], $rows); } public function addTrashItem(int $folderId, string $name, int $deletedTime, string $originalLocation, int $fileId, string $deletedBy): void { @@ -55,38 +58,6 @@ public function addTrashItem(int $folderId, string $name, int $deletedTime, stri $query->executeStatement(); } - public function getTrashItemByFileId(int $fileId): ?array { - $query = $this->connection->getQueryBuilder(); - - $query->select(['trash_id', 'name', 'deleted_time', 'original_location', 'folder_id', 'deleted_by']) - ->from('group_folders_trash') - ->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); - - $row = $query->executeQuery()->fetch(); - if ($row === false) { - return null; - } - - return $row; - } - - public function getTrashItemByFileName(int $folderId, string $name, int $deletedTime): ?array { - $query = $this->connection->getQueryBuilder(); - - $query->select(['trash_id', 'name', 'deleted_time', 'original_location', 'folder_id', 'deleted_by']) - ->from('group_folders_trash') - ->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('name', $query->createNamedParameter($name))) - ->andWhere($query->expr()->eq('deleted_time', $query->createNamedParameter($deletedTime, IQueryBuilder::PARAM_INT))); - - $row = $query->executeQuery()->fetch(); - if ($row === false) { - return null; - } - - return $row; - } - public function removeItem(int $folderId, string $name, int $deletedTime): void { $query = $this->connection->getQueryBuilder(); diff --git a/lib/Versions/ExpireManager.php b/lib/Versions/ExpireManager.php index 567d78756..b8067f08d 100644 --- a/lib/Versions/ExpireManager.php +++ b/lib/Versions/ExpireManager.php @@ -9,7 +9,6 @@ namespace OCA\GroupFolders\Versions; use OCA\Files_Versions\Expiration; -use OCA\Files_Versions\Versions\IMetadataVersion; use OCA\Files_Versions\Versions\IVersion; /** @@ -73,7 +72,7 @@ protected function getAutoExpireList(int $time, array $versions): array { if ($nextInterval === -1 || $prevTimestamp > $nextInterval) { if ($version->getTimestamp() > $nextVersion) { // Do not expire versions with a label. - if ((!($version instanceof IMetadataVersion) || $version->getMetadataValue('label') === null || $version->getMetadataValue('label') === '') && !$version->isCurrentVersion()) { + if (($version->getMetadataValue('label') === null || $version->getMetadataValue('label') === '') && !$version->isCurrentVersion()) { //distance between two version too small, mark to delete $toDelete[] = $version; } @@ -122,7 +121,7 @@ public function getExpiredVersion(array $versions, int $time, bool $quotaExceede } // Do not expire versions with a label. - if ($version instanceof IMetadataVersion && $version->getMetadataValue('label') !== null && $version->getMetadataValue('label') !== '') { + if ($version->getMetadataValue('label') !== null && $version->getMetadataValue('label') !== '') { return false; } diff --git a/lib/Versions/GroupVersion.php b/lib/Versions/GroupVersion.php index f42e8b24e..17c7ed4fe 100644 --- a/lib/Versions/GroupVersion.php +++ b/lib/Versions/GroupVersion.php @@ -16,6 +16,9 @@ use OCP\IUser; class GroupVersion extends Version { + /** + * @param array $metadata + */ public function __construct( int $timestamp, int $revisionId, diff --git a/lib/Versions/GroupVersionEntity.php b/lib/Versions/GroupVersionEntity.php index 7cd289ec5..57989576c 100644 --- a/lib/Versions/GroupVersionEntity.php +++ b/lib/Versions/GroupVersionEntity.php @@ -42,6 +42,9 @@ public function __construct() { $this->addType('metadata', Types::STRING); } + /** + * @return array{id: ?int, file_id: ?int, timestamp: ?int, size: ?int, mimetype: ?int, metadata: ?string} + */ #[\Override] public function jsonSerialize(): array { return [ @@ -54,10 +57,17 @@ public function jsonSerialize(): array { ]; } + /** + * @return array + */ public function getDecodedMetadata(): array { + /** @phpstan-ignore return.type */ return json_decode($this->metadata ?? '', true, 512, JSON_THROW_ON_ERROR) ?? []; } + /** + * @param array $value + */ public function setDecodedMetadata(array $value): void { $this->metadata = json_encode($value, JSON_THROW_ON_ERROR); $this->markFieldUpdated('metadata'); diff --git a/lib/Versions/GroupVersionsExpireManager.php b/lib/Versions/GroupVersionsExpireManager.php index 6094344a2..c85a21dcc 100644 --- a/lib/Versions/GroupVersionsExpireManager.php +++ b/lib/Versions/GroupVersionsExpireManager.php @@ -17,7 +17,6 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\FileInfo; -use OCP\IUser; use Psr\Log\LoggerInterface; class GroupVersionsExpireManager { @@ -49,7 +48,6 @@ public function expireFolders(array $folders): void { public function expireFolder(FolderWithMappingsAndCache $folder): void { $baseFolder = $this->versionsBackend->getVersionsFolder($folder); $files = $this->versionsBackend->getAllVersionedFiles($folder); - /** @var IUser */ $dummyUser = new User('', null, $this->dispatcher); foreach ($files as $fileId => $file) { if ($file instanceof FileInfo) { diff --git a/lib/Versions/VersionsBackend.php b/lib/Versions/VersionsBackend.php index 045035748..f13a59c6e 100644 --- a/lib/Versions/VersionsBackend.php +++ b/lib/Versions/VersionsBackend.php @@ -9,6 +9,7 @@ namespace OCA\GroupFolders\Versions; +use OC\Files\Storage\Storage; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\Files_Versions\Versions\IDeletableVersionBackend; use OCA\Files_Versions\Versions\IMetadataVersion; @@ -39,6 +40,7 @@ use OCP\IUser; use OCP\IUserSession; use Psr\Log\LoggerInterface; +use RuntimeException; class VersionsBackend implements IVersionBackend, IMetadataVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend, IVersionsImporterBackend { public function __construct( @@ -104,7 +106,11 @@ public function getVersionsForFile(IUser $user, FileInfo $file): array { // Insert the entry in the DB for the current version. $versionEntity = new GroupVersionEntity(); - $versionEntity->setFileId($file->getId()); + $fileId = $file->getId(); + if ($fileId === null) { + throw new RuntimeException('Failed to get id of file.'); + } + $versionEntity->setFileId($fileId); $versionEntity->setTimestamp($file->getMTime()); $versionEntity->setSize($file->getSize()); $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype())); @@ -119,7 +125,11 @@ public function getVersionsForFile(IUser $user, FileInfo $file): array { } $versionEntity = new GroupVersionEntity(); - $versionEntity->setFileId($file->getId()); + $fileId = $file->getId(); + if ($fileId === null) { + throw new RuntimeException('Failed to get id of file.'); + } + $versionEntity->setFileId($fileId); // HACK: before this commit, versions were created with the current timestamp instead of the version's mtime. // This means that the name of some versions is the exact mtime of the next version. This behavior is now fixed. // To prevent occasional conflicts between the last version and the current one, we decrement the last version mtime. @@ -155,7 +165,12 @@ private function getVersionsForFileFromDB(FileInfo $fileInfo, IUser $user): arra } $versionsFolder = $this->getVersionFolderForFile($fileInfo); - $versionEntities = $this->groupVersionsMapper->findAllVersionsForFileId($fileInfo->getId()); + $fileInfoId = $fileInfo->getId(); + if ($fileInfoId === null) { + throw new RuntimeException('Failed to get id of fileinfo.'); + } + + $versionEntities = $this->groupVersionsMapper->findAllVersionsForFileId($fileInfoId); $mappedVersions = array_map( function (GroupVersionEntity $versionEntity) use ($versionsFolder, $mountPoint, $fileInfo, $user, $folder): ?GroupVersion { $currentVersion = false; @@ -179,8 +194,8 @@ function (GroupVersionEntity $versionEntity) use ($versionsFolder, $mountPoint, } } - if ($versionFile instanceof Folder) { - $this->logger->warning('Encountered version file that was a folder', ['fileid' => $versionFile->getId(), 'path' => $versionFile->getPath()]); + if (!$versionFile instanceof File) { + $this->logger->warning('Encountered version file that was not a file', ['fileid' => $versionFile->getId(), 'path' => $versionFile->getPath()]); $versionFile->delete(); $this->groupVersionsMapper->delete($versionEntity); @@ -193,7 +208,7 @@ function (GroupVersionEntity $versionEntity) use ($versionsFolder, $mountPoint, $versionEntity->getTimestamp(), $fileInfo->getName(), $versionEntity->getSize(), - $this->mimeTypeLoader->getMimetypeById($versionEntity->getMimetype()), + $this->mimeTypeLoader->getMimetypeById($versionEntity->getMimetype()) ?? '', $mountPoint->getInternalPath($fileInfo->getPath()), $fileInfo, $this, @@ -216,14 +231,27 @@ public function createVersion(IUser $user, FileInfo $file): void { $versionMount = $versionsFolder->getMountPoint(); $sourceMount = $file->getMountPoint(); - $sourceCache = $sourceMount->getStorage()->getCache(); + $sourceStorage = $sourceMount->getStorage(); + if ($sourceStorage === null) { + throw new \RuntimeException('Failed to get storage for source mount.'); + } + $sourceCache = $sourceStorage->getCache(); $revision = $file->getMtime(); $versionInternalPath = $versionsFolder->getInternalPath() . '/' . $revision; $sourceInternalPath = $file->getInternalPath(); - $versionMount->getStorage()->copyFromStorage($sourceMount->getStorage(), $sourceInternalPath, $versionInternalPath); - $versionMount->getStorage()->getCache()->copyFromCache($sourceCache, $sourceCache->get($sourceInternalPath), $versionInternalPath); + $sourceCacheEntry = $sourceCache->get($sourceInternalPath); + if ($sourceCacheEntry === false) { + throw new RuntimeException('Failed to get source cache entry'); + } + + $versionStorage = $versionMount->getStorage(); + if ($versionStorage === null) { + throw new \RuntimeException('Failed to get storage for version mount.'); + } + $versionStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $versionInternalPath); + $versionStorage->getCache()->copyFromCache($sourceCache, $sourceCacheEntry, $versionInternalPath); } public function rollback(IVersion $version): void { @@ -239,15 +267,28 @@ public function rollback(IVersion $version): void { /** @var GroupMountPoint $targetMount */ $targetMount = $version->getSourceFile()->getMountPoint(); - $targetCache = $targetMount->getStorage()->getCache(); + $targetStorage = $targetMount->getStorage(); + if ($targetStorage === null) { + throw new \RuntimeException('Failed to get storage for target mount.'); + } + $targetCache = $targetStorage->getCache(); $versionMount = $version->getVersionFile()->getMountPoint(); - $versionCache = $versionMount->getStorage()->getCache(); + $versionStorage = $versionMount->getStorage(); + if ($versionStorage === null) { + throw new \RuntimeException('Failed to get storage for version mount.'); + } + $versionCache = $versionStorage->getCache(); $targetInternalPath = $version->getSourceFile()->getInternalPath(); $versionInternalPath = $version->getVersionFile()->getInternalPath(); - $targetMount->getStorage()->copyFromStorage($versionMount->getStorage(), $versionInternalPath, $targetInternalPath); - $targetCache->copyFromCache($versionCache, $versionCache->get($versionInternalPath), $targetInternalPath); + $versionCacheEntry = $versionCache->get($versionInternalPath); + if ($versionCacheEntry === false) { + throw new RuntimeException('Failed to get version cache entry'); + } + + $targetStorage->copyFromStorage($versionStorage, $versionInternalPath, $targetInternalPath); + $targetCache->copyFromCache($versionCache, $versionCacheEntry, $targetInternalPath); } public function read(IVersion $version) { @@ -274,10 +315,6 @@ public function getAllVersionedFiles(FolderDefinitionWithMappings $folder): arra $versionsFolder = $this->getVersionsFolder($folder); $folderWithPermissions = FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL); $mount = $this->mountProvider->getMount($folderWithPermissions, '/groupfolders/' . $folder->mountPoint); - if ($mount === null) { - $this->logger->error('Tried to get all the versioned files from a non existing mountpoint'); - return []; - } $this->mountManager->addMount($mount); try { @@ -288,9 +325,15 @@ public function getAllVersionedFiles(FolderDefinitionWithMappings $folder): arra $fileIds = array_map(fn (Node $node): int => (int)$node->getName(), $contents); $files = array_map(function (int $fileId) use ($mount): ?FileInfo { - $cacheEntry = $mount->getStorage()->getCache()->get($fileId); + /** @var ?Storage $storage */ + $storage = $mount->getStorage(); + if ($storage === null) { + throw new \RuntimeException('Failed to get storage for mount.'); + } + + $cacheEntry = $storage->getCache()->get($fileId); if ($cacheEntry) { - return new \OC\Files\FileInfo($mount->getMountPoint() . '/' . $cacheEntry->getPath(), $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount); + return new \OC\Files\FileInfo($mount->getMountPoint() . '/' . $cacheEntry->getPath(), $storage, $cacheEntry->getPath(), $cacheEntry, $mount); } else { return null; } @@ -311,6 +354,10 @@ public function deleteAllVersionsForFile(FolderDefinition $folder, int $fileId): public function getVersionsFolder(FolderDefinition $folder): Folder { $mountPoint = '/dummy/files_versions/groupfolders/' . $folder->id; $mount = $this->mountManager->find($mountPoint); + if ($mount === null) { + throw new \RuntimeException('Failed to get mount for mountpoint.'); + } + // check that $mount is the version mount, and not a mount for a parent folder if ($mount->getMountPoint() !== $mountPoint) { $versionMount = $this->mountProvider->getVersionsMount( @@ -320,7 +367,13 @@ public function getVersionsFolder(FolderDefinition $folder): Folder { ); $this->mountManager->addMount($versionMount); } - return $this->rootFolder->get($mountPoint); + + $folder = $this->rootFolder->get($mountPoint); + if (!$folder instanceof Folder) { + throw new RuntimeException('Versions folder was not a folder.'); + } + + return $folder; } public function setMetadataValue(Node $node, int $revision, string $key, string $value): void { @@ -350,14 +403,19 @@ public function deleteVersion(IVersion $version): void { /** @var Folder $versionsFolder */ $versionsFolder->get((string)$version->getRevisionId())->delete(); + $versionFileId = $version->getSourceFile()->getId(); + if ($versionFileId === null) { + throw new RuntimeException('Failed to get id of file.'); + } + $versionEntity = $this->groupVersionsMapper->findVersionForFileId( - $version->getSourceFile()->getId(), + $versionFileId, $version->getTimestamp(), ); $this->groupVersionsMapper->delete($versionEntity); } - public function createVersionEntity(File $file): void { + public function createVersionEntity(File $file): null { $fileId = $file->getId(); $timestamp = $file->getMTime(); @@ -372,8 +430,13 @@ public function createVersionEntity(File $file): void { $versionEntity->setDecodedMetadata([]); $this->groupVersionsMapper->insert($versionEntity); } + + return null; } + /** + * @param array{timestamp?: int, size?: int, mimetype?: int} $properties + */ public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void { $versionEntity = $this->groupVersionsMapper->findVersionForFileId($sourceFile->getId(), $revision); @@ -418,7 +481,11 @@ public function importVersionsForFile(IUser $user, Node $source, Node $target, a if ($version->getTimestamp() !== $source->getMTime()) { $backend = $version->getBackend(); $versionFile = $backend->getVersionFile($user, $source, $version->getRevisionId()); - $versionsFolder->newFile($version->getRevisionId(), $versionFile->fopen('r')); + $versionFileHandle = $versionFile->fopen('r'); + if ($versionFileHandle === false) { + throw new RuntimeException('Failed to open version file.'); + } + $versionsFolder->newFile((string)$version->getRevisionId(), $versionFileHandle); } // 2. Create the entity in the database diff --git a/openapi.json b/openapi.json index c449b5c16..0a1ba5c9f 100644 --- a/openapi.json +++ b/openapi.json @@ -189,7 +189,8 @@ }, "sortIndex": { "type": "integer", - "format": "int64" + "format": "int64", + "minimum": 0 } } }, diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..c043781dd --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +parameters: + level: 10 + phpVersion: + min: 80200 + max: 80500 + paths: + - lib + - templates + - tests + excludePaths: + - tests/stubs + scanDirectories: + - vendor-bin/phpunit/vendor/phpunit/phpunit + scanFiles: + - tests/stubs/doctrine_dbal_arrayparametertype.php + - tests/stubs/doctrine_dbal_schema_abstractasset.php + - tests/stubs/doctrine_dbal_schema_column.php + - tests/stubs/doctrine_dbal_schema_schema.php + - tests/stubs/doctrine_dbal_schema_table.php + - tests/stubs/icewind_streams_directory.php + - tests/stubs/icewind_streams_iteratordirectory.php + - tests/stubs/icewind_streams_wrapperhandler.php + - tests/stubs/oc.php + - tests/stubs/oc_app.php + - tests/stubs/oc_appframework_ocs_baseresponse.php + - tests/stubs/oc_appframework_ocs_v1response.php + - tests/stubs/oc_appframework_utility_simplecontainer.php + - tests/stubs/oc_core_command_base.php + - tests/stubs/oc_db_querybuilder_querybuilder.php + - tests/stubs/oc_db_querybuilder_typedquerybuilder.php + - tests/stubs/oc_encryption_exceptions_decryptionfailedexception.php + - tests/stubs/oc_files_cache_cache.php + - tests/stubs/oc_files_cache_cacheentry.php + - tests/stubs/oc_files_cache_scanner.php + - tests/stubs/oc_files_cache_wrapper_cachejail.php + - tests/stubs/oc_files_cache_wrapper_cachewrapper.php + - tests/stubs/oc_files_fileinfo.php + - tests/stubs/oc_files_filesystem.php + - tests/stubs/oc_files_mount_mountpoint.php + - tests/stubs/oc_files_node_lazyfolder.php + - tests/stubs/oc_files_node_node.php + - tests/stubs/oc_files_objectstore_objectstorescanner.php + - tests/stubs/oc_files_objectstore_objectstorestorage.php + - tests/stubs/oc_files_objectstore_primaryobjectstoreconfig.php + - tests/stubs/oc_files_setupmanager.php + - tests/stubs/oc_files_storage_common.php + - tests/stubs/oc_files_storage_local.php + - tests/stubs/oc_files_storage_storage.php + - tests/stubs/oc_files_storage_temporary.php + - tests/stubs/oc_files_storage_wrapper_encryption.php + - tests/stubs/oc_files_storage_wrapper_jail.php + - tests/stubs/oc_files_storage_wrapper_permissionsmask.php + - tests/stubs/oc_files_storage_wrapper_quota.php + - tests/stubs/oc_files_storage_wrapper_wrapper.php + - tests/stubs/oc_files_view.php + - tests/stubs/oc_group_database.php + - tests/stubs/oc_group_manager.php + - tests/stubs/oc_hook.php + - tests/stubs/oc_hooks_basicemitter.php + - tests/stubs/oc_hooks_emitter.php + - tests/stubs/oc_hooks_emittertrait.php + - tests/stubs/oc_hooks_publicemitter.php + - tests/stubs/oc_server.php + - tests/stubs/oc_servercontainer.php + - tests/stubs/oc_settings_authorizedgroup.php + - tests/stubs/oc_settings_authorizedgroupmapper.php + - tests/stubs/oc_user_user.php + - tests/stubs/oca_circles_circlesmanager.php + - tests/stubs/oca_circles_circlesqueryhelper.php + - tests/stubs/oca_circles_db_corequerybuilder.php + - tests/stubs/oca_circles_events_circledestroyedevent.php + - tests/stubs/oca_circles_events_circleresultgenericevent.php + - tests/stubs/oca_circles_exceptions_circlenotfoundexception.php + - tests/stubs/oca_circles_exceptions_federateditemexception.php + - tests/stubs/oca_circles_exceptions_federateditemnotfoundexception.php + - tests/stubs/oca_circles_ientity.php + - tests/stubs/oca_circles_ifederatedmodel.php + - tests/stubs/oca_circles_ifederateduser.php + - tests/stubs/oca_circles_iqueryprobe.php + - tests/stubs/oca_circles_model_circle.php + - tests/stubs/oca_circles_model_federateduser.php + - tests/stubs/oca_circles_model_managedmodel.php + - tests/stubs/oca_circles_model_member.php + - tests/stubs/oca_circles_model_probes_basicprobe.php + - tests/stubs/oca_circles_model_probes_circleprobe.php + - tests/stubs/oca_circles_model_probes_memberprobe.php + - tests/stubs/oca_circles_tools_db_extendedquerybuilder.php + - tests/stubs/oca_circles_tools_db_iqueryrow.php + - tests/stubs/oca_circles_tools_ideserializable.php + - tests/stubs/oca_dav_connector_sabre_directory.php + - tests/stubs/oca_dav_connector_sabre_exception_forbidden.php + - tests/stubs/oca_dav_connector_sabre_node.php + - tests/stubs/oca_dav_connector_sabre_principal.php + - tests/stubs/oca_files_event_loadadditionalscriptsevent.php + - tests/stubs/oca_files_sharing_event_beforetemplaterenderedevent.php + - tests/stubs/oca_files_trashbin_expiration.php + - tests/stubs/oca_files_trashbin_storage.php + - tests/stubs/oca_files_trashbin_trash_itrashbackend.php + - tests/stubs/oca_files_trashbin_trash_itrashitem.php + - tests/stubs/oca_files_trashbin_trash_trashitem.php + - tests/stubs/oca_files_versions_expiration.php + - tests/stubs/oca_files_versions_versions_ideletableversionbackend.php + - tests/stubs/oca_files_versions_versions_imetadataversion.php + - tests/stubs/oca_files_versions_versions_imetadataversionbackend.php + - tests/stubs/oca_files_versions_versions_ineedsyncversionbackend.php + - tests/stubs/oca_files_versions_versions_iversion.php + - tests/stubs/oca_files_versions_versions_iversionbackend.php + - tests/stubs/oca_files_versions_versions_iversionsimporterbackend.php + - tests/stubs/oca_files_versions_versions_version.php + - tests/stubs/oca_richdocuments_db_wopi.php + - tests/stubs/oca_richdocuments_db_wopimapper.php + - tests/stubs/oca_settings_service_authorizedgroupservice.php + - tests/stubs/stecman_component_symfony_console_bashcompletion_completion_completionawareinterface.php + - tests/stubs/symfony_component_console_command_command.php + - tests/stubs/symfony_component_console_exception_invalidargumentexception.php + - tests/stubs/symfony_component_console_helper_questionhelper.php + - tests/stubs/symfony_component_console_helper_table.php + - tests/stubs/symfony_component_console_input_inputargument.php + - tests/stubs/symfony_component_console_input_inputinterface.php + - tests/stubs/symfony_component_console_input_inputoption.php + - tests/stubs/symfony_component_console_output_outputinterface.php + - tests/stubs/symfony_component_console_question_confirmationquestion.php + - tests/stubs/symfony_component_console_question_question.php + - tests/stubs/test_testcase.php + - tests/stubs/test_traits_usertrait.php diff --git a/templates/index.php b/templates/index.php index 36dbe8b25..e1e5f1554 100644 --- a/templates/index.php +++ b/templates/index.php @@ -3,11 +3,18 @@ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + +use OCP\IL10N; + ?>

- t('Team folders')); ?> + t('Team folders')); +?>

diff --git a/tests/ACL/ACLCacheWrapperTest.php b/tests/ACL/ACLCacheWrapperTest.php index 1d97641cf..895ce0881 100644 --- a/tests/ACL/ACLCacheWrapperTest.php +++ b/tests/ACL/ACLCacheWrapperTest.php @@ -23,6 +23,7 @@ class ACLCacheWrapperTest extends TestCase { private ACLManager&MockObject $aclManager; private ICache&MockObject $source; private ACLCacheWrapper $cache; + /** @var array */ private array $aclPermissions = []; #[\Override] diff --git a/tests/ACL/ACLScannerTest.php b/tests/ACL/ACLScannerTest.php index e72ea020c..8984d420c 100644 --- a/tests/ACL/ACLScannerTest.php +++ b/tests/ACL/ACLScannerTest.php @@ -20,6 +20,9 @@ * @group DB */ class ACLScannerTest extends TestCase { + /** + * @param array $rules + */ private function getAclManager(array $rules): ACLManager&MockObject { $manager = $this->getMockBuilder(ACLManager::class) ->disableOriginalConstructor() @@ -43,7 +46,9 @@ public function testScanAclStorage(): void { $cache->calculateFolderSize('foo/bar'); $cache->calculateFolderSize('foo'); - $this->assertEquals(-1, $cache->get('foo/bar')->getSize()); + $cacheEntry = $cache->get('foo/bar'); + $this->assertNotFalse($cacheEntry); + $this->assertEquals(-1, $cacheEntry->getSize()); $acls = $this->getAclManager([ 'foo/bar' => 0, @@ -62,9 +67,9 @@ public function testScanAclStorage(): void { $aclCache = $aclStorage->getCache(); $scanner->scan(''); - $this->assertEquals(0, $cache->get('foo/bar')->getSize()); - - $this->assertEquals(31, $cache->get('foo/bar')->getPermissions()); + $cacheEntry = $cache->get('foo/bar'); + $this->assertNotFalse($cacheEntry); + $this->assertEquals(31, $cacheEntry->getPermissions()); $this->assertEquals(false, $aclCache->get('foo/bar')); } } diff --git a/tests/ACL/ACLStorageWrapperTest.php b/tests/ACL/ACLStorageWrapperTest.php index 6b8b48cbe..1a719ce8b 100644 --- a/tests/ACL/ACLStorageWrapperTest.php +++ b/tests/ACL/ACLStorageWrapperTest.php @@ -20,6 +20,7 @@ class ACLStorageWrapperTest extends TestCase { private ACLManager&MockObject $aclManager; private IStorage $source; private ACLStorageWrapper $storage; + /** @var array */ private array $aclPermissions = []; #[\Override] @@ -62,7 +63,7 @@ public function testOpenDir(): void { $dh = $this->storage->opendir('foo'); $result = []; - while ($file = readdir($dh)) { + while ($file = readdir($dh ?: null)) { $result[] = $file; } diff --git a/tests/ACL/RuleManagerTest.php b/tests/ACL/RuleManagerTest.php index 4b9dc835b..4018c1688 100644 --- a/tests/ACL/RuleManagerTest.php +++ b/tests/ACL/RuleManagerTest.php @@ -41,7 +41,10 @@ protected function setUp(): void { $this->userMappingManager = $this->createMock(IUserMappingManager::class); $this->userMappingManager->expects($this->any()) ->method('mappingFromId') - ->willReturnCallback(fn (string $type, string $id): UserMapping => /** @var 'user'|'group'|'dummy' $type */ new UserMapping($type, $id)); + ->willReturnCallback(function (string $type, string $id): UserMapping { + /** @var 'user'|'group'|'dummy' $type */ + return new UserMapping($type, $id); + }); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->ruleManager = new RuleManager(Server::get(IDBConnection::class), $this->userMappingManager, $this->eventDispatcher); diff --git a/tests/ACL/RuleTest.php b/tests/ACL/RuleTest.php index 10717959a..1c8783344 100644 --- a/tests/ACL/RuleTest.php +++ b/tests/ACL/RuleTest.php @@ -13,6 +13,9 @@ use Test\TestCase; class RuleTest extends TestCase { + /** + * @return list> + */ public static function permissionsProvider(): array { return [ [0b00000000, 0b00000000, 0b00000000, 0b00000000], @@ -31,6 +34,9 @@ public function testApplyPermissions(int $input, int $mask, int $permissions, in $this->assertEquals($expected, $rule->applyPermissions($input)); } + /** + * @return list>, int, int}> + */ public static function mergeRulesProvider(): array { return [ [[ @@ -55,6 +61,7 @@ public static function mergeRulesProvider(): array { /** * @dataProvider mergeRulesProvider + * @param list $inputs */ public function testMergeRules(array $inputs, int $expectedMask, int $expectedPermissions): void { $inputRules = array_map(fn (array $input): Rule => new Rule($this->createMock(IUserMapping::class), 0, $input[0], $input[1]), $inputs); diff --git a/tests/Folder/FolderManagerTest.php b/tests/Folder/FolderManagerTest.php index 44fda0c06..eda4f24ee 100644 --- a/tests/Folder/FolderManagerTest.php +++ b/tests/Folder/FolderManagerTest.php @@ -32,7 +32,7 @@ /** * @group DB * - * @psalm-import-type GroupFoldersApplicable from ResponseDefinitions + * @phpstan-import-type GroupFoldersApplicable from ResponseDefinitions */ class FolderManagerTest extends TestCase { private FolderManager $manager; diff --git a/tests/Listeners/CircleDestroyedEventListenerTest.php b/tests/Listeners/CircleDestroyedEventListenerTest.php index 5cac85aea..ab6a35bd6 100644 --- a/tests/Listeners/CircleDestroyedEventListenerTest.php +++ b/tests/Listeners/CircleDestroyedEventListenerTest.php @@ -37,6 +37,9 @@ public function testHandleInvalid(): void { ->expects($this->never()) ->method('deleteCircle'); + /** + * @phpstan-ignore argument.type + */ $this->listener->handle($event); } diff --git a/tests/Listeners/LoadAdditionalScriptsListenerTest.php b/tests/Listeners/LoadAdditionalScriptsListenerTest.php index a183af27a..01992b59c 100644 --- a/tests/Listeners/LoadAdditionalScriptsListenerTest.php +++ b/tests/Listeners/LoadAdditionalScriptsListenerTest.php @@ -26,6 +26,9 @@ protected function setUp(): void { $this->listener = new LoadAdditionalScriptsListener(); } + /** + * @return list}> + */ public static function handleProvider(): array { $expectedScripts = [ 'groupfolders/l10n/en', @@ -41,6 +44,7 @@ public static function handleProvider(): array { /** * @dataProvider handleProvider * @param class-string $class + * @param list $expectedScripts */ public function testHandle(string $class, array $expectedScripts): void { $event = $this->createMock($class); diff --git a/tests/Listeners/NodeRenamedListenerTest.php b/tests/Listeners/NodeRenamedListenerTest.php index 930b9f3eb..d62e70a3c 100644 --- a/tests/Listeners/NodeRenamedListenerTest.php +++ b/tests/Listeners/NodeRenamedListenerTest.php @@ -86,6 +86,9 @@ public function testHandleInvalid(): void { ->expects($this->never()) ->method('getSource'); + /** + * @phpstan-ignore argument.type + */ $this->listener->handle($event); } diff --git a/tests/Trash/TrashBackendTest.php b/tests/Trash/TrashBackendTest.php index 72c983e59..6119313d2 100644 --- a/tests/Trash/TrashBackendTest.php +++ b/tests/Trash/TrashBackendTest.php @@ -11,8 +11,6 @@ use OC\Files\SetupManager; use OC\Group\Database; -use OCA\GroupFolders\ACL\ACLManager; -use OCA\GroupFolders\ACL\ACLManagerFactory; use OCA\GroupFolders\ACL\Rule; use OCA\GroupFolders\ACL\RuleManager; use OCA\GroupFolders\ACL\UserMapping\UserMapping; @@ -38,7 +36,6 @@ class TrashBackendTest extends TestCase { private string $folderName; private TrashBackend $trashBackend; private FolderManager $folderManager; - private ACLManager $aclManager; private RuleManager $ruleManager; private int $folderId; private Folder $managerUserFolder; @@ -63,9 +60,6 @@ public function setUp(): void { $this->trashBackend = \OCP\Server::get(TrashBackend::class); $this->folderManager = \OCP\Server::get(FolderManager::class); - /** @var ACLManagerFactory $aclManagerFactory */ - $aclManagerFactory = \OCP\Server::get(ACLManagerFactory::class); - $this->aclManager = $aclManagerFactory->getACLManager($this->managerUser); $this->ruleManager = \OCP\Server::get(RuleManager::class); $this->folderId = $this->folderManager->createFolder($this->folderName); @@ -83,7 +77,6 @@ public function setUp(): void { $this->assertTrue($this->managerUserFolder->nodeExists($this->folderName)); $this->assertTrue($this->normalUserFolder->nodeExists($this->folderName)); - /** @var GroupFolderStorage $groupFolderStorage */ $groupFolderStorage = $this->managerUserFolder->get($this->folderName)->getStorage(); $this->assertTrue($groupFolderStorage->instanceOfStorage(GroupFolderStorage::class)); $this->assertEquals($this->folderId, $groupFolderStorage->getFolderId()); diff --git a/tests/stubs/doctrine_dbal_arrayparametertype.php b/tests/stubs/doctrine_dbal_arrayparametertype.php new file mode 100644 index 000000000..8aabe3992 --- /dev/null +++ b/tests/stubs/doctrine_dbal_arrayparametertype.php @@ -0,0 +1,37 @@ + + */ + public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array + { + } + + /** + * enables an app + * + * @param array $groups (optional) when set, only these groups will have access to the app + * @throws \Exception + * @deprecated 32.0.0 Use the installer and the app manager instead + * + * This function set an app as enabled in appconfig. + */ + public function enable(string $appId, array $groups = []): void + { + } + + /** + * Find the apps root for an app id. + * + * If multiple copies are found, the apps root the latest version is returned. + * + * @param bool $ignoreCache ignore cache and rebuild it + * @return false|array{path: string, url: string} the apps root shape + * @deprecated 32.0.0 internal, use getAppPath or getAppWebPath + */ + public static function findAppInDirectories(string $appId, bool $ignoreCache = false) + { + } + + /** + * get app's version based on it's path + * + * @deprecated 32.0.0 use Server::get(IAppManager)->getAppInfoByPath() with the path to info.xml directly + */ + public static function getAppVersionByPath(string $path): string + { + } + + /** + * get the id of loaded app + */ + public static function getCurrentApp(): string + { + } + + /** + * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface + */ + public static function registerLogIn(array $entry): void + { + } + + /** + * @return array + */ + public static function getAlternativeLogIns(): array + { + } + + /** + * get a list of all apps in the apps folder + * + * @return string[] an array of app names (string IDs) + * @deprecated 31.0.0 Use IAppManager::getAllAppsInAppsFolders instead + */ + public static function getAllApps(): array + { + } + + /** + * List all supported apps + * + * @deprecated 32.0.0 Use \OCP\Support\Subscription\IRegistry::delegateGetSupportedApps instead + */ + public function getSupportedApps(): array + { + } + + /** + * List all apps, this is used in apps.php + */ + public function listAllApps(): array + { + } + + /** + * @deprecated 32.0.0 Use IAppManager::isUpgradeRequired instead + */ + public static function shouldUpgrade(string $app): bool + { + } + + /** + * Check whether the current Nextcloud version matches the given + * application's version requirements. + * + * The comparison is made based on the number of parts that the + * app info version has. For example for ownCloud 6.0.3 if the + * app info version is expecting version 6.0, the comparison is + * made on the first two parts of the ownCloud version. + * This means that it's possible to specify "requiremin" => 6 + * and "requiremax" => 6 and it will still match ownCloud 6.0.3. + * + * @param string $ocVersion Nextcloud version to check against + * @param array $appInfo app info (from xml) + * + * @return bool true if compatible, otherwise false + * @deprecated 32.0.0 Use IAppManager::isAppCompatible instead + */ + public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool + { + } + + /** + * get the installed version of all apps + * @deprecated 32.0.0 Use IAppManager::getAppInstalledVersions or IAppConfig::getAppInstalledVersions instead + */ + public static function getAppVersions(): array + { + } + + /** + * update the database for the app and call the update script + * + * @deprecated 32.0.0 Use IAppManager::upgradeApp instead + */ + public static function updateApp(string $appId): bool + { + } + + /** + * @param string $appId + * @param string[] $steps + * @throws NeedsUpdateException + */ + public static function executeRepairSteps(string $appId, array $steps) + { + } + + /** + * @deprecated 32.0.0 Use the IJobList directly instead + */ + public static function setupBackgroundJobs(array $jobs): void + { + } + + /** + * @throws \Exception + */ + public static function checkAppDependencies(IConfig $config, IL10N $l, array $info, bool $ignoreMax): void + { + } +} diff --git a/tests/stubs/oc_encryption_exceptions_decryptionfailedexception.php b/tests/stubs/oc_encryption_exceptions_decryptionfailedexception.php new file mode 100644 index 000000000..126aec137 --- /dev/null +++ b/tests/stubs/oc_encryption_exceptions_decryptionfailedexception.php @@ -0,0 +1,15 @@ + + */ +class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { + /** + * @param string|boolean $path + * @param Storage\Storage $storage + * @param string $internalPath + * @param array|ICacheEntry $data + * @param IMountPoint $mount + * @param ?IUser $owner + */ + public function __construct($path, $storage, $internalPath, $data, $mount, $owner = null) + { + } + + public function offsetSet($offset, $value): void + { + } + + public function offsetExists($offset): bool + { + } + + public function offsetUnset($offset): void + { + } + + public function offsetGet(mixed $offset): mixed + { + } + + /** + * @return string + */ + public function getPath() + { + } + + public function getStorage() + { + } + + /** + * @return string + */ + public function getInternalPath() + { + } + + /** + * Get FileInfo ID or null in case of part file + * + * @return int|null + */ + public function getId() + { + } + + public function getMimetype(): string + { + } + + /** + * @return string + */ + public function getMimePart() + { + } + + /** + * @return string + */ + public function getName() + { + } + + /** + * @return string + */ + public function getEtag() + { + } + + /** + * @param bool $includeMounts + * @return int|float + */ + public function getSize($includeMounts = true) + { + } + + /** + * @return int + */ + public function getMTime() + { + } + + /** + * @return bool + */ + public function isEncrypted() + { + } + + /** + * Return the current version used for the HMAC in the encryption app + */ + public function getEncryptedVersion(): int + { + } + + /** + * @return int + */ + public function getPermissions() + { + } + + /** + * @return string \OCP\Files\FileInfo::TYPE_FILE|\OCP\Files\FileInfo::TYPE_FOLDER + */ + public function getType() + { + } + + public function getData() + { + } + + /** + * @param int $permissions + * @return bool + */ + protected function checkPermissions($permissions) + { + } + + /** + * @return bool + */ + public function isReadable() + { + } + + /** + * @return bool + */ + public function isUpdateable() + { + } + + /** + * Check whether new files or folders can be created inside this folder + * + * @return bool + */ + public function isCreatable() + { + } + + /** + * @return bool + */ + public function isDeletable() + { + } + + /** + * @return bool + */ + public function isShareable() + { + } + + /** + * Check if a file or folder is shared + * + * @return bool + */ + public function isShared() + { + } + + public function isMounted() + { + } + + /** + * Get the mountpoint the file belongs to + * + * @return \OCP\Files\Mount\IMountPoint + */ + public function getMountPoint() + { + } + + /** + * Get the owner of the file + * + * @return ?IUser + */ + public function getOwner() + { + } + + /** + * @param IMountPoint[] $mounts + */ + public function setSubMounts(array $mounts) + { + } + + /** + * Add a cache entry which is the child of this folder + * + * Sets the size, etag and size to for cross-storage childs + * + * @param array|ICacheEntry $data cache entry for the child + * @param string $entryPath full path of the child entry + */ + public function addSubEntry($data, $entryPath) + { + } + + /** + * @inheritdoc + */ + public function getChecksum() + { + } + + public function getExtension(): string + { + } + + public function getCreationTime(): int + { + } + + public function getUploadTime(): int + { + } + + public function getParentId(): int + { + } + + /** + * @inheritDoc + * @return array + */ + public function getMetadata(): array + { + } +} diff --git a/tests/stubs/oc_files_storage_wrapper_encryption.php b/tests/stubs/oc_files_storage_wrapper_encryption.php new file mode 100644 index 000000000..e090e5286 --- /dev/null +++ b/tests/stubs/oc_files_storage_wrapper_encryption.php @@ -0,0 +1,230 @@ + */ +class WopiMapper extends QBMapper { + public function __construct(IDBConnection $db, private ISecureRandom $random, private LoggerInterface $logger, private ITimeFactory $timeFactory, private AppConfig $appConfig) + { + } + + /** + * @param int $fileId + * @param string $owner + * @param string $editor + * @param string $version + * @param bool $updatable + * @param string $serverHost + * @param string $guestDisplayname + * @param int $templateDestination + * @return Wopi + */ + public function generateFileToken($fileId, $owner, $editor, $version, $updatable, $serverHost, ?string $guestDisplayname = null, $hideDownload = false, $direct = false, $templateId = 0, $share = null) + { + } + + public function generateUserSettingsToken($fileId, $userId, $version, $serverHost) + { + } + + public function generateInitiatorToken($uid, $remoteServer) + { + } + + /** + * + * @deprecated + * @param $token + * @return Wopi + * @throws ExpiredTokenException + * @throws UnknownTokenException + */ + public function getPathForToken(#[\SensitiveParameter] + $token): Wopi + { + } + + /** + * Given a token, validates it and + * constructs and validates the path. + * Returns the path, if valid, else false. + * + * @param string $token + * @return Wopi + * @throws UnknownTokenException + * @throws ExpiredTokenException + */ + public function getWopiForToken(#[\SensitiveParameter] + string $token): Wopi + { + } + + /** + * @param int|null $limit + * @param int|null $offset + * @return int[] + * @throws \OCP\DB\Exception + */ + public function getExpiredTokenIds(?int $limit = null, ?int $offset = null): array + { + } +} diff --git a/tests/stubs/symfony_component_console_exception_invalidargumentexception.php b/tests/stubs/symfony_component_console_exception_invalidargumentexception.php new file mode 100644 index 000000000..07cc0b61d --- /dev/null +++ b/tests/stubs/symfony_component_console_exception_invalidargumentexception.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Jérôme Tamarelle + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/tests/stubs/symfony_component_console_helper_questionhelper.php b/tests/stubs/symfony_component_console_helper_questionhelper.php new file mode 100644 index 000000000..8dd76875e --- /dev/null +++ b/tests/stubs/symfony_component_console_helper_questionhelper.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Cursor; +use Symfony\Component\Console\Exception\MissingInputException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StreamableInputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Terminal; + +use function Symfony\Component\String\s; + +/** + * The QuestionHelper class provides helpers to interact with the user. + * + * @author Fabien Potencier + */ +class QuestionHelper extends Helper +{ + /** + * Asks a question to the user. + * + * @return mixed The user answer + * + * @throws RuntimeException If there is no data to read in the input stream + */ + public function ask(InputInterface $input, OutputInterface $output, Question $question): mixed + { + } + + public function getName(): string + { + } + + /** + * Prevents usage of stty. + * + * @return void + */ + public static function disableStty() + { + } + + /** + * Outputs the question prompt. + * + * @return void + */ + protected function writePrompt(OutputInterface $output, Question $question) + { + } + + /** + * @return string[] + */ + protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag): array + { + } + + /** + * Outputs an error message. + * + * @return void + */ + protected function writeError(OutputInterface $output, \Exception $error) + { + } +} diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json new file mode 100644 index 000000000..2c8976d51 --- /dev/null +++ b/vendor-bin/phpstan/composer.json @@ -0,0 +1,5 @@ +{ + "require-dev": { + "phpstan/phpstan": "^2.1" + } +} diff --git a/vendor-bin/phpstan/composer.lock b/vendor-bin/phpstan/composer.lock new file mode 100644 index 000000000..12795d0cc --- /dev/null +++ b/vendor-bin/phpstan/composer.lock @@ -0,0 +1,72 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "df9e69712d56aaef230c98a02cbe90f8", + "packages": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "2.1.38", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", + "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-01-30T17:12:46+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +}