refactor(dav): Modernize Node

Signed-off-by: provokateurin <kate@provokateurin.de>
This commit is contained in:
provokateurin 2026-02-12 12:05:11 +01:00
parent bf71461bee
commit e40d6b1c6b
No known key found for this signature in database
13 changed files with 182 additions and 163 deletions

View file

@ -166,11 +166,6 @@ class FilesPlugin extends ServerPlugin {
return;
}
// Ensure source exists
$sourceNodeFileInfo = $sourceNode->getFileInfo();
if ($sourceNodeFileInfo === null) {
throw new NotFound($source . ' does not exist');
}
// Ensure the target name is valid
try {
[$targetPath, $targetName] = \Sabre\Uri\split($target);

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@ -20,42 +22,48 @@ use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\ISharedStorage;
use OCP\Files\StorageNotAvailableException;
use OCP\IUser;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\PreConditionNotMetException;
use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use RuntimeException;
use Sabre\DAV\Exception;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\INode;
abstract class Node implements \Sabre\DAV\INode {
abstract class Node implements INode {
/**
* The path to the current node
*
* @var string
*/
protected $path;
protected string $path;
protected FileInfo $info;
/**
* @var IManager
*/
protected $shareManager;
protected IManager $shareManager;
protected \OCP\Files\Node $node;
/**
* Sets up the node, expects a full path name
* @throws PreConditionNotMetException
*/
public function __construct(
protected View $fileView,
FileInfo $info,
?IManager $shareManager = null,
) {
$this->path = $this->fileView->getRelativePath($info->getPath());
$this->info = $info;
if ($shareManager) {
$this->shareManager = $shareManager;
} else {
$this->shareManager = Server::get(\OCP\Share\IManager::class);
$relativePath = $this->fileView->getRelativePath($info->getPath());
if ($relativePath === null) {
throw new RuntimeException('Failed to get relative path for ' . $info->getPath());
}
$this->path = $relativePath;
$this->info = $info;
$this->shareManager = $shareManager instanceof IManager ? $shareManager : Server::get(IManager::class);
if ($info instanceof Folder || $info instanceof File) {
$this->node = $info;
} else {
@ -70,11 +78,16 @@ abstract class Node implements \Sabre\DAV\INode {
}
}
/**
* @throws Exception
* @throws PreConditionNotMetException
*/
protected function refreshInfo(): void {
$info = $this->fileView->getFileInfo($this->path);
if ($info === false) {
throw new \Sabre\DAV\Exception('Failed to get fileinfo for ' . $this->path);
throw new Exception('Failed to get fileinfo for ' . $this->path);
}
$this->info = $info;
$root = Server::get(IRootFolder::class);
$rootView = Server::get(View::class);
@ -87,19 +100,15 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Returns the name of the node
*
* @return string
*/
public function getName() {
public function getName(): string {
return $this->info->getName();
}
/**
* Returns the full path
*
* @return string
*/
public function getPath() {
public function getPath(): string {
return $this->path;
}
@ -107,25 +116,30 @@ abstract class Node implements \Sabre\DAV\INode {
* Renames the node
*
* @param string $name The new name
* @throws \Sabre\DAV\Exception\BadRequest
* @throws \Sabre\DAV\Exception\Forbidden
* @throws Exception
* @throws Forbidden
* @throws InvalidPath
* @throws PreConditionNotMetException
* @throws LockedException
*/
public function setName($name) {
public function setName($name): void {
// rename is only allowed if the delete privilege is granted
// (basically rename is a copy with delete of the original node)
if (!($this->info->isDeletable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) {
throw new \Sabre\DAV\Exception\Forbidden();
if (!$this->info->isDeletable() && !($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === '')) {
throw new Forbidden();
}
/** @var string $parentPath */
[$parentPath,] = \Sabre\Uri\split($this->path);
/** @var string $newName */
[, $newName] = \Sabre\Uri\split($name);
$newPath = $parentPath . '/' . $newName;
// verify path of the target
$this->verifyPath($newPath);
if (!$this->fileView->rename($this->path, $newPath)) {
throw new \Sabre\DAV\Exception('Failed to rename ' . $this->path . ' to ' . $newPath);
if ($this->fileView->rename($this->path, $newPath) === false) {
throw new Exception('Failed to rename ' . $this->path . ' to ' . $newPath);
}
$this->path = $newPath;
@ -138,12 +152,8 @@ abstract class Node implements \Sabre\DAV\INode {
*
* @return int timestamp as integer
*/
public function getLastModified() {
$timestamp = $this->info->getMtime();
if (!empty($timestamp)) {
return (int)$timestamp;
}
return $timestamp;
public function getLastModified(): int {
return $this->info->getMtime();
}
/**
@ -151,7 +161,7 @@ abstract class Node implements \Sabre\DAV\INode {
* in the second parameter or to now if the second param is empty.
* Even if the modification time is set to a custom value the access time is set to now.
*/
public function touch($mtime) {
public function touch(string $mtime): void {
$mtime = $this->sanitizeMtime($mtime);
$this->fileView->touch($this->path, $mtime);
$this->refreshInfo();
@ -165,37 +175,29 @@ abstract class Node implements \Sabre\DAV\INode {
* arbitrary string, but MUST be surrounded by double-quotes.
*
* Return null if the ETag can not effectively be determined
*
* @return string
*/
public function getETag() {
public function getETag(): string {
return '"' . $this->info->getEtag() . '"';
}
/**
* Sets the ETag
*
* @param string $etag
*
* @return int file id of updated file or -1 on failure
*/
public function setETag($etag) {
public function setETag(string $etag): int {
return $this->fileView->putFileInfo($this->path, ['etag' => $etag]);
}
public function setCreationTime(int $time) {
public function setCreationTime(int $time): int {
return $this->fileView->putFileInfo($this->path, ['creation_time' => $time]);
}
public function setUploadTime(int $time) {
return $this->fileView->putFileInfo($this->path, ['upload_time' => $time]);
}
/**
* Returns the size of the node, in bytes
*
* @psalm-suppress UnusedPsalmSuppress psalm:strict actually thinks there is no mismatch, idk lol
* @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit
* @return int|float
*/
public function getSize(): int|float {
return $this->info->getSize();
@ -203,28 +205,21 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Returns the cache's file id
*
* @return int
*/
public function getId() {
public function getId(): ?int {
return $this->info->getId();
}
/**
* @return string|null
*/
public function getFileId() {
if ($id = $this->info->getId()) {
public function getFileId(): ?string {
$id = $this->info->getId();
if ($id !== null) {
return DavUtil::getDavFileId($id);
}
return null;
}
/**
* @return integer
*/
public function getInternalFileId() {
public function getInternalFileId(): ?int {
return $this->info->getId();
}
@ -232,30 +227,24 @@ abstract class Node implements \Sabre\DAV\INode {
return $this->info->getInternalPath();
}
/**
* @param string $user
* @return int
*/
public function getSharePermissions($user) {
public function getSharePermissions(?string $user): int {
// check of we access a federated share
if ($user !== null) {
try {
$share = $this->shareManager->getShareByToken($user);
return $share->getPermissions();
} catch (ShareNotFound $e) {
return $this->shareManager->getShareByToken($user)->getPermissions();
} catch (ShareNotFound) {
// ignore
}
}
try {
$storage = $this->info->getStorage();
} catch (StorageNotAvailableException $e) {
} catch (StorageNotAvailableException) {
$storage = null;
}
if ($storage && $storage->instanceOfStorage(ISharedStorage::class)) {
/** @var ISharedStorage $storage */
$permissions = (int)$storage->getShare()->getPermissions();
$permissions = $storage->getShare()->getPermissions();
} else {
$permissions = $this->info->getPermissions();
}
@ -266,6 +255,10 @@ abstract class Node implements \Sabre\DAV\INode {
*/
$mountpoint = $this->info->getMountPoint();
if (!($mountpoint instanceof MoveableMount)) {
/**
* @psalm-suppress UnnecessaryVarAnnotation Rector doesn't trust the return type annotation
* @var string $mountpointpath
*/
$mountpointpath = $mountpoint->getMountPoint();
if (str_ends_with($mountpointpath, '/')) {
$mountpointpath = substr($mountpointpath, 0, -1);
@ -286,25 +279,21 @@ abstract class Node implements \Sabre\DAV\INode {
return $permissions;
}
/**
* @return array
*/
public function getShareAttributes(): array {
try {
$storage = $this->node->getStorage();
} catch (NotFoundException $e) {
} catch (NotFoundException) {
return [];
}
$attributes = [];
if ($storage->instanceOfStorage(ISharedStorage::class)) {
/** @var ISharedStorage $storage */
$attributes = $storage->getShare()->getAttributes();
if ($attributes === null) {
return [];
} else {
return $attributes->toArray();
}
return $attributes->toArray();
}
return $attributes;
@ -318,63 +307,66 @@ abstract class Node implements \Sabre\DAV\INode {
}
if ($storage->instanceOfStorage(ISharedStorage::class)) {
/** @var ISharedStorage $storage */
$share = $storage->getShare();
if ($user === $share->getShareOwner()) {
// Note is only for recipient not the owner
return null;
}
return $share->getNote();
}
return null;
}
/**
* @return string
*/
public function getDavPermissions() {
public function getDavPermissions(): string {
return DavUtil::getDavPermissions($this->info);
}
public function getOwner() {
public function getOwner(): ?IUser {
return $this->info->getOwner();
}
/**
* @throws InvalidPath
*/
protected function verifyPath(?string $path = null): void {
try {
$path = $path ?? $this->info->getPath();
$path ??= $this->info->getPath();
$this->fileView->verifyPath(
dirname($path),
basename($path),
);
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
} catch (InvalidPathException $invalidPathException) {
throw new InvalidPath($invalidPathException->getMessage(), false, $invalidPathException);
}
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param ILockingProvider::LOCK_* $type
* @throws LockedException
*/
public function acquireLock($type) {
public function acquireLock($type): void {
$this->fileView->lockFile($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param ILockingProvider::LOCK_* $type
* @throws LockedException
*/
public function releaseLock($type) {
public function releaseLock($type): void {
$this->fileView->unlockFile($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
* @param ILockingProvider::LOCK_* $type
* @throws LockedException
*/
public function changeLock($type) {
public function changeLock($type): void {
$this->fileView->changeLock($this->path, $type);
}
public function getFileInfo() {
public function getFileInfo(): FileInfo {
return $this->info;
}

View file

@ -32,12 +32,12 @@ class FilesHome extends Directory {
throw new Forbidden('Permission denied to delete home folder');
}
public function getName() {
public function getName(): string {
[,$name] = \Sabre\Uri\split($this->principalInfo['uri']);
return $name;
}
public function setName($name) {
public function setName($name): void {
throw new Forbidden('Permission denied to rename this folder');
}
}

View file

@ -61,14 +61,14 @@ class CommentsPropertiesPluginTest extends \Test\TestCase {
public static function baseUriProvider(): array {
return [
['owncloud/remote.php/webdav/', '4567', 'owncloud/remote.php/dav/comments/files/4567'],
['owncloud/remote.php/files/', '4567', 'owncloud/remote.php/dav/comments/files/4567'],
['owncloud/wicked.php/files/', '4567', null]
['owncloud/remote.php/webdav/', 4567, 'owncloud/remote.php/dav/comments/files/4567'],
['owncloud/remote.php/files/', 4567, 'owncloud/remote.php/dav/comments/files/4567'],
['owncloud/wicked.php/files/', 4567, null]
];
}
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'baseUriProvider')]
public function testGetCommentsLink(string $baseUri, string $fid, ?string $expectedHref): void {
public function testGetCommentsLink(string $baseUri, int $fid, ?string $expectedHref): void {
$node = $this->createMock(File::class);
$node->expects($this->any())
->method('getId')
@ -94,7 +94,7 @@ class CommentsPropertiesPluginTest extends \Test\TestCase {
$node = $this->createMock(File::class);
$node->expects($this->any())
->method('getId')
->willReturn('4567');
->willReturn(4567);
if ($user !== null) {
$user = $this->createMock($user);

View file

@ -230,6 +230,10 @@ class DirectoryTest extends \Test\TestCase {
$info->expects($this->any())
->method('isReadable')
->willReturn(false);
$this->view
->method('getRelativePath')
->with(null)
->willReturn('');
$dir = new Directory($this->view, $info);
$dir->getChildren();
@ -242,6 +246,10 @@ class DirectoryTest extends \Test\TestCase {
$this->info->expects($this->any())
->method('isReadable')
->willReturn(false);
$this->view
->method('getRelativePath')
->with('/admin/files/folder')
->willReturn('');
$dir = new Directory($this->view, $this->info);
$dir->getChild('test');
@ -254,6 +262,10 @@ class DirectoryTest extends \Test\TestCase {
$this->view->expects($this->once())
->method('getFileInfo')
->willThrowException(new StorageNotAvailableException());
$this->view
->method('getRelativePath')
->with('/admin/files/folder')
->willReturn('');
$dir = new Directory($this->view, $this->info);
$dir->getChild('.');
@ -268,6 +280,10 @@ class DirectoryTest extends \Test\TestCase {
->willThrowException(new InvalidPathException());
$this->view->expects($this->never())
->method('getFileInfo');
$this->view
->method('getRelativePath')
->with('/admin/files/folder')
->willReturn('');
$dir = new Directory($this->view, $this->info);
$dir->getChild('.');
@ -376,6 +392,11 @@ class DirectoryTest extends \Test\TestCase {
}
public function testGetNodeForPathFailsWithNoReadPermissionsForPath(): void {
$this->view
->method('getRelativePath')
->with('/admin/files/')
->willReturn('');
$directoryNode = $this->createMock(Folder::class);
$pathNode = $this->createMock(Folder::class);
$storage = $this->createMock(IStorage::class);
@ -396,7 +417,7 @@ class DirectoryTest extends \Test\TestCase {
2 => false,
};
});
$directoryNode->expects($this->once())
$directoryNode
->method('getPath')
->willReturn('/admin/files/');
$directoryNode->expects($this->once())

View file

@ -673,6 +673,10 @@ class FileTest extends TestCase {
/** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
$view
->method('getRelativePath')
->with('/test.txt')
->willReturn('');
$view->expects($this->once())
->method('unlink')
@ -697,6 +701,10 @@ class FileTest extends TestCase {
/** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
$view
->method('getRelativePath')
->with('/test.txt')
->willReturn('');
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
'permissions' => 0,
@ -717,6 +725,10 @@ class FileTest extends TestCase {
/** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
$view
->method('getRelativePath')
->with('/test.txt')
->willReturn('');
// but fails
$view->expects($this->once())
@ -742,6 +754,10 @@ class FileTest extends TestCase {
/** @var View&MockObject */
$view = $this->getMockBuilder(View::class)
->getMock();
$view
->method('getRelativePath')
->with('/test.txt')
->willReturn('');
// but fails
$view->expects($this->once())

View file

@ -101,7 +101,7 @@ class FilesPluginTest extends TestCase {
->willReturn('00000123instanceid');
$node->expects($this->any())
->method('getInternalFileId')
->willReturn('123');
->willReturn(123);
$node->expects($this->any())
->method('getEtag')
->willReturn('"abc"');
@ -455,7 +455,7 @@ class FilesPluginTest extends TestCase {
$node->expects($this->once())
->method('setEtag')
->with('newetag')
->willReturn(true);
->willReturn(123);
$node->expects($this->once())
->method('setCreationTime')
@ -562,35 +562,11 @@ class FilesPluginTest extends TestCase {
$this->plugin->checkMove('FolderA/test.txt', 'test.txt');
}
public function testMoveSrcNotExist(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->expectExceptionMessage('FolderA/test.txt does not exist');
$node = $this->createMock(Node::class);
$node->expects($this->atLeastOnce())
->method('getFileInfo')
->willReturn(null);
$this->tree->expects($this->atLeastOnce())
->method('getNodeForPath')
->willReturn($node);
$this->plugin->checkMove('FolderA/test.txt', 'test.txt');
}
public function testMoveDestinationInvalid(): void {
$this->expectException(InvalidPath::class);
$this->expectExceptionMessage('Mocked exception');
$fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
$fileInfoFolderATestTXT->expects(self::any())
->method('isDeletable')
->willReturn(true);
$node = $this->createMock(Node::class);
$node->expects($this->atLeastOnce())
->method('getFileInfo')
->willReturn($fileInfoFolderATestTXT);
$this->tree->expects($this->atLeastOnce())
->method('getNodeForPath')
@ -604,31 +580,11 @@ class FilesPluginTest extends TestCase {
$this->plugin->checkMove('FolderA/test.txt', 'invalid\\path.txt');
}
public function testCopySrcNotExist(): void {
$this->expectException(\Sabre\DAV\Exception\NotFound::class);
$this->expectExceptionMessage('FolderA/test.txt does not exist');
$node = $this->createMock(Node::class);
$node->expects($this->atLeastOnce())
->method('getFileInfo')
->willReturn(null);
$this->tree->expects($this->atLeastOnce())
->method('getNodeForPath')
->willReturn($node);
$this->plugin->checkCopy('FolderA/test.txt', 'test.txt');
}
public function testCopyDestinationInvalid(): void {
$this->expectException(InvalidPath::class);
$this->expectExceptionMessage('Mocked exception');
$fileInfoFolderATestTXT = $this->createMock(FileInfo::class);
$node = $this->createMock(Node::class);
$node->expects($this->atLeastOnce())
->method('getFileInfo')
->willReturn($fileInfoFolderATestTXT);
$this->tree->expects($this->atLeastOnce())
->method('getNodeForPath')

View file

@ -57,6 +57,10 @@ class FilesReportPluginTest extends \Test\TestCase {
$this->tree = $this->createMock(Tree::class);
$this->view = $this->createMock(View::class);
$this->view
->method('getRelativePath')
->with(null)
->willReturn('');
$this->server = $this->getMockBuilder(Server::class)
->setConstructorArgs([$this->tree])
@ -315,14 +319,14 @@ class FilesReportPluginTest extends \Test\TestCase {
$node1->expects($this->once())
->method('getInternalFileId')
->willReturn('111');
->willReturn(111);
$node1->expects($this->any())
->method('getPath')
->willReturn('/node1');
$node1->method('getFileInfo')->willReturn($fileInfo);
$node2->expects($this->once())
->method('getInternalFileId')
->willReturn('222');
->willReturn(222);
$node2->expects($this->once())
->method('getSize')
->willReturn(1024);

View file

@ -94,6 +94,10 @@ class NodeTest extends \Test\TestCase {
$info->method('getStorage')
->willReturn($storage);
$view = $this->createMock(View::class);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$node = new File($view, $info);
$this->assertEquals($expected, $node->getDavPermissions());
@ -169,6 +173,10 @@ class NodeTest extends \Test\TestCase {
$info->method('getPermissions')->willReturn($permissions);
$view = $this->createMock(View::class);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$node = new File($view, $info);
$this->invokePrivate($node, 'shareManager', [$shareManager]);
@ -204,6 +212,10 @@ class NodeTest extends \Test\TestCase {
/** @var View&MockObject $view */
$view = $this->createMock(View::class);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$node = new File($view, $info);
$this->invokePrivate($node, 'shareManager', [$shareManager]);
@ -225,6 +237,10 @@ class NodeTest extends \Test\TestCase {
/** @var View&MockObject */
$view = $this->createMock(View::class);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$node = new File($view, $info);
$this->invokePrivate($node, 'shareManager', [$shareManager]);
@ -243,6 +259,10 @@ class NodeTest extends \Test\TestCase {
$view = $this->getMockBuilder(View::class)
->disableOriginalConstructor()
->getMock();
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$info = $this->getMockBuilder(FileInfo::class)
->disableOriginalConstructor()
->getMock();
@ -263,6 +283,11 @@ class NodeTest extends \Test\TestCase {
$this->expectException(\InvalidArgumentException::class);
$view = $this->createMock(View::class);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$info = $this->createMock(FileInfo::class);
$node = new File($view, $info);

View file

@ -63,6 +63,10 @@ class ObjectTreeTest extends \Test\TestCase {
->method('getFileInfo')
->with($targetParent === '' ? '.' : $targetParent)
->willReturn($info);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$rootDir = new Directory($view, $info);
$objectTree = $this->getMockBuilder(ObjectTree::class)
@ -104,6 +108,10 @@ class ObjectTreeTest extends \Test\TestCase {
->method('getFileInfo')
->with($targetParent === '' ? '.' : $targetParent)
->willReturn($info);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$rootDir = new Directory($view, $info);
$objectTree = $this->getMockBuilder(ObjectTree::class)
@ -141,6 +149,10 @@ class ObjectTreeTest extends \Test\TestCase {
$view->method('getFileInfo')
->with($fileInfoQueryPath)
->willReturn($fileInfo);
$view
->method('getRelativePath')
->with(null)
->willReturn('');
$tree = new ObjectTree();
$tree->init($rootNode, $view, $mountManager);

View file

@ -616,20 +616,11 @@
<code><![CDATA[lockFile]]></code>
<code><![CDATA[putFileInfo]]></code>
<code><![CDATA[putFileInfo]]></code>
<code><![CDATA[putFileInfo]]></code>
<code><![CDATA[rename]]></code>
<code><![CDATA[touch]]></code>
<code><![CDATA[unlockFile]]></code>
<code><![CDATA[verifyPath]]></code>
</InternalMethod>
<InvalidNullableReturnType>
<code><![CDATA[int]]></code>
<code><![CDATA[integer]]></code>
</InvalidNullableReturnType>
<NullableReturnStatement>
<code><![CDATA[$this->info->getId()]]></code>
<code><![CDATA[$this->info->getId()]]></code>
</NullableReturnStatement>
</file>
<file src="apps/dav/lib/Connector/Sabre/ObjectTree.php">
<InternalMethod>

View file

@ -12,6 +12,7 @@ return (require __DIR__ . '/rector-shared.php')
$nextcloudDir . '/build/rector-strict.php',
$nextcloudDir . '/core/BackgroundJobs/ExpirePreviewsJob.php',
$nextcloudDir . '/lib/public/IContainer.php',
$nextcloudDir . '/apps/dav/lib/Connector/Sabre/Node.php',
])
->withPreparedSets(
deadCode: true,

View file

@ -18,6 +18,7 @@
<projectFiles>
<file name="core/BackgroundJobs/ExpirePreviewsJob.php"/>
<file name="lib/public/IContainer.php"/>
<file name="apps/dav/lib/Connector/Sabre/Node.php"/>
<ignoreFiles>
<directory name="apps/**/composer"/>
<directory name="apps/**/tests"/>
@ -27,8 +28,13 @@
</ignoreFiles>
</projectFiles>
<extraFiles>
<directory name="apps/dav/lib"/>
<directory name="3rdparty"/>
</extraFiles>
<stubs>
<!-- Psalm does not find methods in here through <extraFiles/> 🤷‍♀️ -->
<file name="3rdparty/sabre/uri/lib/functions.php"/>
</stubs>
<issueHandlers>
<InternalClass>
<errorLevel type="suppress">