* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing; use OC\Files\Filesystem; use OC\Files\View; use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\ICachedMountInfo; use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; use OCP\IUser; use OCP\Share\Events\VerifyMountPointEvent; use OCP\Share\IManager; use OCP\Share\IShare; /** * Validate that mount target is valid */ class ShareTargetValidator { private CappedMemoryCache $folderExistsCache; public function __construct( private readonly IManager $shareManager, private readonly IEventDispatcher $eventDispatcher, private readonly IRootFolder $rootFolder, ) { $this->folderExistsCache = new CappedMemoryCache(); } private function getViewForUser(IUser $user): View { /** * @psalm-suppress InternalClass * @psalm-suppress InternalMethod */ return new View('/' . $user->getUID() . '/files'); } /** * check if the parent folder exists otherwise move the mount point up * * @param callable(string):?ICachedMountInfo $getMountByPath * @param IShare[] $childShares * @return string */ public function verifyMountPoint( IUser $user, IShare &$share, callable $getMountByPath, array $childShares, ): string { $mountPoint = basename($share->getTarget()); $parent = dirname($share->getTarget()); $recipientView = $this->getViewForUser($user); $event = new VerifyMountPointEvent($share, $recipientView, $parent, $user); $this->eventDispatcher->dispatchTyped($event); $parent = $event->getParent(); /** @psalm-suppress InternalMethod */ $absoluteParent = $recipientView->getAbsolutePath($parent); // the share target always has to be in the users home $userFolder = $this->rootFolder->getUserFolder($user->getUID()); $parentMount = $userFolder->getMountPoint(); $cached = $this->folderExistsCache->get($parent); if ($cached) { $parentExists = $cached; } else { $parentCache = $parentMount->getStorage()->getCache(); $parentExists = $parentCache->inCache($parentMount->getInternalPath($absoluteParent)); $this->folderExistsCache->set($parent, $parentExists); } if (!$parentExists) { if ($event->createParent()) { $internalPath = $parentMount->getInternalPath($absoluteParent); $parentMount->getStorage()->mkdir($internalPath); $parentMount->getStorage()->getUpdater()->update($internalPath); } else { $parent = Helper::getShareFolder($recipientView, $user->getUID()); /** @psalm-suppress InternalMethod */ $absoluteParent = $recipientView->getAbsolutePath($parent); } } $newAbsoluteMountPoint = $this->generateUniqueTarget( $share->getNodeId(), Filesystem::normalizePath($absoluteParent . '/' . $mountPoint), $parentMount, $getMountByPath, ); /** @psalm-suppress InternalMethod */ $newMountPoint = $recipientView->getRelativePath($newAbsoluteMountPoint); if ($newMountPoint === null) { return $share->getTarget(); } if ($newMountPoint !== $share->getTarget()) { $this->updateFileTarget($user, $newMountPoint, $share, $childShares); } return $newMountPoint; } /** * @param callable(string):?ICachedMountInfo $getMountByPath */ public function generateUniqueTarget( int $shareNodeId, string $absolutePath, IMountPoint $parentMount, callable $getMountByPath, ): string { $pathInfo = pathinfo($absolutePath); $ext = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; $name = $pathInfo['filename']; $dir = $pathInfo['dirname']; $i = 2; $parentCache = $parentMount->getStorage()->getCache(); $internalPath = $parentMount->getInternalPath($absolutePath); while ($parentCache->inCache($internalPath) || $this->hasConflictingMount($shareNodeId, $getMountByPath, $absolutePath)) { $absolutePath = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext); $internalPath = $parentMount->getInternalPath($absolutePath); $i++; } return $absolutePath; } /** * @param callable(string):?ICachedMountInfo $getMountByPath */ private function hasConflictingMount(int $shareNodeId, callable $getMountByPath, string $absolutePath): bool { $mount = $getMountByPath($absolutePath . '/'); if ($mount === null) { return false; } if ($mount->getMountProvider() === MountProvider::class && $mount->getRootId() === $shareNodeId) { // "conflicting" mount is a mount for the current share return false; } return true; } /** * update fileTarget in the database if the mount point changed * * @param IShare[] $childShares */ private function updateFileTarget(IUser $user, string $newPath, IShare &$share, array $childShares) { $share->setTarget($newPath); foreach ($childShares as $tmpShare) { $tmpShare->setTarget($newPath); $this->shareManager->moveShare($tmpShare, $user->getUID()); } } }