diff --git a/lib/FilesHooks.php b/lib/FilesHooks.php index b073dd44e..62859f6a5 100644 --- a/lib/FilesHooks.php +++ b/lib/FilesHooks.php @@ -17,7 +17,11 @@ use OCP\Activity\IManager; use OCP\BackgroundJob\IJobList; use OCP\Constants; +use OCP\Files\Cache\IFileAccess; +use OCP\Files\Config\ICachedMountFileInfo; +use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Config\IUserMountCache; +use OCP\Files\Config\MountProviderArgs; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\Node; @@ -66,6 +70,8 @@ public function __construct( protected NotificationGenerator $notificationGenerator, protected ITagManager $tagManager, protected ?CirclesManager $teamManager, + private IFileAccess $fileAccess, + private IMountProviderCollection $mountProviderCollection, ) { } @@ -1273,18 +1279,9 @@ private function getAffectedUsersFromCachedMounts(int $fileId): array { $mountsForFile = $this->userMountCache->getMountsForFileId($fileId); foreach ($mountsForFile as $mount) { $affectedUsers[$mount->getUser()->getUID()] = $this->getVisiblePath($mount->getPath()); - $cachedMounts[] = [ - 'userId' => $mount->getUser()->getUID(), - 'provider' => str_replace('\\\\', '\\', $mount->getMountProvider()), - 'path' => $mount->getPath(), - 'visiblePath' => $this->getVisiblePath($mount->getPath()), - 'storageId' => $mount->getStorageId(), - 'internalPath' => $mount->getInternalPath(), - 'rootInternalPath' => $mount->getRootInternalPath(), - ]; } - $unrelatedUsers = $this->getUnrelatedUsers($cachedMounts); + $unrelatedUsers = $this->getBlockedGroupfoldersAclUsers($mountsForFile); return array_filter($affectedUsers, fn (string $userId): bool => !in_array($userId, $unrelatedUsers, true), ARRAY_FILTER_USE_KEY); } @@ -1292,10 +1289,11 @@ private function getAffectedUsersFromCachedMounts(int $fileId): array { /** * returns an array of users that have confirmed no access to fileId * + * @param ICachedMountFileInfo[] $cachedMounts * - * @return string[] list of unrelated userIds + * @return string[] list of blocked userIds */ - private function getUnrelatedUsers(array $cachedMounts): array { + private function getBlockedGroupfoldersAclUsers(array $cachedMounts): array { /** @var \OCA\GroupFolders\ACL\RuleManager $ruleManager */ /** @var \OCA\GroupFolders\Folder\FolderManager $folderManager */ try { @@ -1305,46 +1303,58 @@ private function getUnrelatedUsers(array $cachedMounts): array { return []; // if we have no access to RuleManager, we cannot filter unrelated users } - $groupFolderAclStatus = []; - /** @var \OCA\GroupFolders\ACL\Rule[] $rules */ $rules = $knownRules = $knownGroupRules = $usersToCheck = $cachedPath = []; + $groupfolderId = null; foreach ($cachedMounts as $cachedMount) { // we are only interested in filtering GroupFolders ACL - if ($cachedMount['provider'] !== 'OCA\GroupFolders\Mount\MountProvider') { + if ($cachedMount->getMountProvider() !== 'OCA\GroupFolders\Mount\MountProvider') { continue; } // caching rules based on storage id - $storageId = $cachedMount['storageId']; + $storageId = $cachedMount->getStorageId(); if (!array_key_exists($storageId, $knownRules)) { $knownRules[$storageId] = []; } - $cachedPath[$cachedMount['userId']] = $fullPath = $cachedMount['path']; + $cachedPath[$cachedMount->getUser()->getUID()] = $fullPath = $cachedMount->getPath(); // caching rules based on storage+path to file - if (!array_key_exists($cachedMount['visiblePath'], $knownRules[$storageId])) { + $visiblePath = $this->getVisiblePath($cachedMount->getPath()); + if (!array_key_exists($visiblePath, $knownRules[$storageId])) { // we need mountPoint and folderId to generate the correct path try { - // only check for groupfolders - if (!str_starts_with($cachedMount['rootInternalPath'], '__groupfolders')) { - continue; - } - - $folderId = (int)basename($cachedMount['rootInternalPath']); - if (!isset($groupFolderAclStatus[$folderId])) { - $groupFolderAclStatus[$folderId] = $folderManager->getFolderAclEnabled($folderId); - } - if (!$groupFolderAclStatus[$folderId]) { - continue; // acl are disable + if ($groupfolderId === null) { + $rootId = $cachedMount->getRootId(); + $rootMetadata = $this->fileAccess->getByFileId($rootId); + if (!$rootMetadata) { + continue; + } + $providerArgs = new MountProviderArgs($cachedMount, $rootMetadata); + /** @psalm-suppress UndefinedInterfaceMethod Could not find a public API for this */ + $mounts = $this->mountProviderCollection->getUserMountsFromProviderByPath( + $cachedMount->getMountProvider(), + '', + false, + [$providerArgs] + ); + $mount = reset($mounts); + if (!($mount instanceof \OCA\GroupFolders\Mount\GroupMountPoint)) { + continue; + } + $groupfolderId = $mount->getFolderId(); + if (!$folderManager->getFolderAclEnabled($groupfolderId)) { + // ACL are disabled for the groupfolder, we can stop here + break; + } } - $folderPath = '/' . $cachedMount['rootInternalPath']; - $path = $cachedMount['internalPath']; + $folderPath = '/' . $cachedMount->getRootInternalPath(); + $path = $cachedMount->getInternalPath(); } catch (\Exception) { // in case of issue during the process, we can imagine the user have no access to the file - $usersToCheck[] = $cachedMount['userId']; + $usersToCheck[] = $cachedMount->getUser()->getUID(); continue; // we'll catch rules on next user with access to the file } @@ -1352,7 +1362,7 @@ private function getUnrelatedUsers(array $cachedMounts): array { $paths = [$folderPath]; while ($path !== '') { $paths[] = $folderPath . '/' . $path; - $path = dirname((string)$path); + $path = dirname($path); if ($path === '.' || $path === '/') { $path = ''; } @@ -1373,7 +1383,7 @@ private function getUnrelatedUsers(array $cachedMounts): array { $rules = array_merge($rules, $rulesPerPath[$path]); } - $knownRules[$storageId][$cachedMount['visiblePath']] = true; + $knownRules[$storageId][$visiblePath] = true; } } @@ -1412,7 +1422,7 @@ private function getUnrelatedUsers(array $cachedMounts): array { // now that we have a list of eventuals filtered users, we confirm they have no access to the file $filteredUsers = []; foreach ($usersToCheck as $userId) { - if (!array_key_exists($userId, $cachedPath) || $cachedPath[$userId] === null) { + if (!array_key_exists($userId, $cachedPath)) { $this->logger->notice('could not find user in list of cached path', ['cachePath' => $cachedPath, 'usersToCheck' => $usersToCheck, 'current' => $userId]); $filteredUsers[] = $userId; continue; diff --git a/psalm.xml b/psalm.xml index 7882e8b5c..73f728fa0 100644 --- a/psalm.xml +++ b/psalm.xml @@ -25,13 +25,29 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/FilesHooksTest.php b/tests/FilesHooksTest.php index a90a70df4..570a22f95 100644 --- a/tests/FilesHooksTest.php +++ b/tests/FilesHooksTest.php @@ -33,6 +33,8 @@ use OCA\Files_Sharing\SharedMount; use OCP\Activity\IEvent; use OCP\Activity\IManager; +use OCP\Files\Cache\IFileAccess; +use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Config\IUserMountCache; use OCP\Files\File; use OCP\Files\Folder; @@ -77,6 +79,8 @@ class FilesHooksTest extends TestCase { protected TagManager&MockObject $tagManager; protected Tags&MockObject $tags; protected (OCA\Circles\CirclesManager&MockObject)|null $teamManager = null; + private IFileAccess&MockObject $fileAccess; + private IMountProviderCollection&MockObject $mountProviderCollection; protected function setUp(): void { parent::setUp(); @@ -98,6 +102,8 @@ protected function setUp(): void { ->willReturn([]); $this->tagManager->method('load') ->willReturn($this->tags); + $this->fileAccess = $this->createMock(IFileAccess::class); + $this->mountProviderCollection = $this->createMock(IMountProviderCollection::class); $this->filesHooks = $this->getFilesHooks(); } @@ -131,6 +137,8 @@ protected function getFilesHooks(array $mockedMethods = [], ?string $user = 'use $this->notificationGenerator, $this->tagManager, $this->teamManager, + $this->fileAccess, + $this->mountProviderCollection, ]) ->onlyMethods($mockedMethods) ->getMock(); @@ -152,6 +160,8 @@ protected function getFilesHooks(array $mockedMethods = [], ?string $user = 'use $this->notificationGenerator, $this->tagManager, $this->teamManager, + $this->fileAccess, + $this->mountProviderCollection, ); } @@ -199,6 +209,8 @@ public function testFileCreate(string $currentUser, bool $isPublicShare, string $this->notificationGenerator, $this->tagManager, $this->teamManager, + $this->fileAccess, + $this->mountProviderCollection, ]) ->onlyMethods(['addNotificationsForFileAction']) ->getMock(); diff --git a/tests/stubs/oc_core_command_base.php b/tests/stubs/oc_core_command_base.php index c062e2fc7..504d82339 100644 --- a/tests/stubs/oc_core_command_base.php +++ b/tests/stubs/oc_core_command_base.php @@ -1,49 +1,89 @@ */ + protected string $class; + protected ?string $storageId = null; + protected ?int $numericStorageId = null; + protected ?int $rootId = null; + + /** + * Configuration options for the storage backend + */ + protected array $arguments = []; + protected string $mountPoint; + + /** + * Mount specific options + */ + protected array $mountOptions = []; + protected string $mountProvider; + + /** + * @param IStorage|class-string $storage + * @param array $arguments (optional) configuration for the storage backend + * @param ?array $mountOptions mount specific options + * @param ?int $mountId + * @param ?string $mountProvider + * @throws \Exception + */ + public function __construct( + string|IStorage $storage, + string $mountpoint, + ?array $arguments = null, + ?IStorageFactory $loader = null, + ?array $mountOptions = null, + protected ?int $mountId = null, + ?string $mountProvider = null, + ) { + } + + /** + * get complete path to the mount point, relative to data/ + * + * @return string + */ + #[\Override] + public function getMountPoint() { + } + + /** + * Sets the mount point path, relative to data/ + * + * @param string $mountPoint new mount point + */ + #[\Override] + public function setMountPoint($mountPoint) { + } + + /** + * @return IStorage|null + */ + #[\Override] + public function getStorage() { + } + + /** + * @return string|null + */ + #[\Override] + public function getStorageId() { + } + + /** + * @return int + */ + #[\Override] + public function getNumericStorageId() { + } + + /** + * @param string $path + * @return string + */ + #[\Override] + public function getInternalPath($path) { + } + + /** + * @param callable $wrapper + */ + #[\Override] + public function wrapStorage($wrapper) { + } + + /** + * Get a mount option + * + * @param string $name Name of the mount option to get + * @param mixed $default Default value for the mount option + * @return mixed + */ + #[\Override] + public function getOption($name, $default) { + } + + /** + * Get all options for the mount + * + * @return array + */ + #[\Override] + public function getOptions() { + } + + /** + * Get the file id of the root of the storage + * + * @return int + */ + #[\Override] + public function getStorageRootId() { + } + + #[\Override] + public function getMountId() { + } + + #[\Override] + public function getMountType() { + } + + #[\Override] + public function getMountProvider(): string { + } +} diff --git a/tests/stubs/oc_files_storage_common.php b/tests/stubs/oc_files_storage_common.php new file mode 100644 index 000000000..afd4f59fd --- /dev/null +++ b/tests/stubs/oc_files_storage_common.php @@ -0,0 +1,288 @@ +stat() . + */ +abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage, IConstructableStorage { + use LocalTempFileTrait; + + protected ?Cache $cache = null; + protected ?Scanner $scanner = null; + protected ?Watcher $watcher = null; + protected ?Propagator $propagator = null; + protected $storageCache; + protected ?Updater $updater = null; + + protected array $mountOptions = []; + protected $owner = null; + + public function __construct(array $parameters) { + } + + protected function remove(string $path): bool { + } + + #[\Override] + public function is_dir(string $path): bool { + } + + #[\Override] + public function is_file(string $path): bool { + } + + #[\Override] + public function filesize(string $path): int|float|false { + } + + #[\Override] + public function isReadable(string $path): bool { + } + + #[\Override] + public function isUpdatable(string $path): bool { + } + + #[\Override] + public function isCreatable(string $path): bool { + } + + #[\Override] + public function isDeletable(string $path): bool { + } + + #[\Override] + public function isSharable(string $path): bool { + } + + #[\Override] + public function getPermissions(string $path): int { + } + + #[\Override] + public function filemtime(string $path): int|false { + } + + #[\Override] + public function file_get_contents(string $path): string|false { + } + + #[\Override] + public function file_put_contents(string $path, mixed $data): int|float|false { + } + + #[\Override] + public function rename(string $source, string $target): bool { + } + + #[\Override] + public function copy(string $source, string $target): bool { + } + + #[\Override] + public function getMimeType(string $path): string|false { + } + + #[\Override] + public function hash(string $type, string $path, bool $raw = false): string|false { + } + + #[\Override] + public function getLocalFile(string $path): string|false { + } + + protected function searchInDir(string $query, string $dir = ''): array { + } + + /** + * @inheritDoc + * Check if a file or folder has been updated since $time + * + * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking + * the mtime should always return false here. As a result storage implementations that always return false expect + * exclusive access to the backend and will not pick up files that have been added in a way that circumvents + * Nextcloud filesystem. + */ + #[\Override] + public function hasUpdated(string $path, int $time): bool { + } + + protected function getCacheDependencies(): CacheDependencies { + } + + #[\Override] + public function getCache(string $path = '', ?IStorage $storage = null): ICache { + } + + #[\Override] + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { + } + + #[\Override] + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { + } + + #[\Override] + public function getPropagator(?IStorage $storage = null): IPropagator { + } + + #[\Override] + public function getUpdater(?IStorage $storage = null): IUpdater { + } + + #[\Override] + public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage { + } + + #[\Override] + public function getOwner(string $path): string|false { + } + + #[\Override] + public function getETag(string $path): string|false { + } + + /** + * clean a path, i.e. remove all redundant '.' and '..' + * making sure that it can't point to higher than '/' + * + * @param string $path The path to clean + * @return string cleaned path + */ + public function cleanPath(string $path): string { + } + + /** + * Test a storage for availability + */ + #[\Override] + public function test(): bool { + } + + #[\Override] + public function free_space(string $path): int|float|false { + } + + #[\Override] + public function isLocal(): bool { + } + + /** + * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class + */ + #[\Override] + public function instanceOfStorage(string $class): bool { + } + + #[Override] + public function getDirectDownload(string $path): array|false { + } + + #[Override] + public function getDirectDownloadById(string $fileId): array|false { + } + + #[\Override] + public function verifyPath(string $path, string $fileName): void { + } + + /** + * Get the filename validator + * (cached for performance) + */ + protected function getFilenameValidator(): IFilenameValidator { + } + + public function setMountOptions(array $options): void { + } + + public function getMountOption(string $name, mixed $default = null): mixed { + } + + #[\Override] + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool { + } + + #[\Override] + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + } + + #[\Override] + public function getMetaData(string $path): ?array { + } + + #[\Override] + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { + } + + #[\Override] + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { + } + + #[\Override] + public function changeLock(string $path, int $type, ILockingProvider $provider): void { + } + + /** + * @return array{available: bool, last_checked: int} + */ + #[\Override] + public function getAvailability(): array { + } + + #[\Override] + public function setAvailability(bool $isAvailable): void { + } + + #[\Override] + public function setOwner(?string $user): void { + } + + #[\Override] + public function needsPartFile(): bool { + } + + #[\Override] + public function writeStream(string $path, $stream, ?int $size = null): int { + } + + #[\Override] + public function getDirectoryContent(string $directory): \Traversable { + } +} diff --git a/tests/stubs/oc_files_storage_dav.php b/tests/stubs/oc_files_storage_dav.php new file mode 100644 index 000000000..926966602 --- /dev/null +++ b/tests/stubs/oc_files_storage_dav.php @@ -0,0 +1,396 @@ +applyBearerToken((string)$settings['bearerToken']); + } + } + + /** + * Register a callback invoked when a request comes back with HTTP 401. The + * callback should return a fresh bearer token, or null to give up. When a + * non-empty token is returned the failing request is replayed once. + * + * @param (callable(): ?string)|null $callback + */ + public function setRefreshTokenCallback(?callable $callback): void { + $this->refreshTokenCallback = $callback === null ? null : \Closure::fromCallable($callback); + } + + #[\Override] + public function send(RequestInterface $request): SabreResponseInterface { + try { + return parent::send($request); + } catch (ClientHttpException $e) { + if ($e->getHttpStatus() !== 401 || $this->retrying || $this->refreshTokenCallback === null) { + throw $e; + } + $this->retrying = true; + try { + $newToken = ($this->refreshTokenCallback)(); + if (!is_string($newToken) || $newToken === '') { + throw $e; + } + $this->applyBearerToken($newToken); + return parent::send($request); + } finally { + $this->retrying = false; + } + } + } + + private function applyBearerToken(string $token): void { + /** @psalm-suppress InvalidArrayOffset */ + $curlType = $this->curlSettings[CURLOPT_HTTPAUTH] ?? 0; + $curlType |= CURLAUTH_BEARER; + $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType); + $this->addCurlSetting(CURLOPT_XOAUTH2_BEARER, $token); + } +} + +/** + * Class DAV + * + * @package OC\Files\Storage + */ +class DAV extends Common { + /** @var string */ + protected $password; + /** @var string */ + protected $user; + protected ?int $authType = null; + /** @var string */ + protected $host; + /** @var bool */ + protected $secure; + protected bool $verify; + /** @var string */ + protected $root; + /** @var string */ + protected $certPath; + /** @var bool */ + protected $ready; + /** The resolved bearer token for AUTH_BEARER (access token or exchanged token) */ + protected string $bearerToken = ''; + /** @var Client */ + protected $client; + /** @var ArrayCache */ + protected $statCache; + /** @var IClientService */ + protected $httpClientService; + /** @var ICertificateManager */ + protected $certManager; + protected LoggerInterface $logger; + protected IEventLogger $eventLogger; + protected IMimeTypeDetector $mimeTypeDetector; + protected IOCMDiscoveryService $discoveryService; + protected ISignatureManager $signatureManager; + protected OCMSignatoryManager $signatoryManager; + protected IAppConfig $appConfig; + protected IURLGenerator $urlGenerator; + + protected const PROPFIND_PROPS = [ + '{DAV:}getlastmodified', + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{http://owncloud.org/ns}permissions', + '{http://open-collaboration-services.org/ns}share-permissions', + '{http://open-cloud-mesh.org/ns}share-permissions', + '{DAV:}resourcetype', + '{DAV:}getetag', + '{DAV:}quota-available-bytes', + ]; + + /** + * @param array $parameters + * @throws \Exception + */ + public function __construct(array $parameters) { + } + + protected function init(): void { + } + + /** + * Exchange refresh token for access token via the remote server's token endpoint + * + * @return string The access token + * @throws StorageNotAvailableException If token exchange fails + */ + protected function exchangeRefreshToken(): string { + } + + /** + * Check if bearer authentication is being used + */ + protected function isBearerAuth(): bool { + } + + /** + * Wrap a Guzzle-based operation with retry-on-401 using the bearer-token + * refresh. Sabre-based operations don't need this — {@see BearerAuthAwareSabreClient} + * handles 401 transparently on its own. + * + * @template T + * @param callable(): T $operation + * @return T + * @throws \GuzzleHttp\Exception\ClientException + */ + protected function withAuthRetry(callable $operation): mixed { + } + + /** + * Exchange the long-lived refresh token for a new short-lived access token + * and update {@see $bearerToken} (and the stored password so subsequent + * init() calls reuse the same token). Used as the refresh callback for the + * Sabre client and by {@see withAuthRetry} for the Guzzle paths. + * + * @return string|null new access token, or null if the exchange failed + */ + protected function refreshAccessToken(): ?string { + } + + /** + * Clear the stat cache + */ + public function clearStatCache(): void { + } + + #[\Override] + public function getId(): string { + } + + public function createBaseUri(): string { + } + + #[\Override] + public function mkdir(string $path): bool { + } + + #[\Override] + public function rmdir(string $path): bool { + } + + #[\Override] + public function opendir(string $path) { + } + + /** + * @return array + */ + protected function getPropfindProperties(): array { + } + + /** + * Get property value from cached PROPFIND response. + * For accessing app-specific properties not included in getMetaData(). + * + * @param string $path + * @param string $propertyName + * @return mixed + */ + public function getPropfindPropertyValue(string $path, string $propertyName): mixed { + } + + /** + * Propfind call with cache handling. + * + * First checks if information is cached. + * If not, request it from the server then store to cache. + * + * @param string $path path to propfind + * + * @return array|false propfind response or false if the entry was not found + * + * @throws ClientHttpException + */ + protected function propfind(string $path): array|false { + } + + #[\Override] + public function filetype(string $path): string|false { + } + + #[\Override] + public function file_exists(string $path): bool { + } + + #[\Override] + public function unlink(string $path): bool { + } + + #[\Override] + public function fopen(string $path, string $mode) { + } + + public function writeBack(string $tmpFile, string $path): void { + } + + #[\Override] + public function free_space(string $path): int|float|false { + } + + #[\Override] + public function touch(string $path, ?int $mtime = null): bool { + } + + #[\Override] + public function file_put_contents(string $path, mixed $data): int|float|false { + } + + protected function uploadFile(string $path, string $target): void { + } + + #[\Override] + public function rename(string $source, string $target): bool { + } + + #[\Override] + public function copy(string $source, string $target): bool { + } + + #[\Override] + public function getMetaData(string $path): ?array { + } + + #[\Override] + public function stat(string $path): array|false { + } + + #[\Override] + public function getMimeType(string $path): string|false { + } + + #[\Override] + public function cleanPath(string $path): string { + } + + /** + * URL encodes the given path but keeps the slashes + * + * @param string $path to encode + * @return string encoded path + */ + protected function encodePath(string $path): string { + } + + /** + * @return bool + * @throws StorageInvalidException + * @throws StorageNotAvailableException + */ + protected function simpleResponse(string $method, string $path, ?string $body, int $expected): bool { + } + + /** + * check if curl is installed + */ + public static function checkDependencies(): bool { + } + + #[\Override] + public function isUpdatable(string $path): bool { + } + + #[\Override] + public function isCreatable(string $path): bool { + } + + #[\Override] + public function isSharable(string $path): bool { + } + + #[\Override] + public function isDeletable(string $path): bool { + } + + #[\Override] + public function getPermissions(string $path): int { + } + + #[\Override] + public function getETag(string $path): string|false { + } + + protected function parsePermissions(string $permissionsString): int { + } + + #[\Override] + public function hasUpdated(string $path, int $time): bool { + } + + /** + * Interpret the given exception and decide whether it is due to an + * unavailable storage, invalid storage or other. + * This will either throw StorageInvalidException, StorageNotAvailableException + * or do nothing. + * + * @param Exception $e sabre exception + * @param string $path optional path from the operation + * + * @throws StorageInvalidException if the storage is invalid, for example + * when the authentication expired or is invalid + * @throws StorageNotAvailableException if the storage is not available, + * which might be temporary + * @throws ForbiddenException if the action is not allowed + */ + protected function convertException(Exception $e, string $path = ''): void { + } + + #[\Override] + public function getDirectoryContent(string $directory): \Traversable { + } +} diff --git a/tests/stubs/oc_files_storage_storage.php b/tests/stubs/oc_files_storage_storage.php new file mode 100644 index 000000000..d53b1140c --- /dev/null +++ b/tests/stubs/oc_files_storage_storage.php @@ -0,0 +1,65 @@ + + */ + public function getMetaData(string $path): ?array; + + /** + * Get the contents of a directory with metadata + * + * The metadata array will contain the following fields + * + * - name + * - mimetype + * - mtime + * - size + * - etag + * - storage_mtime + * - permissions + * + * @return \Traversable> + */ + public function getDirectoryContent(string $directory): \Traversable; +} diff --git a/tests/stubs/oc_files_storage_wrapper_wrapper.php b/tests/stubs/oc_files_storage_wrapper_wrapper.php new file mode 100644 index 000000000..d4148178c --- /dev/null +++ b/tests/stubs/oc_files_storage_wrapper_wrapper.php @@ -0,0 +1,291 @@ + $class + * @psalm-return T|null + */ + public function getInstanceOfStorage(string $class): ?IStorage { + } + + /** + * Pass any methods custom to specific storage implementations to the wrapped storage + * + * @return mixed + */ + public function __call(string $method, array $args) { + } + + #[Override] + public function getDirectDownload(string $path): array|false { + } + + #[Override] + public function getDirectDownloadById(string $fileId): array|false { + } + + #[\Override] + public function getAvailability(): array { + } + + #[\Override] + public function setAvailability(bool $isAvailable): void { + } + + #[\Override] + public function verifyPath(string $path, string $fileName): void { + } + + #[\Override] + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + } + + #[\Override] + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + } + + #[\Override] + public function getMetaData(string $path): ?array { + } + + #[\Override] + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { + } + + #[\Override] + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { + } + + #[\Override] + public function changeLock(string $path, int $type, ILockingProvider $provider): void { + } + + #[\Override] + public function needsPartFile(): bool { + } + + #[\Override] + public function writeStream(string $path, $stream, ?int $size = null): int { + } + + /** + * @param resource $stream + */ + protected function writeStreamFallback(string $path, $stream): int { + } + + #[\Override] + public function getDirectoryContent(string $directory): \Traversable { + } + + public function isWrapperOf(IStorage $storage): bool { + } + + #[\Override] + public function setOwner(?string $user): void { + } +} diff --git a/tests/stubs/oca_files_event.php b/tests/stubs/oca_files_event.php deleted file mode 100644 index 18c0503e3..000000000 --- a/tests/stubs/oca_files_event.php +++ /dev/null @@ -1,6 +0,0 @@ - - */ - public function getAllRulesForPaths(int $storageId, array $filePaths): array { - } - } -} - -namespace OCA\GroupFolders\Folder { - class FolderManager { - public function getFolderAclEnabled(int $id): bool { - } - } -} diff --git a/tests/stubs/oca_groupfolders_acl_rule.php b/tests/stubs/oca_groupfolders_acl_rule.php new file mode 100644 index 000000000..64f333ae8 --- /dev/null +++ b/tests/stubs/oca_groupfolders_acl_rule.php @@ -0,0 +1,123 @@ + Constants::PERMISSION_READ, + 'write' => Constants::PERMISSION_UPDATE, + 'create' => Constants::PERMISSION_CREATE, + 'delete' => Constants::PERMISSION_DELETE, + 'share' => Constants::PERMISSION_SHARE, + ]; + + /** + * @param int $mask for every permission type a rule can either allow, deny or inherit + * these 3 values are stored as 2 bitmaps, one that masks out all inherit values (1 -> set permission, 0 -> inherit) + * and one that specifies the permissions to set for non inherited values (1-> allow, 0 -> deny) + */ + public function __construct( + private readonly IUserMapping $userMapping, + private readonly int $fileId, + private int $mask, + int $permissions, + ) { + } + + public function getUserMapping(): IUserMapping { + } + + public function getFileId(): int { + } + + public function getMask(): int { + } + + public function getPermissions(): int { + } + + /** + * Apply this rule to an existing permission set, returning the resulting permissions + * + * All permissions included in the current mask will overwrite the existing permissions + */ + public function applyPermissions(int $permissions): int { + } + + /** + * Apply the deny permissions this rule to an existing permission set, returning the resulting permissions + * + * Only the deny permissions included in the current mask will overwrite the existing permissions + */ + public function applyDenyPermissions(int $permissions): int { + } + + #[\Override] + public function xmlSerialize(Writer $writer): void { + } + + /** + * @return array{ + * mapping: array{ + * type: 'user'|'group'|'dummy'|'circle', + * id: string, + * }, + * mask: int, + * permissions: int, + * } + */ + #[\Override] + public function jsonSerialize(): array { + } + + #[\Override] + public static function xmlDeserialize(Reader $reader): Rule { + } + + /** + * merge multiple rules that apply on the same file where allow overwrites deny + * @param Rule[] $rules + */ + public static function mergeRules(array $rules): Rule { + } + + /** + * apply a new rule on top of the existing + * + * All non-inherit fields of the new rule will overwrite the current permissions + */ + public function applyRule(Rule $rule): void { + } + + /** + * Create a default, no-op rule + */ + public static function defaultRule(): Rule { + } + + public static function formatRulePermissions(int $mask, int $permissions): string { + } + + public function formatPermissions(): string { + } +} diff --git a/tests/stubs/oca_groupfolders_acl_rulemanager.php b/tests/stubs/oca_groupfolders_acl_rulemanager.php new file mode 100644 index 000000000..cbb8db56c --- /dev/null +++ b/tests/stubs/oca_groupfolders_acl_rulemanager.php @@ -0,0 +1,75 @@ + + */ + public function getRulesForFilesById(IUser $user, array $fileIds): array { + } + + /** + * @param string[] $filePaths + * @return array + */ + public function getRulesForFilesByPath(IUser $user, int $storageId, array $filePaths): array { + } + + /** + * @param int[] $fileIds + * @return array> + */ + public function getRulesForFilesByIds(IUser $user, array $fileIds): array { + } + + /** + * @return array + */ + public function getRulesForFilesByParent(IUser $user, int $storageId, int $parentId): array { + } + + /** + * @param string[] $filePaths + * @return array + */ + public function getAllRulesForPaths(int $storageId, array $filePaths): array { + } + + /** + * @return array + */ + public function getAllRulesForPrefix(int $storageId, string $prefix): array { + } + + /** + * @return array + */ + public function getRulesForPrefix(IUser $user, int $storageId, string $prefix): array { + } + + public function saveRule(Rule $rule): void { + } + + public function deleteRule(Rule $rule): void { + } +} diff --git a/tests/stubs/oca_groupfolders_acl_usermapping_iusermapping.php b/tests/stubs/oca_groupfolders_acl_usermapping_iusermapping.php new file mode 100644 index 000000000..bf5edd1b3 --- /dev/null +++ b/tests/stubs/oca_groupfolders_acl_usermapping_iusermapping.php @@ -0,0 +1,20 @@ + + * @throws Exception + */ + public function getAllFolders(): array { + } + + /** + * @return array + * @throws Exception + */ + public function getAllFoldersWithSize(int $offset = 0, ?int $limit = null, string $orderBy = 'mount_point', \SortDirection $order = \SortDirection::Ascending, ?string $mountPoint = null): array { + } + + /** + * @return array + * @throws Exception + */ + public function getAllFoldersForUserWithSize(IUser $user): array { + } + + public function getFolder(int $id): ?FolderWithMappingsAndCache { + } + + /** + * Return just the ACL for the folder. + * + * @throws Exception + */ + public function getFolderAclEnabled(int $id): bool { + } + + public function getFolderByPath(string $path): int { + } + + /** + * Check if the user is able to configure the advanced folder permissions. This + * is the case if the user is an admin, has admin permissions for the group folder + * app or is member of a group that can manage permissions for the specific folder. + * + * @throws Exception + */ + public function canManageACL(int $folderId, IUser $user, bool $excludeAdmins = false): bool { + } + + public function mountPointExists(string $mountPoint): bool { + } + + /** + * @return list + * @throws Exception + */ + public function searchGroups(int $id, string $search = ''): array { + } + + /** + * @return list + * @throws Exception + */ + public function searchCircles(int $id, string $search = ''): array { + } + + /** + * @return list + * @throws Exception + */ + public function searchUsers(int $id, string $search = '', int $limit = 10, int $offset = 0): array { + } + + /** + * @param string[] $groupIds + * @param list $paths + * @return list + * @throws Exception + */ + public function getFoldersForGroups(array $groupIds, ?int $folderId = null, ?array $paths = null): array { + } + + /** + * @throws \InvalidArgumentException + * @throws Exception + */ + public function hasFolderForGroup(string $groupId): bool { + } + + /** + * @throws \InvalidArgumentException + * @throws Exception + */ + public function hasFolderForCircle(string $circleId): bool { + } + + /** + * Return all group folders directly assigned to or owned by a circle. + * + * @return list + * @throws Exception + */ + public function getFoldersForCircle(string $circleId): array { + } + + /** + * @param list $paths + * @return list + * @throws Exception + */ + public function getFoldersFromCircleMemberships(IUser $user, ?int $folderId = null, ?array $paths = null): array { + } + + 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 { + } + + /** + * @throws Exception + */ + public function addApplicableGroup(int $folderId, string $groupId): void { + } + + /** + * @throws Exception + */ + public function removeApplicableGroup(int $folderId, string $groupId): void { + } + + /** + * @throws Exception + */ + public function setGroupPermissions(int $folderId, string $groupId, int $permissions): void { + } + + /** + * @throws Exception + */ + public function setManageACL(int $folderId, string $type, string $id, bool $manageAcl): void { + } + + /** + * @throws Exception + */ + public function removeFolder(int $folderId): void { + } + + /** + * @throws Exception + */ + public function setFolderQuota(int $folderId, int $quota): void { + } + + /** + * Update the JSON-encoded options of a folder. + * + * @param int $folderId + * @param array{separate-storage?: bool} $options + * @throws Exception + */ + public function setFolderOptions(int $folderId, array $options): void { + } + + /** + * @throws Exception + */ + public function renameFolder(int $folderId, string $newMountPoint): void { + } + + /** + * @throws Exception + */ + public function deleteGroup(string $groupId): void { + } + + /** + * @throws Exception + */ + public function deleteUser(string $userId): void { + } + + /** + * @throws Exception + */ + public function deleteCircle(string $circleId): void { + } + + /** + * Look up the team folder that belongs to the given team (circle single id) + * via the `team_circle_id` column. + * + * This is the single source of truth for the "folder belongs to team" + * relationship. The legacy `group_folders_groups.circle_id` column tracks + * applicable-group membership (a separate concern) and must not be used to + * determine team ownership. + * + * @return int|null The folder id, or null if no folder belongs to this team. + */ + public function getFolderIdByTeamCircleId(string $circleId): ?int { + } + + /** + * Mark a team folder as belonging to a team by setting the `team_circle_id` + * column. + */ + public function setTeamCircleId(int $folderId, string $circleId): void { + } + + /** + * Whether the folder is assigned exclusively to the given circle. A folder + * must have precisely one applicable mapping, and that mapping must be the + * target team; group mappings and additional teams make it ineligible. + * + * @throws Exception + */ + public function isExclusivelyAssignedToCircle(int $folderId, string $circleId): bool { + } + + /** + * Clear the team ownership of a team folder by resetting the + * `team_circle_id` column to null. + */ + public function clearTeamCircleId(int $folderId): void { + } + + /** + * @throws Exception + */ + public function setFolderACL(int $folderId, bool $acl): void { + } + + /** + * @param list $paths + * @return list + * @throws Exception + */ + public function getFoldersForUser(IUser $user, ?int $folderId = null, ?array $paths = null): array { + } + + /** + * @throws Exception + */ + public function getFolderPermissionsForUser(IUser $user, int $folderId): int { + } + + /** + * returns if the groupId is in fact the singleId of an existing Circle + */ + public function isACircle(string $groupId): bool { + } + + /** + * returns the Circle from its single Id, or NULL if not available + */ + public function getCircle(string $groupId): ?Circle { + } + + public function getCirclesManager(): ?CirclesManager { + } + + public function updateOverwriteHomeFolders(): void { + } + + public function hasFolderACLDefaultNoPermission(int $folderId): bool { + } + + /** + * Seed the cache from an already-loaded folder so bulk callers (the mount + * provider) skip one `getBasePermission()` lookup per folder. + */ + public function primeAclDefaultNoPermission(FolderDefinition $folder): void { + } + + public function countAllFolders(): int { + } +} diff --git a/tests/stubs/oca_groupfolders_mount_groupmountpoint.php b/tests/stubs/oca_groupfolders_mount_groupmountpoint.php new file mode 100644 index 000000000..631777799 --- /dev/null +++ b/tests/stubs/oca_groupfolders_mount_groupmountpoint.php @@ -0,0 +1,47 @@ + $arguments + * @param ?array $mountOptions + */ + public function __construct( + private readonly FolderDefinition $folder, + IStorage $storage, + string $mountpoint, + ?array $arguments = null, + ?IStorageFactory $loader = null, + ?array $mountOptions = null, + ?int $mountId = null, + ?int $rootId = null, + ) { + } + + #[\Override] + public function getMountType(): string { + } + + public function getFolderId(): int { + } + + public function getFolder(): FolderDefinition { + } + + public function getSourcePath(): string { + } +} diff --git a/tests/stubs/oca_theming_defaults.php b/tests/stubs/oca_theming_defaults.php deleted file mode 100644 index f3f4c5e6b..000000000 --- a/tests/stubs/oca_theming_defaults.php +++ /dev/null @@ -1,21 +0,0 @@ -next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Reader $reader); +} diff --git a/tests/stubs/sabre_xml_xmlserializable.php b/tests/stubs/sabre_xml_xmlserializable.php new file mode 100644 index 000000000..fef237ffe --- /dev/null +++ b/tests/stubs/sabre_xml_xmlserializable.php @@ -0,0 +1,33 @@ +commandLine + * + * Bash equivalent: COMP_POINT + * + * @var int + */ + protected $charIndex = 0; + + /** + * An array of the individual words in the current command line. + * + * This is not set until $this->splitCommand() is called, when it is populated by + * $commandLine exploded by $wordBreaks + * + * Bash equivalent: COMP_WORDS + * + * @var string[]|null + */ + protected $words = null; + + /** + * Words from the currently command-line before quotes and escaping is processed + * + * This is indexed the same as $this->words, but in their raw input terms are in their input form, including + * quotes and escaping. + * + * @var string[]|null + */ + protected $rawWords = null; + + /** + * The index in $this->words containing the word at the current cursor position. + * + * This is not set until $this->splitCommand() is called. + * + * Bash equivalent: COMP_CWORD + * + * @var int|null + */ + protected $wordIndex = null; + + /** + * Characters that $this->commandLine should be split on to get a list of individual words + * + * Bash equivalent: COMP_WORDBREAKS + * + * @var string + */ + protected $wordBreaks = "= \t\n"; + + /** + * Set the whole contents of the command line as a string + * + * @param string $commandLine + */ + public function setCommandLine($commandLine) { + } + + /** + * Return the current command line verbatim as a string + * + * @return string + */ + public function getCommandLine() { + } + + /** + * Return the word from the command line that the cursor is currently in + * + * Most of the time this will be a partial word. If the cursor has a space before it, + * this will return an empty string, indicating a new word. + * + * @return string + */ + public function getCurrentWord() { + } + + /** + * Return the unprocessed string for the word under the cursor + * + * This preserves any quotes and escaping that are present in the input command line. + * + * @return string + */ + public function getRawCurrentWord() { + } + + /** + * Return a word by index from the command line + * + * @see $words, $wordBreaks + * @param int $index + * @return string + */ + public function getWordAtIndex($index) { + } + + /** + * Get the contents of the command line, exploded into words based on the configured word break characters + * + * @see $wordBreaks, setWordBreaks + * @return array + */ + public function getWords() { + } + + /** + * Get the unprocessed/literal words from the command line + * + * This is indexed the same as getWords(), but preserves any quoting and escaping from the command line + * + * @return string[] + */ + public function getRawWords() { + } + + /** + * Get the index of the word the cursor is currently in + * + * @see getWords, getCurrentWord + * @return int + */ + public function getWordIndex() { + } + + /** + * Get the character index of the user's cursor on the command line + * + * This is in the context of the full command line string, so includes word break characters. + * Note that some shells can only provide an approximation for character index. Under ZSH for + * example, this will always be the character at the start of the current word. + * + * @return int + */ + public function getCharIndex() { + } + + /** + * Set the cursor position as a character index relative to the start of the command line + * + * @param int $index + */ + public function setCharIndex($index) { + } + + /** + * Set characters to use as split points when breaking the command line into words + * + * This defaults to a sane value based on BASH's word break characters and shouldn't + * need to be changed unless your completions contain the default word break characters. + * + * @deprecated This is becoming an internal setting that doesn't make sense to expose publicly. + * + * @see wordBreaks + * @param string $charList - a single string containing all of the characters to break words on + */ + public function setWordBreaks($charList) { + } + + /** + * Split the command line into words using the configured word break characters + * + * @return string[] + */ + protected function splitCommand() { + } + + /** + * Return a token's value with escaping and quotes removed + * + * @see self::tokenizeString() + * @param array $token + * @return string + */ + protected function getTokenValue($token) { + } + + /** + * Break a string into words, quoted strings and non-words (breaks) + * + * Returns an array of unmodified segments of $string with offset and type information. + * + * @param string $string + * @return array as [ [type => string, value => string, offset => int], ... ] + */ + protected function tokenizeString($string) { + } + + /** + * Reset the computed words so that $this->splitWords is forced to run again + */ + protected function reset() { + } +}