diff options
Diffstat (limited to 'apps/dav/lib/Files')
-rw-r--r-- | apps/dav/lib/Files/BrowserErrorPagePlugin.php | 42 | ||||
-rw-r--r-- | apps/dav/lib/Files/FileSearchBackend.php | 296 | ||||
-rw-r--r-- | apps/dav/lib/Files/FilesHome.php | 40 | ||||
-rw-r--r-- | apps/dav/lib/Files/LazySearchBackend.php | 23 | ||||
-rw-r--r-- | apps/dav/lib/Files/RootCollection.php | 30 | ||||
-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 |
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'; + } +} |