Skip to content

Commit ca2e296

Browse files
authored
Merge pull request #49552 from nextcloud/mount-move-checks
fix: improve checks for moving shares/storages into other mounts
2 parents 92acfef + 757076a commit ca2e296

File tree

10 files changed

+147
-36
lines changed

10 files changed

+147
-36
lines changed

apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function isDeletable($path) {
4242
return $this->deletables[$path];
4343
}
4444

45-
public function rename($path1, $path2) {
45+
public function rename($path1, $path2, array $options = []) {
4646
return $this->canRename;
4747
}
4848

apps/files/lib/Service/OwnershipTransferService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ protected function transferFiles(string $sourceUid,
397397
$view->mkdir($finalTarget);
398398
$finalTarget = $finalTarget . '/' . basename($sourcePath);
399399
}
400-
if ($view->rename($sourcePath, $finalTarget) === false) {
400+
if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
401401
throw new TransferOwnershipException('Could not transfer files.', 1);
402402
}
403403
if (!is_dir("$sourceUid/files")) {

apps/files/src/actions/moveOrCopyAction.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
import type { Folder, Node, View } from '@nextcloud/files'
66
import type { IFilePickerButton } from '@nextcloud/dialogs'
7-
import type { FileStat, ResponseDataDetailed } from 'webdav'
7+
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
88
import type { MoveCopyResult } from './moveOrCopyActionUtils'
99

1010
import { isAxiosError } from '@nextcloud/axios'
@@ -165,7 +165,18 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
165165
}
166166
// getting here means either no conflict, file was renamed to keep both files
167167
// in a conflict, or the selected file was chosen to be kept during the conflict
168-
await client.moveFile(currentPath, join(destinationPath, node.basename))
168+
try {
169+
await client.moveFile(currentPath, join(destinationPath, node.basename))
170+
} catch (error) {
171+
const parser = new DOMParser()
172+
const text = await (error as WebDAVClientError).response?.text()
173+
const message = parser.parseFromString(text ?? '', 'text/xml')
174+
.querySelector('message')?.textContent
175+
if (message) {
176+
showError(message)
177+
}
178+
throw error
179+
}
169180
// Delete the node as it will be fetched again
170181
// when navigating to the destination folder
171182
emit('files:node:deleted', node)

build/integration/files_features/transfer-ownership.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ Feature: transfer-ownership
514514
And user "user2" accepts last share
515515
When transferring ownership of path "test" from "user0" to "user1"
516516
Then the command failed with exit code 1
517-
And the command output contains the text "Could not transfer files."
517+
And the command error output contains the text "Moving a storage (user0/files/test) into another storage (user1) is not allowed"
518518

519519
Scenario: transferring ownership does not transfer received shares
520520
Given user "user0" exists

dist/files-init.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-init.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-main.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-main.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/private/Files/View.php

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
use OCP\Files\InvalidCharacterInPathException;
2525
use OCP\Files\InvalidDirectoryException;
2626
use OCP\Files\InvalidPathException;
27+
use OCP\Files\Mount\IMountManager;
2728
use OCP\Files\Mount\IMountPoint;
2829
use OCP\Files\NotFoundException;
2930
use OCP\Files\ReservedWordException;
31+
use OCP\IL10N;
3032
use OCP\IUser;
3133
use OCP\IUserManager;
34+
use OCP\L10N\IFactory;
3235
use OCP\Lock\ILockingProvider;
3336
use OCP\Lock\LockedException;
3437
use OCP\Server;
@@ -59,6 +62,7 @@ class View {
5962
private bool $updaterEnabled = true;
6063
private UserManager $userManager;
6164
private LoggerInterface $logger;
65+
private IL10N $l10n;
6266

6367
/**
6468
* @throws \Exception If $root contains an invalid path
@@ -73,6 +77,7 @@ public function __construct(string $root = '') {
7377
$this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
7478
$this->userManager = \OC::$server->getUserManager();
7579
$this->logger = \OC::$server->get(LoggerInterface::class);
80+
$this->l10n = \OC::$server->get(IFactory::class)->get('files');
7681
}
7782

7883
/**
@@ -695,18 +700,24 @@ public function deleteAll($directory) {
695700
*
696701
* @param string $source source path
697702
* @param string $target target path
703+
* @param array $options
698704
*
699705
* @return bool|mixed
700706
* @throws LockedException
701707
*/
702-
public function rename($source, $target) {
708+
public function rename($source, $target, array $options = []) {
709+
$checkSubMounts = $options['checkSubMounts'] ?? true;
710+
703711
$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
704712
$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
705713

706714
if (str_starts_with($absolutePath2, $absolutePath1 . '/')) {
707715
throw new ForbiddenException('Moving a folder into a child folder is forbidden', false);
708716
}
709717

718+
/** @var IMountManager $mountManager */
719+
$mountManager = \OC::$server->get(IMountManager::class);
720+
710721
$targetParts = explode('/', $absolutePath2);
711722
$targetUser = $targetParts[1] ?? null;
712723
$result = false;
@@ -764,31 +775,38 @@ public function rename($source, $target) {
764775
try {
765776
$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
766777

778+
if ($checkSubMounts) {
779+
$movedMounts = $mountManager->findIn($this->getAbsolutePath($source));
780+
} else {
781+
$movedMounts = [];
782+
}
783+
767784
if ($internalPath1 === '') {
768-
if ($mount1 instanceof MoveableMount) {
769-
$sourceParentMount = $this->getMount(dirname($source));
770-
if ($sourceParentMount === $mount2 && $this->targetIsNotShared($targetUser, $absolutePath2)) {
771-
/**
772-
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
773-
*/
774-
$sourceMountPoint = $mount1->getMountPoint();
775-
$result = $mount1->moveMount($absolutePath2);
776-
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
777-
} else {
778-
$result = false;
779-
}
780-
} else {
781-
$result = false;
782-
}
785+
$sourceParentMount = $this->getMount(dirname($source));
786+
$movedMounts[] = $mount1;
787+
$this->validateMountMove($movedMounts, $sourceParentMount, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
788+
/**
789+
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
790+
*/
791+
$sourceMountPoint = $mount1->getMountPoint();
792+
$result = $mount1->moveMount($absolutePath2);
793+
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
794+
783795
// moving a file/folder within the same mount point
784796
} elseif ($storage1 === $storage2) {
797+
if (count($movedMounts) > 0) {
798+
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
799+
}
785800
if ($storage1) {
786801
$result = $storage1->rename($internalPath1, $internalPath2);
787802
} else {
788803
$result = false;
789804
}
790805
// moving a file/folder between storages (from $storage1 to $storage2)
791806
} else {
807+
if (count($movedMounts) > 0) {
808+
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
809+
}
792810
$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
793811
}
794812

@@ -838,6 +856,55 @@ public function rename($source, $target) {
838856
return $result;
839857
}
840858

859+
/**
860+
* @throws ForbiddenException
861+
*/
862+
private function validateMountMove(array $mounts, IMountPoint $sourceMount, IMountPoint $targetMount, bool $targetIsShared): void {
863+
$targetPath = $this->getRelativePath($targetMount->getMountPoint());
864+
if ($targetPath) {
865+
$targetPath = trim($targetPath, '/');
866+
} else {
867+
$targetPath = $targetMount->getMountPoint();
868+
}
869+
870+
foreach ($mounts as $mount) {
871+
$sourcePath = $this->getRelativePath($mount->getMountPoint());
872+
if ($sourcePath) {
873+
$sourcePath = trim($sourcePath, '/');
874+
} else {
875+
$sourcePath = $mount->getMountPoint();
876+
}
877+
878+
if (!$mount instanceof MoveableMount) {
879+
throw new ForbiddenException($this->l10n->t('Storage %s cannot be moved', [$sourcePath]), false);
880+
}
881+
882+
if ($targetIsShared) {
883+
if ($sourceMount instanceof SharedMount) {
884+
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into a shared folder is not allowed', [$sourcePath]), false);
885+
} else {
886+
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a shared folder is not allowed', [$sourcePath]), false);
887+
}
888+
}
889+
890+
if ($sourceMount !== $targetMount) {
891+
if ($sourceMount instanceof SharedMount) {
892+
if ($targetMount instanceof SharedMount) {
893+
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another share (%s) is not allowed', [$sourcePath, $targetPath]), false);
894+
} else {
895+
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
896+
}
897+
} else {
898+
if ($targetMount instanceof SharedMount) {
899+
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a share (%s) is not allowed', [$sourcePath, $targetPath]), false);
900+
} else {
901+
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
902+
}
903+
}
904+
}
905+
}
906+
}
907+
841908
/**
842909
* Copy a file/folder from the source path to target path
843910
*

tests/lib/Files/ViewTest.php

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use OCP\Constants;
2222
use OCP\Files\Config\IMountProvider;
2323
use OCP\Files\FileInfo;
24+
use OCP\Files\ForbiddenException;
2425
use OCP\Files\GenericFileException;
2526
use OCP\Files\Mount\IMountManager;
2627
use OCP\Files\Storage\IStorage;
@@ -1641,10 +1642,27 @@ public function testMountPointMove(): void {
16411642
$this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
16421643
}
16431644

1644-
/**
1645-
* Test that moving a mount point into another is forbidden
1646-
*/
1647-
public function testMoveMountPointIntoAnother(): void {
1645+
public function testMoveMountPointOverwrite(): void {
1646+
self::loginAsUser($this->user);
1647+
1648+
[$mount1, $mount2] = $this->createTestMovableMountPoints([
1649+
$this->user . '/files/mount1',
1650+
$this->user . '/files/mount2',
1651+
]);
1652+
1653+
$mount1->expects($this->never())
1654+
->method('moveMount');
1655+
1656+
$mount2->expects($this->never())
1657+
->method('moveMount');
1658+
1659+
$view = new View('/' . $this->user . '/files/');
1660+
1661+
$this->expectException(ForbiddenException::class);
1662+
$view->rename('mount1', 'mount2');
1663+
}
1664+
1665+
public function testMoveMountPointIntoMount(): void {
16481666
self::loginAsUser($this->user);
16491667

16501668
[$mount1, $mount2] = $this->createTestMovableMountPoints([
@@ -1660,8 +1678,8 @@ public function testMoveMountPointIntoAnother(): void {
16601678

16611679
$view = new View('/' . $this->user . '/files/');
16621680

1663-
$this->assertFalse($view->rename('mount1', 'mount2'), 'Cannot overwrite another mount point');
1664-
$this->assertFalse($view->rename('mount1', 'mount2/sub'), 'Cannot move a mount point into another');
1681+
$this->expectException(ForbiddenException::class);
1682+
$view->rename('mount1', 'mount2/sub');
16651683
}
16661684

16671685
/**
@@ -1703,9 +1721,24 @@ public function testMoveMountPointIntoSharedFolder(): void {
17031721
->setNode($shareDir);
17041722
$shareManager->createShare($share);
17051723

1706-
$this->assertFalse($view->rename('mount1', 'shareddir'), 'Cannot overwrite shared folder');
1707-
$this->assertFalse($view->rename('mount1', 'shareddir/sub'), 'Cannot move mount point into shared folder');
1708-
$this->assertFalse($view->rename('mount1', 'shareddir/sub/sub2'), 'Cannot move mount point into shared subfolder');
1724+
try {
1725+
$view->rename('mount1', 'shareddir');
1726+
$this->fail('Cannot overwrite shared folder');
1727+
} catch (ForbiddenException $e) {
1728+
1729+
}
1730+
try {
1731+
$view->rename('mount1', 'shareddir/sub');
1732+
$this->fail('Cannot move mount point into shared folder');
1733+
} catch (ForbiddenException $e) {
1734+
1735+
}
1736+
try {
1737+
$view->rename('mount1', 'shareddir/sub/sub2');
1738+
$this->fail('Cannot move mount point into shared subfolder');
1739+
} catch (ForbiddenException $e) {
1740+
1741+
}
17091742
$this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');
17101743

17111744
$shareManager->deleteShare($share);

0 commit comments

Comments
 (0)