mirror of
https://github.com/nextcloud/server.git
synced 2026-05-19 08:25:56 -04:00
Merge pull request #50160 from nextcloud/backport/49552/stable28
[stable28] fix: improve checks for moving shares/storages into other mounts
This commit is contained in:
commit
97db4a7292
10 changed files with 152 additions and 36 deletions
|
|
@ -62,7 +62,7 @@ class TestViewDirectory extends \OC\Files\View {
|
|||
return $this->deletables[$path];
|
||||
}
|
||||
|
||||
public function rename($path1, $path2) {
|
||||
public function rename($path1, $path2, array $options = []) {
|
||||
return $this->canRename;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ class OwnershipTransferService {
|
|||
$view->mkdir($finalTarget);
|
||||
$finalTarget = $finalTarget . '/' . basename($sourcePath);
|
||||
}
|
||||
if ($view->rename($sourcePath, $finalTarget) === false) {
|
||||
if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
|
||||
throw new TransferOwnershipException("Could not transfer files.", 1);
|
||||
}
|
||||
if (!is_dir("$sourceUid/files")) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
import '@nextcloud/dialogs/style.css'
|
||||
import type { Folder, Node, View } from '@nextcloud/files'
|
||||
import type { IFilePickerButton } from '@nextcloud/dialogs'
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
|
||||
import type { MoveCopyResult } from './moveOrCopyActionUtils'
|
||||
|
||||
import { FilePickerClosed, getFilePickerBuilder, showError, showInfo, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
|
||||
|
|
@ -184,7 +184,18 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
|
|||
}
|
||||
// getting here means either no conflict, file was renamed to keep both files
|
||||
// in a conflict, or the selected file was chosen to be kept during the conflict
|
||||
await client.moveFile(currentPath, join(destinationPath, node.basename))
|
||||
try {
|
||||
await client.moveFile(currentPath, join(destinationPath, node.basename))
|
||||
} catch (error) {
|
||||
const parser = new DOMParser()
|
||||
const text = await (error as WebDAVClientError).response?.text()
|
||||
const message = parser.parseFromString(text ?? '', 'text/xml')
|
||||
.querySelector('message')?.textContent
|
||||
if (message) {
|
||||
showError(message)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
// Delete the node as it will be fetched again
|
||||
// when navigating to the destination folder
|
||||
emit('files:node:deleted', node)
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ Feature: transfer-ownership
|
|||
And user "user2" accepts last share
|
||||
When transferring ownership of path "test" from "user0" to "user1"
|
||||
Then the command failed with exit code 1
|
||||
And the command output contains the text "Could not transfer files."
|
||||
And the command error output contains the text "Moving a storage (user0/files/test) into another storage (user1) is not allowed"
|
||||
|
||||
Scenario: transferring ownership does not transfer received shares
|
||||
Given user "user0" exists
|
||||
|
|
|
|||
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -63,11 +63,14 @@ use OCP\Files\ForbiddenException;
|
|||
use OCP\Files\InvalidCharacterInPathException;
|
||||
use OCP\Files\InvalidDirectoryException;
|
||||
use OCP\Files\InvalidPathException;
|
||||
use OCP\Files\Mount\IMountManager;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\ReservedWordException;
|
||||
use OCP\Files\Storage\IStorage;
|
||||
use OCP\IL10N;
|
||||
use OCP\IUser;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCP\Lock\ILockingProvider;
|
||||
use OCP\Lock\LockedException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
|
@ -95,6 +98,7 @@ class View {
|
|||
private bool $updaterEnabled = true;
|
||||
private UserManager $userManager;
|
||||
private LoggerInterface $logger;
|
||||
private IL10N $l10n;
|
||||
|
||||
/**
|
||||
* @throws \Exception If $root contains an invalid path
|
||||
|
|
@ -109,6 +113,7 @@ class View {
|
|||
$this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
|
||||
$this->userManager = \OC::$server->getUserManager();
|
||||
$this->logger = \OC::$server->get(LoggerInterface::class);
|
||||
$this->l10n = \OC::$server->get(IFactory::class)->get('files');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -725,11 +730,14 @@ class View {
|
|||
*
|
||||
* @param string $source source path
|
||||
* @param string $target target path
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|mixed
|
||||
* @throws LockedException
|
||||
*/
|
||||
public function rename($source, $target) {
|
||||
public function rename($source, $target, array $options = []) {
|
||||
$checkSubMounts = $options['checkSubMounts'] ?? true;
|
||||
|
||||
$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
|
||||
$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
|
||||
|
||||
|
|
@ -737,6 +745,11 @@ class View {
|
|||
throw new ForbiddenException("Moving a folder into a child folder is forbidden", false);
|
||||
}
|
||||
|
||||
/** @var IMountManager $mountManager */
|
||||
$mountManager = \OC::$server->get(IMountManager::class);
|
||||
|
||||
$targetParts = explode('/', $absolutePath2);
|
||||
$targetUser = $targetParts[1] ?? null;
|
||||
$result = false;
|
||||
if (
|
||||
Filesystem::isValidPath($target)
|
||||
|
|
@ -788,24 +801,28 @@ class View {
|
|||
try {
|
||||
$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
|
||||
|
||||
if ($checkSubMounts) {
|
||||
$movedMounts = $mountManager->findIn($this->getAbsolutePath($source));
|
||||
} else {
|
||||
$movedMounts = [];
|
||||
}
|
||||
|
||||
if ($internalPath1 === '') {
|
||||
if ($mount1 instanceof MoveableMount) {
|
||||
$sourceParentMount = $this->getMount(dirname($source));
|
||||
if ($sourceParentMount === $mount2 && $this->targetIsNotShared($storage2, $internalPath2)) {
|
||||
/**
|
||||
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
|
||||
*/
|
||||
$sourceMountPoint = $mount1->getMountPoint();
|
||||
$result = $mount1->moveMount($absolutePath2);
|
||||
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
|
||||
} else {
|
||||
$result = false;
|
||||
}
|
||||
} else {
|
||||
$result = false;
|
||||
}
|
||||
$sourceParentMount = $this->getMount(dirname($source));
|
||||
$movedMounts[] = $mount1;
|
||||
$this->validateMountMove($movedMounts, $sourceParentMount, $mount2, !$this->targetIsNotShared($storage2, $internalPath2));
|
||||
/**
|
||||
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
|
||||
*/
|
||||
$sourceMountPoint = $mount1->getMountPoint();
|
||||
$result = $mount1->moveMount($absolutePath2);
|
||||
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
|
||||
|
||||
// moving a file/folder within the same mount point
|
||||
} elseif ($storage1 === $storage2) {
|
||||
if (count($movedMounts) > 0) {
|
||||
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($storage2, $absolutePath2));
|
||||
}
|
||||
if ($storage1) {
|
||||
$result = $storage1->rename($internalPath1, $internalPath2);
|
||||
} else {
|
||||
|
|
@ -813,6 +830,9 @@ class View {
|
|||
}
|
||||
// moving a file/folder between storages (from $storage1 to $storage2)
|
||||
} else {
|
||||
if (count($movedMounts) > 0) {
|
||||
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($storage2, $absolutePath2));
|
||||
}
|
||||
$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
|
||||
}
|
||||
|
||||
|
|
@ -862,6 +882,55 @@ class View {
|
|||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ForbiddenException
|
||||
*/
|
||||
private function validateMountMove(array $mounts, IMountPoint $sourceMount, IMountPoint $targetMount, bool $targetIsShared): void {
|
||||
$targetPath = $this->getRelativePath($targetMount->getMountPoint());
|
||||
if ($targetPath) {
|
||||
$targetPath = trim($targetPath, '/');
|
||||
} else {
|
||||
$targetPath = $targetMount->getMountPoint();
|
||||
}
|
||||
|
||||
foreach ($mounts as $mount) {
|
||||
$sourcePath = $this->getRelativePath($mount->getMountPoint());
|
||||
if ($sourcePath) {
|
||||
$sourcePath = trim($sourcePath, '/');
|
||||
} else {
|
||||
$sourcePath = $mount->getMountPoint();
|
||||
}
|
||||
|
||||
if (!$mount instanceof MoveableMount) {
|
||||
throw new ForbiddenException($this->l10n->t('Storage %s cannot be moved', [$sourcePath]), false);
|
||||
}
|
||||
|
||||
if ($targetIsShared) {
|
||||
if ($sourceMount instanceof SharedMount) {
|
||||
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into a shared folder is not allowed', [$sourcePath]), false);
|
||||
} else {
|
||||
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a shared folder is not allowed', [$sourcePath]), false);
|
||||
}
|
||||
}
|
||||
|
||||
if ($sourceMount !== $targetMount) {
|
||||
if ($sourceMount instanceof SharedMount) {
|
||||
if ($targetMount instanceof SharedMount) {
|
||||
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another share (%s) is not allowed', [$sourcePath, $targetPath]), false);
|
||||
} else {
|
||||
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
|
||||
}
|
||||
} else {
|
||||
if ($targetMount instanceof SharedMount) {
|
||||
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a share (%s) is not allowed', [$sourcePath, $targetPath]), false);
|
||||
} else {
|
||||
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file/folder from the source path to target path
|
||||
*
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use OCP\Cache\CappedMemoryCache;
|
|||
use OCP\Constants;
|
||||
use OCP\Files\Config\IMountProvider;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\ForbiddenException;
|
||||
use OCP\Files\GenericFileException;
|
||||
use OCP\Files\Mount\IMountManager;
|
||||
use OCP\Files\Storage\IStorage;
|
||||
|
|
@ -1594,6 +1595,9 @@ class ViewTest extends \Test\TestCase {
|
|||
$storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
|
||||
return new \OC\Files\Cache\Storage($storage, true, \OC::$server->get(IDBConnection::class));
|
||||
});
|
||||
$storage->method('getCache')->willReturnCallback(function () use ($storage) {
|
||||
return new \OC\Files\Cache\Cache($storage);
|
||||
});
|
||||
|
||||
$mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
|
||||
->setMethods(['moveMount'])
|
||||
|
|
@ -1638,10 +1642,7 @@ class ViewTest extends \Test\TestCase {
|
|||
$this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that moving a mount point into another is forbidden
|
||||
*/
|
||||
public function testMoveMountPointIntoAnother() {
|
||||
public function testMoveMountPointOverwrite(): void {
|
||||
self::loginAsUser($this->user);
|
||||
|
||||
[$mount1, $mount2] = $this->createTestMovableMountPoints([
|
||||
|
|
@ -1657,8 +1658,28 @@ class ViewTest extends \Test\TestCase {
|
|||
|
||||
$view = new View('/' . $this->user . '/files/');
|
||||
|
||||
$this->assertFalse($view->rename('mount1', 'mount2'), 'Cannot overwrite another mount point');
|
||||
$this->assertFalse($view->rename('mount1', 'mount2/sub'), 'Cannot move a mount point into another');
|
||||
$this->expectException(ForbiddenException::class);
|
||||
$view->rename('mount1', 'mount2');
|
||||
}
|
||||
|
||||
public function testMoveMountPointIntoMount(): void {
|
||||
self::loginAsUser($this->user);
|
||||
|
||||
[$mount1, $mount2] = $this->createTestMovableMountPoints([
|
||||
$this->user . '/files/mount1',
|
||||
$this->user . '/files/mount2',
|
||||
]);
|
||||
|
||||
$mount1->expects($this->never())
|
||||
->method('moveMount');
|
||||
|
||||
$mount2->expects($this->never())
|
||||
->method('moveMount');
|
||||
|
||||
$view = new View('/' . $this->user . '/files/');
|
||||
|
||||
$this->expectException(ForbiddenException::class);
|
||||
$view->rename('mount1', 'mount2/sub');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1693,9 +1714,24 @@ class ViewTest extends \Test\TestCase {
|
|||
->setNode($shareDir);
|
||||
$shareManager->createShare($share);
|
||||
|
||||
$this->assertFalse($view->rename('mount1', 'shareddir'), 'Cannot overwrite shared folder');
|
||||
$this->assertFalse($view->rename('mount1', 'shareddir/sub'), 'Cannot move mount point into shared folder');
|
||||
$this->assertFalse($view->rename('mount1', 'shareddir/sub/sub2'), 'Cannot move mount point into shared subfolder');
|
||||
try {
|
||||
$view->rename('mount1', 'shareddir');
|
||||
$this->fail('Cannot overwrite shared folder');
|
||||
} catch (ForbiddenException $e) {
|
||||
|
||||
}
|
||||
try {
|
||||
$view->rename('mount1', 'shareddir/sub');
|
||||
$this->fail('Cannot move mount point into shared folder');
|
||||
} catch (ForbiddenException $e) {
|
||||
|
||||
}
|
||||
try {
|
||||
$view->rename('mount1', 'shareddir/sub/sub2');
|
||||
$this->fail('Cannot move mount point into shared subfolder');
|
||||
} catch (ForbiddenException $e) {
|
||||
|
||||
}
|
||||
|
||||
$shareManager->deleteShare($share);
|
||||
$userObject->delete();
|
||||
|
|
|
|||
Loading…
Reference in a new issue