diff options
Diffstat (limited to 'apps/dav/lib/Connector')
19 files changed, 108 insertions, 74 deletions
diff --git a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php index b39dc7197b0..0e2b1c58748 100644 --- a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php index 88fff5e6a5a..9cff113140a 100644 --- a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php +++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php index d977721bdfa..a174920946a 100644 --- a/apps/dav/lib/Connector/Sabre/Auth.php +++ b/apps/dav/lib/Connector/Sabre/Auth.php @@ -55,8 +55,8 @@ class Auth extends AbstractBasic { * @see https://github.com/owncloud/core/issues/13245 */ public function isDavAuthenticated(string $username): bool { - return !is_null($this->session->get(self::DAV_AUTHENTICATED)) && - $this->session->get(self::DAV_AUTHENTICATED) === $username; + return !is_null($this->session->get(self::DAV_AUTHENTICATED)) + && $this->session->get(self::DAV_AUTHENTICATED) === $username; } /** @@ -71,8 +71,8 @@ class Auth extends AbstractBasic { * @throws PasswordLoginForbidden */ protected function validateUserPass($username, $password) { - if ($this->userSession->isLoggedIn() && - $this->isDavAuthenticated($this->userSession->getUser()->getUID()) + if ($this->userSession->isLoggedIn() + && $this->isDavAuthenticated($this->userSession->getUser()->getUID()) ) { $this->session->close(); return true; @@ -118,7 +118,7 @@ class Auth extends AbstractBasic { * Checks whether a CSRF check is required on the request */ private function requiresCSRFCheck(): bool { - + $methodsWithoutCsrf = ['GET', 'HEAD', 'OPTIONS']; if (in_array($this->request->getMethod(), $methodsWithoutCsrf)) { return false; @@ -144,8 +144,8 @@ class Auth extends AbstractBasic { } // If logged-in AND DAV authenticated no check is required - if ($this->userSession->isLoggedIn() && - $this->isDavAuthenticated($this->userSession->getUser()->getUID())) { + if ($this->userSession->isLoggedIn() + && $this->isDavAuthenticated($this->userSession->getUser()->getUID())) { return false; } @@ -159,8 +159,8 @@ class Auth extends AbstractBasic { private function auth(RequestInterface $request, ResponseInterface $response): array { $forcedLogout = false; - if (!$this->request->passesCSRFCheck() && - $this->requiresCSRFCheck()) { + if (!$this->request->passesCSRFCheck() + && $this->requiresCSRFCheck()) { // In case of a fail with POST we need to recheck the credentials if ($this->request->getMethod() === 'POST') { $forcedLogout = true; @@ -178,10 +178,10 @@ class Auth extends AbstractBasic { } if ( //Fix for broken webdav clients - ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) || + ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) //Well behaved clients that only send the cookie are allowed - ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && empty($request->getHeader('Authorization'))) || - \OC_User::handleApacheAuth() + || ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && empty($request->getHeader('Authorization'))) + || \OC_User::handleApacheAuth() ) { $user = $this->userSession->getUser()->getUID(); $this->currentUser = $user; diff --git a/apps/dav/lib/Connector/Sabre/BearerAuth.php b/apps/dav/lib/Connector/Sabre/BearerAuth.php index e189d8fa128..23453ae8efb 100644 --- a/apps/dav/lib/Connector/Sabre/BearerAuth.php +++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/CachingTree.php b/apps/dav/lib/Connector/Sabre/CachingTree.php index 86e102677c1..5d72b530f58 100644 --- a/apps/dav/lib/Connector/Sabre/CachingTree.php +++ b/apps/dav/lib/Connector/Sabre/CachingTree.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php index 64a61a43a9b..18009080585 100644 --- a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php +++ b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php @@ -27,20 +27,6 @@ class ChecksumUpdatePlugin extends ServerPlugin { } /** @return string[] */ - public function getHTTPMethods($path): array { - $tree = $this->server->tree; - - if ($tree->nodeExists($path)) { - $node = $tree->getNodeForPath($path); - if ($node instanceof File) { - return ['PATCH']; - } - } - - return []; - } - - /** @return string[] */ public function getFeatures(): array { return ['nextcloud-checksum-update']; } diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php index 7846896182f..100d719ef01 100644 --- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php +++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php @@ -62,7 +62,7 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin { ) ); } - + } return $access; diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index 3ebaa55786c..fe09c3f423f 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -176,13 +176,14 @@ class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuot public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) { $storage = $this->info->getStorage(); $allowDirectory = false; + + // Checking if we're in a file drop + // If we are, then only PUT and MKCOL are allowed (see plugin) + // so we are safe to return the directory without a risk of + // leaking files and folders structure. if ($storage instanceof PublicShareWrapper) { $share = $storage->getShare(); - $allowDirectory = - // Only allow directories for file drops - ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ && - // And only allow it for directories which are a direct child of the share root - $this->info->getId() === $share->getNodeId(); + $allowDirectory = ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ; } // For file drop we need to be allowed to read the directory with the nickname diff --git a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php index 4a7e30caa10..f6baceb748b 100644 --- a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php +++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php @@ -43,8 +43,8 @@ class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin { * @return false */ public function httpGet(RequestInterface $request, ResponseInterface $response) { - $string = 'This is the WebDAV interface. It can only be accessed by ' . - 'WebDAV clients such as the Nextcloud desktop sync client.'; + $string = 'This is the WebDAV interface. It can only be accessed by ' + . 'WebDAV clients such as the Nextcloud desktop sync client.'; $stream = fopen('php://memory', 'r+'); fwrite($stream, $string); rewind($stream); diff --git a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php index 41ace002660..1e1e4aaed04 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php +++ b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php index 61ecfaf845c..b0c5a079ce1 100644 --- a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php @@ -117,8 +117,8 @@ class FakeLockerPlugin extends ServerPlugin { $lockInfo->timeout = 1800; $body = $this->server->xml->write('{DAV:}prop', [ - '{DAV:}lockdiscovery' => - new LockDiscovery([$lockInfo]) + '{DAV:}lockdiscovery' + => new LockDiscovery([$lockInfo]) ]); $response->setStatus(Http::STATUS_OK); diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 045b9d7e784..218d38e1c4b 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -19,6 +19,7 @@ use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; use OCP\App\IAppManager; use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\Files; use OCP\Files\EntityTooLargeException; use OCP\Files\FileInfo; use OCP\Files\ForbiddenException; @@ -215,7 +216,9 @@ class File extends Node implements IFile { try { /** @var IWriteStreamStorage $partStorage */ $count = $partStorage->writeStream($internalPartPath, $wrappedData); - } catch (GenericFileException) { + } catch (GenericFileException $e) { + $logger = Server::get(LoggerInterface::class); + $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']); $result = $isEOF; if (is_resource($wrappedData)) { $result = feof($wrappedData); @@ -229,7 +232,7 @@ class File extends Node implements IFile { // because we have no clue about the cause we can only throw back a 500/Internal Server Error throw new Exception($this->l10n->t('Could not write file contents')); } - [$count, $result] = \OC_Helper::streamCopy($data, $target); + [$count, $result] = Files::streamCopy($data, $target, true); fclose($target); } diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 9e2affddb6b..843383a0452 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -9,6 +9,7 @@ namespace OCA\DAV\Connector\Sabre; use OC\AppFramework\Http\Request; use OC\FilesMetadata\Model\FilesMetadata; +use OC\User\NoUserException; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCA\Files_Sharing\External\Mount as SharingExternalMount; use OCP\Accounts\IAccountManager; @@ -256,8 +257,8 @@ class FilesPlugin extends ServerPlugin { // adds a 'Content-Disposition: attachment' header in case no disposition // header has been set before - if ($this->downloadAttachment && - $response->getHeader('Content-Disposition') === null) { + if ($this->downloadAttachment + && $response->getHeader('Content-Disposition') === null) { $filename = $node->getName(); if ($this->request->isUserAgent( [ @@ -374,7 +375,13 @@ class FilesPlugin extends ServerPlugin { } // Check if the user published their display name - $ownerAccount = $this->accountManager->getAccount($owner); + try { + $ownerAccount = $this->accountManager->getAccount($owner); + } catch (NoUserException) { + // do not lock process if owner is not local + return null; + } + $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); // Since we are not logged in, we need to have at least the published scope @@ -534,8 +541,8 @@ class FilesPlugin extends ServerPlugin { $ocmPermissions[] = 'read'; } - if (($ncPermissions & Constants::PERMISSION_CREATE) || - ($ncPermissions & Constants::PERMISSION_UPDATE)) { + if (($ncPermissions & Constants::PERMISSION_CREATE) + || ($ncPermissions & Constants::PERMISSION_UPDATE)) { $ocmPermissions[] = 'write'; } diff --git a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php index efed6ce09f8..e18ef58149a 100644 --- a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php +++ b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php index 67edb1c4035..b61cabedf5f 100644 --- a/apps/dav/lib/Connector/Sabre/Principal.php +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -41,9 +42,6 @@ class Principal implements BackendInterface { /** @var bool */ private $hasCircles; - /** @var ProxyMapper */ - private $proxyMapper; - /** @var KnownUserService */ private $knownUserService; @@ -54,7 +52,7 @@ class Principal implements BackendInterface { private IShareManager $shareManager, private IUserSession $userSession, private IAppManager $appManager, - ProxyMapper $proxyMapper, + private ProxyMapper $proxyMapper, KnownUserService $knownUserService, private IConfig $config, private IFactory $languageFactory, @@ -62,7 +60,6 @@ class Principal implements BackendInterface { ) { $this->principalPrefix = trim($principalPrefix, '/'); $this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/'); - $this->proxyMapper = $proxyMapper; $this->knownUserService = $knownUserService; } diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php index b5d9ce3db72..2ca1c25e2f6 100644 --- a/apps/dav/lib/Connector/Sabre/PublicAuth.php +++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php @@ -14,6 +14,7 @@ namespace OCA\DAV\Connector\Sabre; use OCP\Defaults; use OCP\IRequest; use OCP\ISession; +use OCP\IURLGenerator; use OCP\Security\Bruteforce\IThrottler; use OCP\Security\Bruteforce\MaxDelayReached; use OCP\Share\Exceptions\ShareNotFound; @@ -23,6 +24,7 @@ use Psr\Log\LoggerInterface; use Sabre\DAV\Auth\Backend\AbstractBasic; use Sabre\DAV\Exception\NotAuthenticated; use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\PreconditionFailed; use Sabre\DAV\Exception\ServiceUnavailable; use Sabre\HTTP; use Sabre\HTTP\RequestInterface; @@ -45,6 +47,7 @@ class PublicAuth extends AbstractBasic { private ISession $session, private IThrottler $throttler, private LoggerInterface $logger, + private IURLGenerator $urlGenerator, ) { // setup realm $defaults = new Defaults(); @@ -52,10 +55,6 @@ class PublicAuth extends AbstractBasic { } /** - * @param RequestInterface $request - * @param ResponseInterface $response - * - * @return array * @throws NotAuthenticated * @throws MaxDelayReached * @throws ServiceUnavailable @@ -64,6 +63,10 @@ class PublicAuth extends AbstractBasic { try { $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); + if (count($_COOKIE) > 0 && !$this->request->passesStrictCookieCheck() && $this->getShare()->getPassword() !== null) { + throw new PreconditionFailed('Strict cookie check failed'); + } + $auth = new HTTP\Auth\Basic( $this->realm, $request, @@ -80,6 +83,15 @@ class PublicAuth extends AbstractBasic { } catch (NotAuthenticated|MaxDelayReached $e) { $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); throw $e; + } catch (PreconditionFailed $e) { + $response->setHeader( + 'Location', + $this->urlGenerator->linkToRoute( + 'files_sharing.share.showShare', + [ 'token' => $this->getToken() ], + ), + ); + throw $e; } catch (\Exception $e) { $class = get_class($e); $msg = $e->getMessage(); @@ -90,7 +102,6 @@ class PublicAuth extends AbstractBasic { /** * Extract token from request url - * @return string * @throws NotFound */ private function getToken(): string { @@ -107,7 +118,7 @@ class PublicAuth extends AbstractBasic { /** * Check token validity - * @return array + * * @throws NotFound * @throws NotAuthenticated */ @@ -155,15 +166,13 @@ class PublicAuth extends AbstractBasic { protected function validateUserPass($username, $password) { $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); - $token = $this->getToken(); try { - $share = $this->shareManager->getShareByToken($token); + $share = $this->getShare(); } catch (ShareNotFound $e) { $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); return false; } - $this->share = $share; \OC_User::setIncognitoMode(true); // check if the share is password protected @@ -206,7 +215,13 @@ class PublicAuth extends AbstractBasic { } public function getShare(): IShare { - assert($this->share !== null); + $token = $this->getToken(); + + if ($this->share === null) { + $share = $this->shareManager->getShareByToken($token); + $this->share = $share; + } + return $this->share; } } diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index bdd13b7f44e..214412e1744 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -15,6 +15,7 @@ use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\ViewOnlyPlugin; use OCA\DAV\Files\BrowserErrorPagePlugin; +use OCA\DAV\Files\Sharing\RootCollection; use OCA\DAV\Upload\CleanupService; use OCA\Theming\ThemingDefaults; use OCP\Accounts\IAccountManager; @@ -150,7 +151,7 @@ class ServerFactory { ); // Mount the share collection at /public.php/dav/shares/<share token> - $rootCollection->addChild(new \OCA\DAV\Files\Sharing\RootCollection( + $rootCollection->addChild(new RootCollection( $root, $userPrincipalBackend, 'principals/shares', diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php index 088cf33d85f..f49e85333f3 100644 --- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -176,8 +176,8 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { if ($sabreNode instanceof Directory && $propFind->getDepth() !== 0 && ( - !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) || - !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME)) + !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) + || !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME)) ) ) { $folderNode = $sabreNode->getNode(); diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php index eb06fa5cef6..25c1633df36 100644 --- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php @@ -94,6 +94,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { $this->server = $server; $this->server->on('propFind', [$this, 'handleGetProperties']); $this->server->on('propPatch', [$this, 'handleUpdateProperties']); + $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']); } /** @@ -150,6 +151,24 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { } /** + * Prefetches tags for a list of file IDs and caches the results + * + * @param array $fileIds List of file IDs to prefetch tags for + * @return void + */ + private function prefetchTagsForFileIds(array $fileIds) { + $tags = $this->getTagger()->getTagsForObjects($fileIds); + if ($tags === false) { + // the tags API returns false on error... + $tags = []; + } + + foreach ($fileIds as $fileId) { + $this->cachedTags[$fileId] = $tags[$fileId] ?? []; + } + } + + /** * Updates the tags of the given file id * * @param int $fileId @@ -199,22 +218,11 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { )) { // note: pre-fetching only supported for depth <= 1 $folderContent = $node->getChildren(); - $fileIds[] = (int)$node->getId(); + $fileIds = [(int)$node->getId()]; foreach ($folderContent as $info) { $fileIds[] = (int)$info->getId(); } - $tags = $this->getTagger()->getTagsForObjects($fileIds); - if ($tags === false) { - // the tags API returns false on error... - $tags = []; - } - - $this->cachedTags = $this->cachedTags + $tags; - $emptyFileIds = array_diff($fileIds, array_keys($tags)); - // also cache the ones that were not found - foreach ($emptyFileIds as $fileId) { - $this->cachedTags[$fileId] = []; - } + $this->prefetchTagsForFileIds($fileIds); } $isFav = null; @@ -270,4 +278,14 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { return 200; }); } + + public function handlePreloadProperties(array $nodes, array $requestProperties): void { + if ( + !in_array(self::FAVORITE_PROPERTYNAME, $requestProperties, true) + && !in_array(self::TAGS_PROPERTYNAME, $requestProperties, true) + ) { + return; + } + $this->prefetchTagsForFileIds(array_map(fn ($node) => $node->getId(), $nodes)); + } } |