diff options
Diffstat (limited to 'apps/dav/lib/Files/Sharing')
-rw-r--r-- | apps/dav/lib/Files/Sharing/FilesDropPlugin.php | 206 | ||||
-rw-r--r-- | apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php | 26 | ||||
-rw-r--r-- | apps/dav/lib/Files/Sharing/RootCollection.php | 32 |
3 files changed, 197 insertions, 67 deletions
diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php index 3ac541bbfd9..a3dbd32ce6b 100644 --- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php +++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php @@ -1,30 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Files\Sharing; -use OC\Files\View; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use OCP\Share\IShare; +use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; @@ -35,51 +20,180 @@ use Sabre\HTTP\ResponseInterface; */ class FilesDropPlugin extends ServerPlugin { - /** @var View */ - private $view; - - /** @var bool */ - private $enabled = false; + private ?IShare $share = null; + private bool $enabled = false; - /** - * @param View $view - */ - public function setView($view) { - $this->view = $view; + public function setShare(IShare $share): void { + $this->share = $share; } - public function enable() { + public function enable(): void { $this->enabled = true; } - /** * This initializes the plugin. - * - * @param \Sabre\DAV\Server $server Sabre server - * - * @return void - * @throws MethodNotAllowed + * It is ONLY initialized by the server on a file drop request. */ - public function initialize(\Sabre\DAV\Server $server) { + public function initialize(\Sabre\DAV\Server $server): void { $server->on('beforeMethod:*', [$this, 'beforeMethod'], 999); + $server->on('method:MKCOL', [$this, 'onMkcol']); $this->enabled = false; } + public function onMkcol(RequestInterface $request, ResponseInterface $response) { + if (!$this->enabled || $this->share === null) { + return; + } + + $node = $this->share->getNode(); + if (!($node instanceof Folder)) { + return; + } + + // If this is a folder creation request we need + // to fake a success so we can pretend every + // folder now exists. + $response->setStatus(201); + return false; + } + public function beforeMethod(RequestInterface $request, ResponseInterface $response) { - if (!$this->enabled) { + if (!$this->enabled || $this->share === null) { return; } + $node = $this->share->getNode(); + if (!($node instanceof Folder)) { + return; + } + + // Retrieve the nickname from the request + $nickname = $request->hasHeader('X-NC-Nickname') + ? trim(urldecode($request->getHeader('X-NC-Nickname'))) + : null; + if ($request->getMethod() !== 'PUT') { - throw new MethodNotAllowed('Only PUT is allowed on files drop'); + // If uploading subfolders we need to ensure they get created + // within the nickname folder + if ($request->getMethod() === 'MKCOL') { + if (!$nickname) { + throw new BadRequest('A nickname header is required when uploading subfolders'); + } + } else { + throw new MethodNotAllowed('Only PUT is allowed on files drop'); + } + } + + // If this is a folder creation request + // let's stop there and let the onMkcol handle it + if ($request->getMethod() === 'MKCOL') { + return; + } + + // Now if we create a file, we need to create the + // full path along the way. We'll only handle conflict + // resolution on file conflicts, but not on folders. + + // e.g files/dCP8yn3N86EK9sL/Folder/image.jpg + $path = $request->getPath(); + $token = $this->share->getToken(); + + // e.g files/dCP8yn3N86EK9sL + $rootPath = substr($path, 0, strpos($path, $token) + strlen($token)); + // e.g /Folder/image.jpg + $relativePath = substr($path, strlen($rootPath)); + $isRootUpload = substr_count($relativePath, '/') === 1; + + // Extract the attributes for the file request + $isFileRequest = false; + $attributes = $this->share->getAttributes(); + if ($attributes !== null) { + $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true; + } + + // We need a valid nickname for file requests + if ($isFileRequest && !$nickname) { + throw new BadRequest('A nickname header is required for file requests'); + } + + // We're only allowing the upload of + // long path with subfolders if a nickname is set. + // This prevents confusion when uploading files and help + // classify them by uploaders. + if (!$nickname && !$isRootUpload) { + throw new BadRequest('A nickname header is required when uploading subfolders'); } - $path = explode('/', $request->getPath()); - $path = array_pop($path); + if ($nickname) { + try { + $node->verifyPath($nickname); + } catch (\Exception $e) { + // If the path is not valid, we throw an exception + throw new BadRequest('Invalid nickname: ' . $nickname); + } - $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view); - $url = $request->getBaseUrl() . $newName; + // Forbid nicknames starting with a dot + if (str_starts_with($nickname, '.')) { + throw new BadRequest('Invalid nickname: ' . $nickname); + } + + // If we have a nickname, let's put + // all files in the subfolder + $relativePath = '/' . $nickname . '/' . $relativePath; + $relativePath = str_replace('//', '/', $relativePath); + } + + // Create the folders along the way + $folder = $node; + $pathSegments = $this->getPathSegments(dirname($relativePath)); + foreach ($pathSegments as $pathSegment) { + if ($pathSegment === '') { + continue; + } + + try { + // get the current folder + $currentFolder = $folder->get($pathSegment); + // check target is a folder + if ($currentFolder instanceof Folder) { + $folder = $currentFolder; + } else { + // otherwise look in the parent folder if we already create an unique folder name + foreach ($folder->getDirectoryListing() as $child) { + // we look for folders which match "NAME (SUFFIX)" + if ($child instanceof Folder && str_starts_with($child->getName(), $pathSegment)) { + $suffix = substr($child->getName(), strlen($pathSegment)); + if (preg_match('/^ \(\d+\)$/', $suffix)) { + // we found the unique folder name and can use it + $folder = $child; + break; + } + } + } + // no folder found so we need to create a new unique folder name + if (!isset($child) || $child !== $folder) { + $folder = $folder->newFolder($folder->getNonExistingName($pathSegment)); + } + } + } catch (NotFoundException) { + // the folder does simply not exist so we create it + $folder = $folder->newFolder($pathSegment); + } + } + + // Finally handle conflicts on the end files + $uniqueName = $folder->getNonExistingName(basename($relativePath)); + $relativePath = substr($folder->getPath(), strlen($node->getPath())); + $path = '/files/' . $token . '/' . $relativePath . '/' . $uniqueName; + $url = rtrim($request->getBaseUrl(), '/') . str_replace('//', '/', $path); $request->setUrl($url); } + + private function getPathSegments(string $path): array { + // Normalize slashes and remove trailing slash + $path = trim(str_replace('\\', '/', $path), '/'); + + return explode('/', $path); + } } diff --git a/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php index 94cd6d29c6c..38a45b3fc37 100644 --- a/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php +++ b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Files\Sharing; @@ -57,7 +41,7 @@ class PublicLinkCheckPlugin extends ServerPlugin { } public function beforeMethod(RequestInterface $request, ResponseInterface $response) { - // verify that the owner didn't have his share permissions revoked + // verify that the owner didn't have their share permissions revoked if ($this->fileInfo && !$this->fileInfo->isShareable()) { throw new NotFound(); } diff --git a/apps/dav/lib/Files/Sharing/RootCollection.php b/apps/dav/lib/Files/Sharing/RootCollection.php new file mode 100644 index 00000000000..dd585fbb59b --- /dev/null +++ b/apps/dav/lib/Files/Sharing/RootCollection.php @@ -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'; + } +} |