nextcloud/apps/files_sharing/lib/ShareTargetValidator.php
Robin Appelman 891686653a
fix: don't rely on share providers being avaiable in CleanupShareTarget
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-01-30 16:54:06 +01:00

163 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Sharing;
use OC\Files\Filesystem;
use OC\Files\SetupManager;
use OC\Files\View;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Mount\IMountManager;
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 SetupManager $setupManager,
private readonly IMountManager $mountManager,
) {
$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 array<string, IMountPoint> $allCachedMounts Other mounts for the user, indexed by path
* @param IShare[] $childShares
* @return string
*/
public function verifyMountPoint(
IUser $user,
IShare &$share,
array $allCachedMounts,
array $childShares,
): string {
$mountPoint = basename($share->getTarget());
$parent = dirname($share->getTarget());
$recipientView = $this->getViewForUser($user);
$event = new VerifyMountPointEvent($share, $recipientView, $parent);
$this->eventDispatcher->dispatchTyped($event);
$parent = $event->getParent();
/** @psalm-suppress InternalMethod */
$absoluteParent = $recipientView->getAbsolutePath($parent);
$this->setupManager->setupForPath($absoluteParent);
$parentMount = $this->mountManager->find($absoluteParent);
$cached = $this->folderExistsCache->get($parent);
if ($cached !== null) {
$parentExists = $cached;
} else {
$parentCache = $parentMount->getStorage()->getCache();
$parentExists = $parentCache->inCache($parentMount->getInternalPath($absoluteParent));
$this->folderExistsCache->set($parent, $parentExists);
}
if (!$parentExists) {
$parent = Helper::getShareFolder($recipientView, $user->getUID());
/** @psalm-suppress InternalMethod */
$absoluteParent = $recipientView->getAbsolutePath($parent);
}
$newAbsoluteMountPoint = $this->generateUniqueTarget(
$share->getNodeId(),
Filesystem::normalizePath($absoluteParent . '/' . $mountPoint),
$parentMount,
$allCachedMounts,
);
/** @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 IMountPoint[] $allCachedMounts
*/
public function generateUniqueTarget(
int $shareNodeId,
string $absolutePath,
IMountPoint $parentMount,
array $allCachedMounts,
): 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, $allCachedMounts, $absolutePath)) {
$absolutePath = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext);
$internalPath = $parentMount->getInternalPath($absolutePath);
$i++;
}
return $absolutePath;
}
/**
* @param IMountPoint[] $allCachedMounts
*/
private function hasConflictingMount(int $shareNodeId, array $allCachedMounts, string $absolutePath): bool {
if (!isset($allCachedMounts[$absolutePath . '/'])) {
return false;
}
$mount = $allCachedMounts[$absolutePath . '/'];
if ($mount instanceof SharedMount && $mount->getShare()->getNodeId() === $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());
}
}
}