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.php42
-rw-r--r--apps/dav/lib/Files/FileSearchBackend.php296
-rw-r--r--apps/dav/lib/Files/FilesHome.php40
-rw-r--r--apps/dav/lib/Files/LazySearchBackend.php23
-rw-r--r--apps/dav/lib/Files/RootCollection.php30
-rw-r--r--apps/dav/lib/Files/Sharing/FilesDropPlugin.php206
-rw-r--r--apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php26
-rw-r--r--apps/dav/lib/Files/Sharing/RootCollection.php32
8 files changed, 421 insertions, 274 deletions
diff --git a/apps/dav/lib/Files/BrowserErrorPagePlugin.php b/apps/dav/lib/Files/BrowserErrorPagePlugin.php
index b3ce591bd4a..85ed975a409 100644
--- a/apps/dav/lib/Files/BrowserErrorPagePlugin.php
+++ b/apps/dav/lib/Files/BrowserErrorPagePlugin.php
@@ -1,33 +1,18 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @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;
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;
@@ -70,12 +55,15 @@ class BrowserErrorPagePlugin extends ServerPlugin {
}
/**
- * @param \Exception $ex
+ * @param \Throwable $ex
*/
- public function logException(\Exception $ex) {
+ public function logException(\Throwable $ex): void {
if ($ex instanceof Exception) {
$httpCode = $ex->getHTTPCode();
$headers = $ex->getHTTPHeaders($this->server);
+ } elseif ($ex instanceof MaxDelayReached) {
+ $httpCode = 429;
+ $headers = [];
} else {
$httpCode = 500;
$headers = [];
@@ -94,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 7ee82779849..eb548bbd55c 100644
--- a/apps/dav/lib/Files/FileSearchBackend.php
+++ b/apps/dav/lib/Files/FileSearchBackend.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
- *
- * @author Christian <16852529+cviereck@users.noreply.github.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Robin Appelman <robin@icewind.nl>
- * @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: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Files;
@@ -29,19 +10,26 @@ use OC\Files\Search\SearchBinaryOperator;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchOrder;
use OC\Files\Search\SearchQuery;
+use OC\Files\Storage\Wrapper\Jail;
use OC\Files\View;
-use OC\Metadata\IMetadataManager;
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;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
+use OCP\Files\Search\ISearchBinaryOperator;
+use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\Files\Search\ISearchQuery;
+use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\FilesMetadata\IMetadataQuery;
+use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IUser;
use OCP\Share\IManager;
use Sabre\DAV\Exception\NotFound;
@@ -55,37 +43,17 @@ use SearchDAV\Query\Order;
use SearchDAV\Query\Query;
class FileSearchBackend implements ISearchBackend {
- /** @var CachingTree */
- private $tree;
-
- /** @var IUser */
- private $user;
-
- /** @var IRootFolder */
- private $rootFolder;
-
- /** @var IManager */
- private $shareManager;
-
- /** @var View */
- private $view;
-
- /**
- * FileSearchBackend constructor.
- *
- * @param CachingTree $tree
- * @param IUser $user
- * @param IRootFolder $rootFolder
- * @param IManager $shareManager
- * @param View $view
- * @internal param IRootFolder $rootFolder
- */
- public function __construct(CachingTree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
- $this->tree = $tree;
- $this->user = $user;
- $this->rootFolder = $rootFolder;
- $this->shareManager = $shareManager;
- $this->view = $view;
+ public const OPERATOR_LIMIT = 100;
+
+ public function __construct(
+ private Server $server,
+ private CachingTree $tree,
+ private IUser $user,
+ private IRootFolder $rootFolder,
+ private IManager $shareManager,
+ private View $view,
+ private IFilesMetadataManager $filesMetadataManager,
+ ) {
}
/**
@@ -113,7 +81,7 @@ class FileSearchBackend implements ISearchBackend {
// all valid scopes support the same schema
//todo dynamically load all propfind properties that are supported
- return [
+ $props = [
// queryable properties
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
@@ -132,9 +100,35 @@ class FileSearchBackend implements ISearchBackend {
new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false),
new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false),
new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
- new SearchPropertyDefinition(FilesPlugin::FILE_METADATA_SIZE, true, false, false, SearchPropertyDefinition::DATATYPE_STRING),
new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
];
+
+ return array_merge($props, $this->getPropertyDefinitionsForMetadata());
+ }
+
+
+ private function getPropertyDefinitionsForMetadata(): array {
+ $metadataProps = [];
+ $metadata = $this->filesMetadataManager->getKnownMetadata();
+ $indexes = $metadata->getIndexes();
+ foreach ($metadata->getKeys() as $key) {
+ $isIndex = in_array($key, $indexes);
+ $type = match ($metadata->getType($key)) {
+ IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER,
+ IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL,
+ IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN,
+ default => SearchPropertyDefinition::DATATYPE_STRING
+ };
+ $metadataProps[] = new SearchPropertyDefinition(
+ FilesPlugin::FILE_METADATA_PREFIX . $key,
+ true,
+ $isIndex,
+ $isIndex,
+ $type
+ );
+ }
+
+ return $metadataProps;
}
/**
@@ -142,58 +136,84 @@ class FileSearchBackend implements ISearchBackend {
* @param string[] $requestProperties
*/
public function preloadPropertyFor(array $nodes, array $requestProperties): void {
- if (in_array(FilesPlugin::FILE_METADATA_SIZE, $requestProperties, true)) {
- // Preloading of the metadata
- $fileIds = [];
- foreach ($nodes as $node) {
- /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */
- if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) {
- /** @var \OCA\DAV\Connector\Sabre\File $node */
- $fileIds[] = $node->getFileInfo()->getId();
- }
- }
- /** @var IMetaDataManager $metadataManager */
- $metadataManager = \OC::$server->get(IMetadataManager::class);
- $preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds);
- foreach ($nodes as $node) {
- /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */
- if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) {
- /** @var \OCA\DAV\Connector\Sabre\File $node */
- $node->setMetadata('size', $preloadedMetadata[$node->getFileInfo()->getId()]);
- }
- }
- }
+ $this->server->emit('preloadProperties', [$nodes, $requestProperties]);
}
- /**
- * @param Query $search
- * @return SearchResult[]
- */
- public function search(Query $search): array {
- if (count($search->from) !== 1) {
- throw new \InvalidArgumentException('Searching more than one folder is not supported');
- }
- $query = $this->transformQuery($search);
- $scope = $search->from[0];
- if ($scope->path === null) {
+ private function getFolderForPath(?string $path = null): Folder {
+ if ($path === null) {
throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
}
- $node = $this->tree->getNodeForPath($scope->path);
+
+ $node = $this->tree->getNodeForPath($path);
+
if (!$node instanceof Directory) {
throw new \InvalidArgumentException('Search is only supported on directories');
}
$fileInfo = $node->getFileInfo();
- $folder = $this->rootFolder->get($fileInfo->getPath());
- /** @var Folder $folder $results */
- $results = $folder->search($query);
+
+ /** @var Folder */
+ return $this->rootFolder->get($fileInfo->getPath());
+ }
+
+ /**
+ * @param Query $search
+ * @return SearchResult[]
+ */
+ public function search(Query $search): array {
+ switch (count($search->from)) {
+ case 0:
+ throw new \InvalidArgumentException('You need to specify a scope for the search.');
+ break;
+ case 1:
+ $scope = $search->from[0];
+ $folder = $this->getFolderForPath($scope->path);
+ $query = $this->transformQuery($search);
+ $results = $folder->search($query);
+ break;
+ default:
+ $scopes = [];
+ foreach ($search->from as $scope) {
+ $folder = $this->getFolderForPath($scope->path);
+ $folderStorage = $folder->getStorage();
+ if ($folderStorage->instanceOfStorage(Jail::class)) {
+ /** @var Jail $folderStorage */
+ $internalPath = $folderStorage->getUnjailedPath($folder->getInternalPath());
+ } else {
+ $internalPath = $folder->getInternalPath();
+ }
+
+ $scopes[] = new SearchBinaryOperator(
+ ISearchBinaryOperator::OPERATOR_AND,
+ [
+ new SearchComparison(
+ ISearchComparison::COMPARE_EQUAL,
+ 'storage',
+ $folderStorage->getCache()->getNumericStorageId(),
+ ''
+ ),
+ new SearchComparison(
+ ISearchComparison::COMPARE_LIKE,
+ 'path',
+ $internalPath . '/%',
+ ''
+ ),
+ ]
+ );
+ }
+
+ $scopeOperators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $scopes);
+ $query = $this->transformQuery($search, $scopeOperators);
+ $userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
+ $results = $userFolder->search($query);
+ }
/** @var SearchResult[] $nodes */
$nodes = array_map(function (Node $node) {
if ($node instanceof Folder) {
- $davNode = new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager);
+ $davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
} else {
- $davNode = new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager);
+ $davNode = new File($this->view, $node, $this->shareManager);
}
$path = $this->getHrefForNode($node);
$this->tree->cacheNode($davNode, $path);
@@ -298,11 +318,20 @@ class FileSearchBackend implements ISearchBackend {
/**
* @param Query $query
+ *
* @return ISearchQuery
*/
- private function transformQuery(Query $query): ISearchQuery {
+ private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery {
+ $orders = array_map(function (Order $order): ISearchOrder {
+ $direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
+ if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
+ return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA);
+ } else {
+ return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property));
+ }
+ }, $query->orderBy);
+
$limit = $query->limit;
- $orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
$offset = $limit->firstResult;
$limitHome = false;
@@ -315,8 +344,21 @@ class FileSearchBackend implements ISearchBackend {
}
}
+ $operatorCount = $this->countSearchOperators($query->where);
+ if ($operatorCount > self::OPERATOR_LIMIT) {
+ throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators');
+ }
+
+ /** @var SearchBinaryOperator|SearchComparison */
+ $queryOperators = $this->transformSearchOperation($query->where);
+ if ($scopeOperators === null) {
+ $operators = $queryOperators;
+ } else {
+ $operators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$queryOperators, $scopeOperators]);
+ }
+
return new SearchQuery(
- $this->transformSearchOperation($query->where),
+ $operators,
(int)$limit->maxResults,
$offset,
$orders,
@@ -325,12 +367,24 @@ class FileSearchBackend implements ISearchBackend {
);
}
- /**
- * @param Order $order
- * @return ISearchOrder
- */
- private function mapSearchOrder(Order $order) {
- return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property));
+ private function countSearchOperators(Operator $operator): int {
+ switch ($operator->type) {
+ case Operator::OPERATION_AND:
+ case Operator::OPERATION_OR:
+ case Operator::OPERATION_NOT:
+ /** @var Operator[] $arguments */
+ $arguments = $operator->arguments;
+ return array_sum(array_map([$this, 'countSearchOperators'], $arguments));
+ case Operator::OPERATION_EQUAL:
+ case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
+ case Operator::OPERATION_GREATER_THAN:
+ case Operator::OPERATION_LESS_OR_EQUAL_THAN:
+ case Operator::OPERATION_LESS_THAN:
+ case Operator::OPERATION_IS_LIKE:
+ case Operator::OPERATION_IS_COLLECTION:
+ default:
+ return 1;
+ }
}
/**
@@ -354,13 +408,37 @@ class FileSearchBackend implements ISearchBackend {
if (count($operator->arguments) !== 2) {
throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
}
+ if (!($operator->arguments[1] instanceof Literal)) {
+ throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
+ }
+ $value = $operator->arguments[1]->value;
+ // no break
+ case Operator::OPERATION_IS_DEFINED:
if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
}
- if (!($operator->arguments[1] instanceof Literal)) {
- throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
+ $property = $operator->arguments[0];
+
+ if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
+ $field = substr($property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX));
+ $extra = IMetadataQuery::EXTRA;
+ } else {
+ $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, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value));
+
+ return new SearchComparison(
+ $trimmedType,
+ $field,
+ $castedValue,
+ $extra ?? ''
+ );
+
case Operator::OPERATION_IS_COLLECTION:
return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
default:
@@ -394,6 +472,10 @@ class FileSearchBackend implements ISearchBackend {
}
private function castValue(SearchPropertyDefinition $property, $value) {
+ if ($value === '') {
+ return '';
+ }
+
switch ($property->dataType) {
case SearchPropertyDefinition::DATATYPE_BOOLEAN:
return $value === 'yes';
@@ -405,7 +487,7 @@ class FileSearchBackend implements ISearchBackend {
if (is_numeric($value)) {
return max(0, 0 + $value);
}
- $date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, $value);
+ $date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value);
return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
default:
return $value;
diff --git a/apps/dav/lib/Files/FilesHome.php b/apps/dav/lib/Files/FilesHome.php
index 0a781b5589d..f8aa82cdcc9 100644
--- a/apps/dav/lib/Files/FilesHome.php
+++ b/apps/dav/lib/Files/FilesHome.php
@@ -1,30 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @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;
+use OC\Files\Filesystem;
use OCA\DAV\Connector\Sabre\Directory;
use OCP\Files\FileInfo;
use Sabre\DAV\Exception\Forbidden;
@@ -32,19 +15,16 @@ use Sabre\DAV\Exception\Forbidden;
class FilesHome extends Directory {
/**
- * @var array
- */
- private $principalInfo;
-
- /**
* FilesHome constructor.
*
* @param array $principalInfo
* @param FileInfo $userFolder
*/
- public function __construct($principalInfo, FileInfo $userFolder) {
- $this->principalInfo = $principalInfo;
- $view = \OC\Files\Filesystem::getView();
+ public function __construct(
+ private $principalInfo,
+ FileInfo $userFolder,
+ ) {
+ $view = Filesystem::getView();
parent::__construct($view, $userFolder);
}
diff --git a/apps/dav/lib/Files/LazySearchBackend.php b/apps/dav/lib/Files/LazySearchBackend.php
index c3b2f27d72a..6ba539ddd87 100644
--- a/apps/dav/lib/Files/LazySearchBackend.php
+++ b/apps/dav/lib/Files/LazySearchBackend.php
@@ -1,28 +1,11 @@
<?php
+
/**
- * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
- *
- * @author Robin Appelman <robin@icewind.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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Files;
-use Sabre\DAV\INode;
use SearchDAV\Backend\ISearchBackend;
use SearchDAV\Query\Query;
diff --git a/apps/dav/lib/Files/RootCollection.php b/apps/dav/lib/Files/RootCollection.php
index 15498ec26ec..a11bea72c59 100644
--- a/apps/dav/lib/Files/RootCollection.php
+++ b/apps/dav/lib/Files/RootCollection.php
@@ -1,31 +1,15 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @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;
use OCP\Files\FileInfo;
+use OCP\IUserSession;
+use OCP\Server;
use Sabre\DAV\INode;
use Sabre\DAV\SimpleCollection;
use Sabre\DAVACL\AbstractPrincipalCollection;
@@ -44,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 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';
+ }
+}