Files
Kent Delante 050032842c fix(trashbin): properly show deleted by for federated shares
Signed-off-by: Kent Delante <kent@delante.me>
Assisted-by: ClaudeCode:claude-sonnet-5

When a remote user deletes a file in a federated share, the deleted by
column in trashbin shows "Unknown" instead of the remote user. This
fixes it so that it properly shows the remote user under the deleted
by column.
2026-08-06 21:29:25 +08:00

156 lines
4.6 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Trashbin\Trash;
use OC\Files\Filesystem;
use OC\Files\View;
use OCA\Files_Trashbin\Helper;
use OCA\Files_Trashbin\Storage;
use OCA\Files_Trashbin\Trashbin;
use OCP\Federation\ICloudIdManager;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IStorage;
use OCP\IUser;
use OCP\IUserManager;
class LegacyTrashBackend implements ITrashBackend {
/** @var array */
private $deletedFiles = [];
public function __construct(
private IRootFolder $rootFolder,
private IUserManager $userManager,
private ICloudIdManager $cloudIdManager,
) {
}
private function mapTrashItem(FileInfo $file, IUser $user, ?ITrashItem $parent = null): ITrashItem {
$parentTrashPath = ($parent instanceof ITrashItem) ? $parent->getTrashPath() : '';
$isRoot = $parent === null;
$originalLocation = $isRoot ? $file['extraData'] : $parent->getOriginalLocation() . '/' . $file->getName();
if (!$originalLocation) {
$originalLocation = $file->getName();
}
/** @psalm-suppress UndefinedInterfaceMethod */
$deletedBy = $this->resolveDeletedBy($file['deletedBy']) ?? $parent?->getDeletedBy();
$trashFilename = Trashbin::getTrashFilename($file->getName(), $file->getMtime());
return new TrashItem(
$this,
$originalLocation,
$file->getMTime(),
$parentTrashPath . '/' . ($isRoot ? $trashFilename : $file->getName()),
$file,
$user,
$deletedBy,
);
}
#[\Override]
public function listTrashRoot(IUser $user): array {
$entries = Helper::getTrashFiles('/', $user->getUID());
return array_map(fn (FileInfo $fileInfo): ITrashItem => $this->mapTrashItem($fileInfo, $user), $entries);
}
public function getTrashRootItem(IUser $user, string $name): ?ITrashItem {
$entry = Helper::getTrashFile('/', $user->getUID(), $name);
if ($entry === null) {
return null;
}
return $this->mapTrashItem($entry, $user);
}
#[\Override]
public function listTrashFolder(ITrashItem $folder): array {
$user = $folder->getUser();
$entries = Helper::getTrashFiles($folder->getTrashPath(), $user->getUID());
return array_map(fn (FileInfo $fileInfo): ITrashItem => $this->mapTrashItem($fileInfo, $user, $folder), $entries);
}
#[\Override]
public function restoreItem(ITrashItem $item) {
Trashbin::restore($item->getTrashPath(), $item->getName(), $item->isRootItem() ? $item->getDeletedTime() : null);
}
#[\Override]
public function removeItem(ITrashItem $item) {
$user = $item->getUser();
if ($item->isRootItem()) {
$path = substr($item->getTrashPath(), 0, -strlen('.d' . $item->getDeletedTime()));
Trashbin::delete($path, $user->getUID(), $item->getDeletedTime());
} else {
Trashbin::delete($item->getTrashPath(), $user->getUID(), null);
}
}
#[\Override]
public function moveToTrash(IStorage $storage, string $internalPath): bool {
if (!$storage instanceof Storage) {
return false;
}
$normalized = Filesystem::normalizePath($storage->getMountPoint() . '/' . $internalPath, true, false, true);
$view = Filesystem::getView();
if (!isset($this->deletedFiles[$normalized]) && $view instanceof View) {
$this->deletedFiles[$normalized] = $normalized;
if ($filesPath = $view->getRelativePath($normalized)) {
$filesPath = trim($filesPath, '/');
$result = Trashbin::move2trash($filesPath);
} else {
$result = false;
}
unset($this->deletedFiles[$normalized]);
} else {
$result = false;
}
return $result;
}
#[\Override]
public function getTrashNodeById(IUser $user, int $fileId) {
try {
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$trash = $userFolder->getParent()->get('files_trashbin/files');
if ($trash instanceof Folder) {
return $trash->getFirstNodeById($fileId);
} else {
return null;
}
} catch (NotFoundException $e) {
return null;
}
}
/**
* Resolve the user that deleted a trash item. Files deleted by a federated share
* recipient only carry the recipient's remote cloud ID, which no local IUserManager
* backend can resolve, so fall back to a display-only user for the cloud ID in that
* case instead of leaving the item without an "Unknown" deleted by user.
*/
private function resolveDeletedBy(?string $uid): ?IUser {
if (!$uid) {
return null;
}
$user = $this->userManager->get($uid);
if ($user !== null) {
return $user;
}
try {
$cloudId = $this->cloudIdManager->resolveCloudId($uid);
} catch (\InvalidArgumentException $e) {
return null;
}
return $this->userManager->getFederatedUser($cloudId);
}
}