aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/Files
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/Files')
-rw-r--r--apps/dav/lib/Files/BrowserErrorPagePlugin.php13
-rw-r--r--apps/dav/lib/Files/FileSearchBackend.php12
-rw-r--r--apps/dav/lib/Files/LazySearchBackend.php1
-rw-r--r--apps/dav/lib/Files/RootCollection.php4
-rw-r--r--apps/dav/lib/Files/Sharing/FilesDropPlugin.php173
-rw-r--r--apps/dav/lib/Files/Sharing/RootCollection.php32
6 files changed, 196 insertions, 39 deletions
diff --git a/apps/dav/lib/Files/BrowserErrorPagePlugin.php b/apps/dav/lib/Files/BrowserErrorPagePlugin.php
index 46598db2040..85ed975a409 100644
--- a/apps/dav/lib/Files/BrowserErrorPagePlugin.php
+++ b/apps/dav/lib/Files/BrowserErrorPagePlugin.php
@@ -8,9 +8,11 @@
namespace OCA\DAV\Files;
use OC\AppFramework\Http\Request;
-use OC_Template;
use OCP\AppFramework\Http\ContentSecurityPolicy;
+use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
+use OCP\Security\Bruteforce\MaxDelayReached;
+use OCP\Template\ITemplateManager;
use Sabre\DAV\Exception;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
@@ -59,6 +61,9 @@ class BrowserErrorPagePlugin extends ServerPlugin {
if ($ex instanceof Exception) {
$httpCode = $ex->getHTTPCode();
$headers = $ex->getHTTPHeaders($this->server);
+ } elseif ($ex instanceof MaxDelayReached) {
+ $httpCode = 429;
+ $headers = [];
} else {
$httpCode = 500;
$headers = [];
@@ -77,14 +82,14 @@ class BrowserErrorPagePlugin extends ServerPlugin {
* @return bool|string
*/
public function generateBody(int $httpCode) {
- $request = \OC::$server->getRequest();
+ $request = \OCP\Server::get(IRequest::class);
$templateName = 'exception';
- if ($httpCode === 403 || $httpCode === 404) {
+ if ($httpCode === 403 || $httpCode === 404 || $httpCode === 429) {
$templateName = (string)$httpCode;
}
- $content = new OC_Template('core', $templateName, 'guest');
+ $content = \OCP\Server::get(ITemplateManager::class)->getTemplate('core', $templateName, TemplateResponse::RENDER_AS_GUEST);
$content->assign('title', $this->server->httpResponse->getStatusText());
$content->assign('remoteAddr', $request->getRemoteAddress());
$content->assign('requestID', $request->getId());
diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php
index 1b785962112..eb548bbd55c 100644
--- a/apps/dav/lib/Files/FileSearchBackend.php
+++ b/apps/dav/lib/Files/FileSearchBackend.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -15,6 +16,7 @@ use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\File;
use OCA\DAV\Connector\Sabre\FilesPlugin;
+use OCA\DAV\Connector\Sabre\Server;
use OCA\DAV\Connector\Sabre\TagsPlugin;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Folder;
@@ -44,6 +46,7 @@ class FileSearchBackend implements ISearchBackend {
public const OPERATOR_LIMIT = 100;
public function __construct(
+ private Server $server,
private CachingTree $tree,
private IUser $user,
private IRootFolder $rootFolder,
@@ -133,6 +136,7 @@ class FileSearchBackend implements ISearchBackend {
* @param string[] $requestProperties
*/
public function preloadPropertyFor(array $nodes, array $requestProperties): void {
+ $this->server->emit('preloadProperties', [$nodes, $requestProperties]);
}
private function getFolderForPath(?string $path = null): Folder {
@@ -422,10 +426,16 @@ class FileSearchBackend implements ISearchBackend {
$field = $this->mapPropertyNameToColumn($property);
}
+ try {
+ $castedValue = $this->castValue($property, $value ?? '');
+ } catch (\Error $e) {
+ throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
+ }
+
return new SearchComparison(
$trimmedType,
$field,
- $this->castValue($property, $value ?? ''),
+ $castedValue,
$extra ?? ''
);
diff --git a/apps/dav/lib/Files/LazySearchBackend.php b/apps/dav/lib/Files/LazySearchBackend.php
index a0ad730ff2b..6ba539ddd87 100644
--- a/apps/dav/lib/Files/LazySearchBackend.php
+++ b/apps/dav/lib/Files/LazySearchBackend.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/Files/RootCollection.php b/apps/dav/lib/Files/RootCollection.php
index d538d15ec1b..a11bea72c59 100644
--- a/apps/dav/lib/Files/RootCollection.php
+++ b/apps/dav/lib/Files/RootCollection.php
@@ -8,6 +8,8 @@
namespace OCA\DAV\Files;
use OCP\Files\FileInfo;
+use OCP\IUserSession;
+use OCP\Server;
use Sabre\DAV\INode;
use Sabre\DAV\SimpleCollection;
use Sabre\DAVACL\AbstractPrincipalCollection;
@@ -26,7 +28,7 @@ class RootCollection extends AbstractPrincipalCollection {
*/
public function getChildForPrincipal(array $principalInfo) {
[,$name] = \Sabre\Uri\split($principalInfo['uri']);
- $user = \OC::$server->getUserSession()->getUser();
+ $user = Server::get(IUserSession::class)->getUser();
if (is_null($user) || $name !== $user->getUID()) {
// a user is only allowed to see their own home contents, so in case another collection
// is accessed, we return a simple empty collection for now
diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
index 9d883be81fc..a3dbd32ce6b 100644
--- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
+++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
@@ -1,12 +1,15 @@
<?php
+
/**
* 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;
@@ -17,14 +20,9 @@ use Sabre\HTTP\ResponseInterface;
*/
class FilesDropPlugin extends ServerPlugin {
- private ?View $view = null;
private ?IShare $share = null;
private bool $enabled = false;
- public function setView(View $view): void {
- $this->view = $view;
- }
-
public function setShare(IShare $share): void {
$this->share = $share;
}
@@ -33,60 +31,169 @@ class FilesDropPlugin extends ServerPlugin {
$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): void {
$server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
+ $server->on('method:MKCOL', [$this, 'onMkcol']);
$this->enabled = false;
}
- public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
- if (!$this->enabled || $this->share === null || $this->view === null) {
+ 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 || $this->share === null) {
return;
}
- // Only allow file drop
+ $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;
}
- // Always upload at the root level
- $path = explode('/', $request->getPath());
- $path = array_pop($path);
+ // 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();
- $nickName = $request->hasHeader('X-NC-Nickname') ? urldecode($request->getHeader('X-NC-Nickname')) : null;
if ($attributes !== null) {
$isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
}
// We need a valid nickname for file requests
- if ($isFileRequest && ($nickName == null || trim($nickName) === '')) {
- throw new MethodNotAllowed('Nickname is required 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');
+ }
+
+ 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);
+ }
+
+ // 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);
}
-
- // If this is a file request we need to create a folder for the user
- if ($isFileRequest) {
- // Check if the folder already exists
- if (!($this->view->file_exists($nickName) === true)) {
- $this->view->mkdir($nickName);
+
+ // 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);
}
- // Put all files in the subfolder
- $path = $nickName . '/' . $path;
}
-
- $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
- $url = $request->getBaseUrl() . $newName;
+
+ // 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/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';
+ }
+}