Skip to content

Commit 97db4a7

Browse files
authored
Merge pull request #50160 from nextcloud/backport/49552/stable28
[stable28] fix: improve checks for moving shares/storages into other mounts
2 parents 656c941 + 6f148a5 commit 97db4a7

File tree

10 files changed

+152
-36
lines changed

10 files changed

+152
-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
@@ -62,7 +62,7 @@ public function isDeletable($path) {
6262
return $this->deletables[$path];
6363
}
6464

65-
public function rename($path1, $path2) {
65+
public function rename($path1, $path2, array $options = []) {
6666
return $this->canRename;
6767
}
6868

apps/files/lib/Service/OwnershipTransferService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ protected function transferFiles(string $sourceUid,
416416
$view->mkdir($finalTarget);
417417
$finalTarget = $finalTarget . '/' . basename($sourcePath);
418418
}
419-
if ($view->rename($sourcePath, $finalTarget) === false) {
419+
if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
420420
throw new TransferOwnershipException("Could not transfer files.", 1);
421421
}
422422
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
@@ -22,7 +22,7 @@
2222
import '@nextcloud/dialogs/style.css'
2323
import type { Folder, Node, View } from '@nextcloud/files'
2424
import type { IFilePickerButton } from '@nextcloud/dialogs'
25-
import type { FileStat, ResponseDataDetailed } from 'webdav'
25+
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
2626
import type { MoveCopyResult } from './moveOrCopyActionUtils'
2727

2828
import { FilePickerClosed, getFilePickerBuilder, showError, showInfo, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
@@ -184,7 +184,18 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
184184
}
185185
// getting here means either no conflict, file was renamed to keep both files
186186
// in a conflict, or the selected file was chosen to be kept during the conflict
187-
await client.moveFile(currentPath, join(destinationPath, node.basename))
187+
try {
188+
await client.moveFile(currentPath, join(destinationPath, node.basename))
189+
} catch (error) {
190+
const parser = new DOMParser()
191+
const text = await (error as WebDAVClientError).response?.text()
192+
const message = parser.parseFromString(text ?? '', 'text/xml')
193+
.querySelector('message')?.textContent
194+
if (message) {
195+
showError(message)
196+
}
197+
throw error
198+
}
188199
// Delete the node as it will be fetched again
189200
// when navigating to the destination folder
190201
emit('files:node:deleted', node)

build/integration/features/transfer-ownership.feature

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

516516
Scenario: transferring ownership does not transfer received shares
517517
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: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@
6363
use OCP\Files\InvalidCharacterInPathException;
6464
use OCP\Files\InvalidDirectoryException;
6565
use OCP\Files\InvalidPathException;
66+
use OCP\Files\Mount\IMountManager;
6667
use OCP\Files\Mount\IMountPoint;
6768
use OCP\Files\NotFoundException;
6869
use OCP\Files\ReservedWordException;
6970
use OCP\Files\Storage\IStorage;
71+
use OCP\IL10N;
7072
use OCP\IUser;
73+
use OCP\L10N\IFactory;
7174
use OCP\Lock\ILockingProvider;
7275
use OCP\Lock\LockedException;
7376
use Psr\Log\LoggerInterface;
@@ -95,6 +98,7 @@ class View {
9598
private bool $updaterEnabled = true;
9699
private UserManager $userManager;
97100
private LoggerInterface $logger;
101+
private IL10N $l10n;
98102

99103
/**
100104
* @throws \Exception If $root contains an invalid path
@@ -109,6 +113,7 @@ public function __construct(string $root = '') {
109113
$this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
110114
$this->userManager = \OC::$server->getUserManager();
111115
$this->logger = \OC::$server->get(LoggerInterface::class);
116+
$this->l10n = \OC::$server->get(IFactory::class)->get('files');
112117
}
113118

114119
/**
@@ -725,18 +730,26 @@ public function deleteAll($directory) {
725730
*
726731
* @param string $source source path
727732
* @param string $target target path
733+
* @param array $options
728734
*
729735
* @return bool|mixed
730736
* @throws LockedException
731737
*/
732-
public function rename($source, $target) {
738+
public function rename($source, $target, array $options = []) {
739+
$checkSubMounts = $options['checkSubMounts'] ?? true;
740+
733741
$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
734742
$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
735743

736744
if (str_starts_with($absolutePath2, $absolutePath1 . '/')) {
737745
throw new ForbiddenException("Moving a folder into a child folder is forbidden", false);
738746
}
739747

748+
/** @var IMountManager $mountManager */
749+
$mountManager = \OC::$server->get(IMountManager::class);
750+
751+
$targetParts = explode('/', $absolutePath2);
752+
$targetUser = $targetParts[1] ?? null;
740753
$result = false;
741754
if (
742755
Filesystem::isValidPath($target)
@@ -788,31 +801,38 @@ public function rename($source, $target) {
788801
try {
789802
$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
790803

804+
if ($checkSubMounts) {
805+
$movedMounts = $mountManager->findIn($this->getAbsolutePath($source));
806+
} else {
807+
$movedMounts = [];
808+
}
809+
791810
if ($internalPath1 === '') {
792-
if ($mount1 instanceof MoveableMount) {
793-
$sourceParentMount = $this->getMount(dirname($source));
794-
if ($sourceParentMount === $mount2 && $this->targetIsNotShared($storage2, $internalPath2)) {
795-
/**
796-
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
797-
*/
798-
$sourceMountPoint = $mount1->getMountPoint();
799-
$result = $mount1->moveMount($absolutePath2);
800-
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
801-
} else {
802-
$result = false;
803-
}
804-
} else {
805-
$result = false;
806-
}
811+
$sourceParentMount = $this->getMount(dirname($source));
812+
$movedMounts[] = $mount1;
813+
$this->validateMountMove($movedMounts, $sourceParentMount, $mount2, !$this->targetIsNotShared($storage2, $internalPath2));
814+
/**
815+
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
816+
*/
817+
$sourceMountPoint = $mount1->getMountPoint();
818+
$result = $mount1->moveMount($absolutePath2);
819+
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
820+
807821
// moving a file/folder within the same mount point
808822
} elseif ($storage1 === $storage2) {
823+
if (count($movedMounts) > 0) {
824+
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($storage2, $absolutePath2));
825+
}
809826
if ($storage1) {
810827
$result = $storage1->rename($internalPath1, $internalPath2);
811828
} else {
812829
$result = false;
813830
}
814831
// moving a file/folder between storages (from $storage1 to $storage2)
815832
} else {
833+
if (count($movedMounts) > 0) {
834+
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($storage2, $absolutePath2));
835+
}
816836
$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
817837
}
818838

@@ -862,6 +882,55 @@ public function rename($source, $target) {
862882
return $result;
863883
}
864884

885+
/**
886+
* @throws ForbiddenException
887+
*/
888+
private function validateMountMove(array $mounts, IMountPoint $sourceMount, IMountPoint $targetMount, bool $targetIsShared): void {
889+
$targetPath = $this->getRelativePath($targetMount->getMountPoint());
890+
if ($targetPath) {
891+
$targetPath = trim($targetPath, '/');
892+
} else {
893+
$targetPath = $targetMount->getMountPoint();
894+
}
895+
896+
foreach ($mounts as $mount) {
897+
$sourcePath = $this->getRelativePath($mount->getMountPoint());
898+
if ($sourcePath) {
899+
$sourcePath = trim($sourcePath, '/');
900+
} else {
901+
$sourcePath = $mount->getMountPoint();
902+
}
903+
904+
if (!$mount instanceof MoveableMount) {
905+
throw new ForbiddenException($this->l10n->t('Storage %s cannot be moved', [$sourcePath]), false);
906+
}
907+
908+
if ($targetIsShared) {
909+
if ($sourceMount instanceof SharedMount) {
910+
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into a shared folder is not allowed', [$sourcePath]), false);
911+
} else {
912+
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a shared folder is not allowed', [$sourcePath]), false);
913+
}
914+
}
915+
916+
if ($sourceMount !== $targetMount) {
917+
if ($sourceMount instanceof SharedMount) {
918+
if ($targetMount instanceof SharedMount) {
919+
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another share (%s) is not allowed', [$sourcePath, $targetPath]), false);
920+
} else {
921+
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
922+
}
923+
} else {
924+
if ($targetMount instanceof SharedMount) {
925+
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a share (%s) is not allowed', [$sourcePath, $targetPath]), false);
926+
} else {
927+
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
928+
}
929+
}
930+
}
931+
}
932+
}
933+
865934
/**
866935
* Copy a file/folder from the source path to target path
867936
*

tests/lib/Files/ViewTest.php

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use OCP\Constants;
2121
use OCP\Files\Config\IMountProvider;
2222
use OCP\Files\FileInfo;
23+
use OCP\Files\ForbiddenException;
2324
use OCP\Files\GenericFileException;
2425
use OCP\Files\Mount\IMountManager;
2526
use OCP\Files\Storage\IStorage;
@@ -1594,6 +1595,9 @@ private function createTestMovableMountPoints($mountPoints) {
15941595
$storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
15951596
return new \OC\Files\Cache\Storage($storage, true, \OC::$server->get(IDBConnection::class));
15961597
});
1598+
$storage->method('getCache')->willReturnCallback(function () use ($storage) {
1599+
return new \OC\Files\Cache\Cache($storage);
1600+
});
15971601

15981602
$mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
15991603
->setMethods(['moveMount'])
@@ -1638,10 +1642,7 @@ public function testMountPointMove() {
16381642
$this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
16391643
}
16401644

1641-
/**
1642-
* Test that moving a mount point into another is forbidden
1643-
*/
1644-
public function testMoveMountPointIntoAnother() {
1645+
public function testMoveMountPointOverwrite(): void {
16451646
self::loginAsUser($this->user);
16461647

16471648
[$mount1, $mount2] = $this->createTestMovableMountPoints([
@@ -1657,8 +1658,28 @@ public function testMoveMountPointIntoAnother() {
16571658

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

1660-
$this->assertFalse($view->rename('mount1', 'mount2'), 'Cannot overwrite another mount point');
1661-
$this->assertFalse($view->rename('mount1', 'mount2/sub'), 'Cannot move a mount point into another');
1661+
$this->expectException(ForbiddenException::class);
1662+
$view->rename('mount1', 'mount2');
1663+
}
1664+
1665+
public function testMoveMountPointIntoMount(): void {
1666+
self::loginAsUser($this->user);
1667+
1668+
[$mount1, $mount2] = $this->createTestMovableMountPoints([
1669+
$this->user . '/files/mount1',
1670+
$this->user . '/files/mount2',
1671+
]);
1672+
1673+
$mount1->expects($this->never())
1674+
->method('moveMount');
1675+
1676+
$mount2->expects($this->never())
1677+
->method('moveMount');
1678+
1679+
$view = new View('/' . $this->user . '/files/');
1680+
1681+
$this->expectException(ForbiddenException::class);
1682+
$view->rename('mount1', 'mount2/sub');
16621683
}
16631684

16641685
/**
@@ -1693,9 +1714,24 @@ public function testMoveMountPointIntoSharedFolder() {
16931714
->setNode($shareDir);
16941715
$shareManager->createShare($share);
16951716

1696-
$this->assertFalse($view->rename('mount1', 'shareddir'), 'Cannot overwrite shared folder');
1697-
$this->assertFalse($view->rename('mount1', 'shareddir/sub'), 'Cannot move mount point into shared folder');
1698-
$this->assertFalse($view->rename('mount1', 'shareddir/sub/sub2'), 'Cannot move mount point into shared subfolder');
1717+
try {
1718+
$view->rename('mount1', 'shareddir');
1719+
$this->fail('Cannot overwrite shared folder');
1720+
} catch (ForbiddenException $e) {
1721+
1722+
}
1723+
try {
1724+
$view->rename('mount1', 'shareddir/sub');
1725+
$this->fail('Cannot move mount point into shared folder');
1726+
} catch (ForbiddenException $e) {
1727+
1728+
}
1729+
try {
1730+
$view->rename('mount1', 'shareddir/sub/sub2');
1731+
$this->fail('Cannot move mount point into shared subfolder');
1732+
} catch (ForbiddenException $e) {
1733+
1734+
}
16991735

17001736
$shareManager->deleteShare($share);
17011737
$userObject->delete();

0 commit comments

Comments
 (0)