refactor(dav): Replace baseuri manipulation with RootCollection for public shares

Signed-off-by: provokateurin <kate@provokateurin.de>
pull/52182/head
provokateurin 2025-04-14 15:16:44 +07:00
parent e90e3a70fa
commit 7f0953d520
No known key found for this signature in database
12 changed files with 129 additions and 39 deletions

@ -68,7 +68,7 @@ $requestUri = Server::get(IRequest::class)->getRequestUri();
$linkCheckPlugin = new PublicLinkCheckPlugin();
$filesDropPlugin = new FilesDropPlugin();
$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
$server = $serverFactory->createServer(false, $baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
$isAjax = in_array('XMLHttpRequest', explode(',', $_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''));
/** @var FederatedShareProvider $shareProvider */
$federatedShareProvider = Server::get(FederatedShareProvider::class);

@ -68,7 +68,7 @@ $authPlugin->addBackend($bearerAuthPlugin);
$requestUri = Server::get(IRequest::class)->getRequestUri();
$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function () {
$server = $serverFactory->createServer(false, $baseuri, $requestUri, $authPlugin, function () {
// use the view for the logged in user
return Filesystem::getView();
});

@ -75,12 +75,8 @@ $serverFactory = new ServerFactory(
$linkCheckPlugin = new PublicLinkCheckPlugin();
$filesDropPlugin = new FilesDropPlugin();
// Define root url with /public.php/dav/files/TOKEN
/** @var string $baseuri defined in public.php */
preg_match('/(^files\/[a-z0-9-_]+)/i', substr($requestUri, strlen($baseuri)), $match);
$baseuri = $baseuri . $match[0];
$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
$server = $serverFactory->createServer(true, $baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
// GET must be allowed for e.g. showing images and allowing Zip downloads
if ($server->httpRequest->getMethod() !== 'GET') {
// If this is *not* a GET request we only allow access to public DAV from AJAX or when Server2Server is allowed

@ -282,6 +282,7 @@ return array(
'OCA\\DAV\\Files\\RootCollection' => $baseDir . '/../lib/Files/RootCollection.php',
'OCA\\DAV\\Files\\Sharing\\FilesDropPlugin' => $baseDir . '/../lib/Files/Sharing/FilesDropPlugin.php',
'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => $baseDir . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php',
'OCA\\DAV\\Files\\Sharing\\RootCollection' => $baseDir . '/../lib/Files/Sharing/RootCollection.php',
'OCA\\DAV\\Listener\\ActivityUpdaterListener' => $baseDir . '/../lib/Listener/ActivityUpdaterListener.php',
'OCA\\DAV\\Listener\\AddMissingIndicesListener' => $baseDir . '/../lib/Listener/AddMissingIndicesListener.php',
'OCA\\DAV\\Listener\\AddressbookListener' => $baseDir . '/../lib/Listener/AddressbookListener.php',

@ -297,6 +297,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Files\\RootCollection' => __DIR__ . '/..' . '/../lib/Files/RootCollection.php',
'OCA\\DAV\\Files\\Sharing\\FilesDropPlugin' => __DIR__ . '/..' . '/../lib/Files/Sharing/FilesDropPlugin.php',
'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => __DIR__ . '/..' . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php',
'OCA\\DAV\\Files\\Sharing\\RootCollection' => __DIR__ . '/..' . '/../lib/Files/Sharing/RootCollection.php',
'OCA\\DAV\\Listener\\ActivityUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/ActivityUpdaterListener.php',
'OCA\\DAV\\Listener\\AddMissingIndicesListener' => __DIR__ . '/..' . '/../lib/Listener/AddMissingIndicesListener.php',
'OCA\\DAV\\Listener\\AddressbookListener' => __DIR__ . '/..' . '/../lib/Listener/AddressbookListener.php',

@ -13,7 +13,9 @@ use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCA\DAV\Storage\PublicShareWrapper;
use OCP\App\IAppManager;
use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\ForbiddenException;
@ -172,7 +174,19 @@ class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuot
* @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
if (!$this->info->isReadable()) {
$storage = $this->info->getStorage();
$allowDirectory = false;
if ($storage instanceof PublicShareWrapper) {
$share = $storage->getShare();
$allowDirectory =
// Only allow directories for file drops
($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ &&
// And only allow it for directories which are a direct child of the share root
$this->info->getId() === $share->getNodeId();
}
// For file drop we need to be allowed to read the directory with the nickname
if (!$allowDirectory && !$this->info->isReadable()) {
// avoid detecting files through this way
throw new NotFound();
}
@ -198,6 +212,11 @@ class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuot
if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
} else {
// In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
if (!$this->info->isReadable()) {
throw new NotFound();
}
$node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
}
if ($this->tree) {

@ -720,15 +720,15 @@ class FilesPlugin extends ServerPlugin {
*/
public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
if (!$this->server->tree->nodeExists($filePath)) {
return;
}
$node = $this->server->tree->getNodeForPath($filePath);
if ($node instanceof Node) {
$fileId = $node->getFileId();
if (!is_null($fileId)) {
$this->server->httpResponse->setHeader('OC-FileId', $fileId);
try {
$node = $this->server->tree->getNodeForPath($filePath);
if ($node instanceof Node) {
$fileId = $node->getFileId();
if (!is_null($fileId)) {
$this->server->httpResponse->setHeader('OC-FileId', $fileId);
}
}
} catch (NotFound) {
}
}
}

@ -8,8 +8,10 @@
namespace OCA\DAV\Connector\Sabre;
use OC\Files\View;
use OC\KnownUser\KnownUserService;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Files\BrowserErrorPagePlugin;
@ -28,12 +30,14 @@ use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\ITagManager;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\SabrePluginEvent;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Plugin;
use Sabre\DAV\SimpleCollection;
class ServerFactory {
@ -54,13 +58,22 @@ class ServerFactory {
/**
* @param callable $viewCallBack callback that should return the view for the dav endpoint
*/
public function createServer(string $baseUri,
public function createServer(
bool $isPublicShare,
string $baseUri,
string $requestUri,
Plugin $authPlugin,
callable $viewCallBack): Server {
callable $viewCallBack,
): Server {
// Fire up server
$objectTree = new ObjectTree();
$server = new Server($objectTree);
if ($isPublicShare) {
$rootCollection = new SimpleCollection('root');
$tree = new CachingTree($rootCollection);
} else {
$rootCollection = null;
$tree = new ObjectTree();
}
$server = new Server($tree);
// Set URL explicitly due to reverse-proxy situations
$server->httpRequest->setUrl($requestUri);
$server->setBaseUri($baseUri);
@ -81,7 +94,7 @@ class ServerFactory {
$server->addPlugin(new RequestIdHeaderPlugin($this->request));
$server->addPlugin(new ZipFolderPlugin(
$objectTree,
$tree,
$this->logger,
$this->eventDispatcher,
));
@ -101,7 +114,7 @@ class ServerFactory {
}
// wait with registering these until auth is handled and the filesystem is setup
$server->on('beforeMethod:*', function () use ($server, $objectTree, $viewCallBack): void {
$server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void {
// ensure the skeleton is copied
$userFolder = \OC::$server->getUserFolder();
@ -115,15 +128,39 @@ class ServerFactory {
// Create Nextcloud Dir
if ($rootInfo->getType() === 'dir') {
$root = new Directory($view, $rootInfo, $objectTree);
$root = new Directory($view, $rootInfo, $tree);
} else {
$root = new File($view, $rootInfo);
}
$objectTree->init($root, $view, $this->mountManager);
if ($isPublicShare) {
$userPrincipalBackend = new Principal(
\OCP\Server::get(IUserManager::class),
\OCP\Server::get(IGroupManager::class),
\OCP\Server::get(IAccountManager::class),
\OCP\Server::get(\OCP\Share\IManager::class),
\OCP\Server::get(IUserSession::class),
\OCP\Server::get(IAppManager::class),
\OCP\Server::get(ProxyMapper::class),
\OCP\Server::get(KnownUserService::class),
\OCP\Server::get(IConfig::class),
\OC::$server->getL10NFactory(),
);
// Mount the share collection at /public.php/dav/shares/<share token>
$rootCollection->addChild(new \OCA\DAV\Files\Sharing\RootCollection(
$root,
$userPrincipalBackend,
'principals/shares',
));
} else {
/** @var ObjectTree $tree */
$tree->init($root, $view, $this->mountManager);
}
$server->addPlugin(
new FilesPlugin(
$objectTree,
$tree,
$this->config,
$this->request,
$this->previewManager,
@ -143,16 +180,16 @@ class ServerFactory {
));
if ($this->userSession->isLoggedIn()) {
$server->addPlugin(new TagsPlugin($objectTree, $this->tagManager, $this->eventDispatcher, $this->userSession));
$server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession));
$server->addPlugin(new SharesPlugin(
$objectTree,
$tree,
$this->userSession,
$userFolder,
\OCP\Server::get(\OCP\Share\IManager::class)
));
$server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession));
$server->addPlugin(new FilesReportPlugin(
$objectTree,
$tree,
$view,
\OCP\Server::get(ISystemTagManager::class),
\OCP\Server::get(ISystemTagObjectMapper::class),
@ -167,7 +204,7 @@ class ServerFactory {
new \Sabre\DAV\PropertyStorage\Plugin(
new CustomPropertiesBackend(
$server,
$objectTree,
$tree,
$this->databaseConnection,
$this->userSession->getUser(),
\OCP\Server::get(DefaultCalendarValidator::class),

@ -73,7 +73,7 @@ class FilesDropPlugin extends ServerPlugin {
if ($isFileRequest && ($nickName == null || trim($nickName) === '')) {
throw new MethodNotAllowed('Nickname is required for file requests');
}
// If this is a file request we need to create a folder for the user
if ($isFileRequest) {
// Check if the folder already exists
@ -83,9 +83,9 @@ class FilesDropPlugin extends ServerPlugin {
// Put all files in the subfolder
$path = $nickName . '/' . $path;
}
$newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
$url = $request->getBaseUrl() . $newName;
$url = $request->getBaseUrl() . '/files/' . $this->share->getToken() . $newName;
$request->setUrl($url);
}

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Files\Sharing;
use Sabre\DAV\INode;
use Sabre\DAVACL\AbstractPrincipalCollection;
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class RootCollection extends AbstractPrincipalCollection {
public function __construct(
private INode $root,
BackendInterface $principalBackend,
string $principalPrefix = 'principals',
) {
parent::__construct($principalBackend, $principalPrefix);
}
public function getChildForPrincipal(array $principalInfo): INode {
return $this->root;
}
public function getName() {
return 'files';
}
}

@ -137,7 +137,7 @@ abstract class RequestTestCase extends TestCase {
$authBackend = new Auth($user, $password);
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
$server = $this->serverFactory->createServer('/', 'dummy', $authPlugin, function () use ($view) {
$server = $this->serverFactory->createServer(false, '/', 'dummy', $authPlugin, function () use ($view) {
return $view;
});
$server->addPlugin($exceptionPlugin);

@ -53,6 +53,10 @@ class FilesDropPluginTest extends TestCase {
$this->share->expects($this->any())
->method('getAttributes')
->willReturn($attributes);
$this->share
->method('getToken')
->willReturn('token');
}
public function testInitialize(): void {
@ -86,7 +90,7 @@ class FilesDropPluginTest extends TestCase {
->willReturn('PUT');
$this->request->method('getPath')
->willReturn('file.txt');
->willReturn('/files/token/file.txt');
$this->request->method('getBaseUrl')
->willReturn('https://example.com');
@ -97,7 +101,7 @@ class FilesDropPluginTest extends TestCase {
$this->request->expects($this->once())
->method('setUrl')
->with('https://example.com/file.txt');
->with('https://example.com/files/token/file.txt');
$this->plugin->beforeMethod($this->request, $this->response);
}
@ -111,7 +115,7 @@ class FilesDropPluginTest extends TestCase {
->willReturn('PUT');
$this->request->method('getPath')
->willReturn('file.txt');
->willReturn('/files/token/file.txt');
$this->request->method('getBaseUrl')
->willReturn('https://example.com');
@ -127,7 +131,7 @@ class FilesDropPluginTest extends TestCase {
$this->request->expects($this->once())
->method('setUrl')
->with($this->equalTo('https://example.com/file (2).txt'));
->with($this->equalTo('https://example.com/files/token/file (2).txt'));
$this->plugin->beforeMethod($this->request, $this->response);
}
@ -154,7 +158,7 @@ class FilesDropPluginTest extends TestCase {
->willReturn('PUT');
$this->request->method('getPath')
->willReturn('folder/file.txt');
->willReturn('/files/token/folder/file.txt');
$this->request->method('getBaseUrl')
->willReturn('https://example.com');
@ -170,7 +174,7 @@ class FilesDropPluginTest extends TestCase {
$this->request->expects($this->once())
->method('setUrl')
->with($this->equalTo('https://example.com/file (2).txt'));
->with($this->equalTo('https://example.com/files/token/file (2).txt'));
$this->plugin->beforeMethod($this->request, $this->response);
}