aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/DAV
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/DAV')
-rw-r--r--apps/dav/lib/DAV/CustomPropertiesBackend.php487
-rw-r--r--apps/dav/lib/DAV/GroupPrincipalBackend.php70
-rw-r--r--apps/dav/lib/DAV/PublicAuth.php29
-rw-r--r--apps/dav/lib/DAV/Sharing/Backend.php323
-rw-r--r--apps/dav/lib/DAV/Sharing/IShareable.php44
-rw-r--r--apps/dav/lib/DAV/Sharing/Plugin.php70
-rw-r--r--apps/dav/lib/DAV/Sharing/SharingMapper.php137
-rw-r--r--apps/dav/lib/DAV/Sharing/SharingService.php53
-rw-r--r--apps/dav/lib/DAV/Sharing/Xml/Invite.php59
-rw-r--r--apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php40
-rw-r--r--apps/dav/lib/DAV/SystemPrincipalBackend.php27
-rw-r--r--apps/dav/lib/DAV/ViewOnlyPlugin.php114
12 files changed, 919 insertions, 534 deletions
diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php
index acee65cd00d..f9a4f8ee986 100644
--- a/apps/dav/lib/DAV/CustomPropertiesBackend.php
+++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php
@@ -1,36 +1,33 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @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: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
+
namespace OCA\DAV\DAV;
-use OCA\DAV\Connector\Sabre\Node;
+use Exception;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
+use Sabre\DAV\Exception as DavException;
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
use Sabre\DAV\Tree;
+use Sabre\DAV\Xml\Property\Complex;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAV\Xml\Property\LocalHref;
+use Sabre\Xml\ParseException;
+use Sabre\Xml\Service as XmlService;
+
use function array_intersect;
class CustomPropertiesBackend implements BackendInterface {
@@ -39,6 +36,26 @@ class CustomPropertiesBackend implements BackendInterface {
private const TABLE_NAME = 'properties';
/**
+ * Value is stored as string.
+ */
+ public const PROPERTY_TYPE_STRING = 1;
+
+ /**
+ * Value is stored as XML fragment.
+ */
+ public const PROPERTY_TYPE_XML = 2;
+
+ /**
+ * Value is stored as a property object.
+ */
+ public const PROPERTY_TYPE_OBJECT = 3;
+
+ /**
+ * Value is stored as a {DAV:}href string.
+ */
+ public const PROPERTY_TYPE_HREF = 4;
+
+ /**
* Ignored properties
*
* @var string[]
@@ -49,58 +66,35 @@ class CustomPropertiesBackend implements BackendInterface {
'{DAV:}getetag',
'{DAV:}quota-used-bytes',
'{DAV:}quota-available-bytes',
- '{http://owncloud.org/ns}permissions',
- '{http://owncloud.org/ns}downloadURL',
- '{http://owncloud.org/ns}dDC',
- '{http://owncloud.org/ns}size',
- '{http://nextcloud.org/ns}is-encrypted',
-
- // Currently, returning null from any propfind handler would still trigger the backend,
- // so we add all known Nextcloud custom properties in here to avoid that
-
- // text app
- '{http://nextcloud.org/ns}rich-workspace',
- '{http://nextcloud.org/ns}rich-workspace-file',
- // groupfolders
- '{http://nextcloud.org/ns}acl-enabled',
- '{http://nextcloud.org/ns}acl-can-manage',
- '{http://nextcloud.org/ns}acl-list',
- '{http://nextcloud.org/ns}inherited-acl-list',
- '{http://nextcloud.org/ns}group-folder-id',
- // files_lock
- '{http://nextcloud.org/ns}lock',
- '{http://nextcloud.org/ns}lock-owner-type',
- '{http://nextcloud.org/ns}lock-owner',
- '{http://nextcloud.org/ns}lock-owner-displayname',
- '{http://nextcloud.org/ns}lock-owner-editor',
- '{http://nextcloud.org/ns}lock-time',
- '{http://nextcloud.org/ns}lock-timeout',
- '{http://nextcloud.org/ns}lock-token',
];
/**
- * Properties set by one user, readable by all others
+ * Allowed properties for the oc/nc namespace, all other properties in the namespace are ignored
*
- * @var array[]
+ * @var string[]
*/
- private const PUBLISHED_READ_ONLY_PROPERTIES = [
- '{urn:ietf:params:xml:ns:caldav}calendar-availability',
+ private const ALLOWED_NC_PROPERTIES = [
+ '{http://owncloud.org/ns}calendar-enabled',
+ '{http://owncloud.org/ns}enabled',
];
/**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var IDBConnection
+ * Properties set by one user, readable by all others
+ *
+ * @var string[]
*/
- private $connection;
+ private const PUBLISHED_READ_ONLY_PROPERTIES = [
+ '{urn:ietf:params:xml:ns:caldav}calendar-availability',
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+ ];
/**
- * @var IUser
+ * Map of custom XML elements to parse when trying to deserialize an instance of
+ * \Sabre\DAV\Xml\Property\Complex to find a more specialized PROPERTY_TYPE_*
*/
- private $user;
+ private const COMPLEX_XML_ELEMENT_MAP = [
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => Href::class,
+ ];
/**
* Properties cache
@@ -108,6 +102,7 @@ class CustomPropertiesBackend implements BackendInterface {
* @var array
*/
private $userCache = [];
+ private XmlService $xmlService;
/**
* @param Tree $tree node tree
@@ -115,12 +110,17 @@ class CustomPropertiesBackend implements BackendInterface {
* @param IUser $user owner of the tree and properties
*/
public function __construct(
- Tree $tree,
- IDBConnection $connection,
- IUser $user) {
- $this->tree = $tree;
- $this->connection = $connection;
- $this->user = $user;
+ private Server $server,
+ private Tree $tree,
+ private IDBConnection $connection,
+ private IUser $user,
+ private DefaultCalendarValidator $defaultCalendarValidator,
+ ) {
+ $this->xmlService = new XmlService();
+ $this->xmlService->elementMap = array_merge(
+ $this->xmlService->elementMap,
+ self::COMPLEX_XML_ELEMENT_MAP,
+ );
}
/**
@@ -133,15 +133,14 @@ class CustomPropertiesBackend implements BackendInterface {
public function propFind($path, PropFind $propFind) {
$requestedProps = $propFind->get404Properties();
- // these might appear
- $requestedProps = array_diff(
+ $requestedProps = array_filter(
$requestedProps,
- self::IGNORED_PROPERTIES
+ $this->isPropertyAllowed(...),
);
// substr of calendars/ => path is inside the CalDAV component
// two '/' => this a calendar (no calendar-home nor calendar object)
- if (substr($path, 0, 10) === 'calendars/' && substr_count($path, '/') === 2) {
+ if (str_starts_with($path, 'calendars/') && substr_count($path, '/') === 2) {
$allRequestedProps = $propFind->getRequestedProperties();
$customPropertiesForShares = [
'{DAV:}displayname',
@@ -159,20 +158,80 @@ class CustomPropertiesBackend implements BackendInterface {
}
}
+ // substr of addressbooks/ => path is inside the CardDAV component
+ // three '/' => this a addressbook (no addressbook-home nor contact object)
+ if (str_starts_with($path, 'addressbooks/') && substr_count($path, '/') === 3) {
+ $allRequestedProps = $propFind->getRequestedProperties();
+ $customPropertiesForShares = [
+ '{DAV:}displayname',
+ ];
+
+ foreach ($customPropertiesForShares as $customPropertyForShares) {
+ if (in_array($customPropertyForShares, $allRequestedProps, true)) {
+ $requestedProps[] = $customPropertyForShares;
+ }
+ }
+ }
+
+ // substr of principals/users/ => path is a user principal
+ // two '/' => this a principal collection (and not some child object)
+ if (str_starts_with($path, 'principals/users/') && substr_count($path, '/') === 2) {
+ $allRequestedProps = $propFind->getRequestedProperties();
+ $customProperties = [
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+ ];
+
+ foreach ($customProperties as $customProperty) {
+ if (in_array($customProperty, $allRequestedProps, true)) {
+ $requestedProps[] = $customProperty;
+ }
+ }
+ }
+
if (empty($requestedProps)) {
return;
}
+ $node = $this->tree->getNodeForPath($path);
+ if ($node instanceof Directory && $propFind->getDepth() !== 0) {
+ $this->cacheDirectory($path, $node);
+ }
+
+ if ($node instanceof CalendarObject) {
+ // No custom properties supported on individual events
+ return;
+ }
+
// First fetch the published properties (set by another user), then get the ones set by
// the current user. If both are set then the latter as priority.
foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
+ try {
+ $this->validateProperty($path, $propName, $propValue);
+ } catch (DavException $e) {
+ continue;
+ }
$propFind->set($propName, $propValue);
}
foreach ($this->getUserProperties($path, $requestedProps) as $propName => $propValue) {
+ try {
+ $this->validateProperty($path, $propName, $propValue);
+ } catch (DavException $e) {
+ continue;
+ }
$propFind->set($propName, $propValue);
}
}
+ private function isPropertyAllowed(string $property): bool {
+ if (in_array($property, self::IGNORED_PROPERTIES)) {
+ return false;
+ }
+ if (str_starts_with($property, '{http://owncloud.org/ns}') || str_starts_with($property, '{http://nextcloud.org/ns}')) {
+ return in_array($property, self::ALLOWED_NC_PROPERTIES);
+ }
+ return true;
+ }
+
/**
* Updates properties for a path
*
@@ -212,14 +271,39 @@ class CustomPropertiesBackend implements BackendInterface {
*/
public function move($source, $destination) {
$statement = $this->connection->prepare(
- 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' .
- ' WHERE `userid` = ? AND `propertypath` = ?'
+ 'UPDATE `*PREFIX*properties` SET `propertypath` = ?'
+ . ' WHERE `userid` = ? AND `propertypath` = ?'
);
$statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]);
$statement->closeCursor();
}
/**
+ * Validate the value of a property. Will throw if a value is invalid.
+ *
+ * @throws DavException The value of the property is invalid
+ */
+ private function validateProperty(string $path, string $propName, mixed $propValue): void {
+ switch ($propName) {
+ case '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL':
+ /** @var Href $propValue */
+ $href = $propValue->getHref();
+ if ($href === null) {
+ throw new DavException('Href is empty');
+ }
+
+ // $path is the principal here as this prop is only set on principals
+ $node = $this->tree->getNodeForPath($href);
+ if (!($node instanceof Calendar) || $node->getOwner() !== $path) {
+ throw new DavException('No such calendar');
+ }
+
+ $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
+ break;
+ }
+ }
+
+ /**
* @param string $path
* @param string[] $requestedProperties
*
@@ -239,13 +323,48 @@ class CustomPropertiesBackend implements BackendInterface {
$result = $qb->executeQuery();
$props = [];
while ($row = $result->fetch()) {
- $props[$row['propertyname']] = $row['propertyvalue'];
+ $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
}
$result->closeCursor();
return $props;
}
/**
+ * prefetch all user properties in a directory
+ */
+ private function cacheDirectory(string $path, Directory $node): void {
+ $prefix = ltrim($path . '/', '/');
+ $query = $this->connection->getQueryBuilder();
+ $query->select('name', 'p.propertypath', 'p.propertyname', 'p.propertyvalue', 'p.valuetype')
+ ->from('filecache', 'f')
+ ->hintShardKey('storage', $node->getNode()->getMountPoint()->getNumericStorageId())
+ ->leftJoin('f', 'properties', 'p', $query->expr()->eq('p.propertypath', $query->func()->concat(
+ $query->createNamedParameter($prefix),
+ 'f.name'
+ )),
+ )
+ ->where($query->expr()->eq('parent', $query->createNamedParameter($node->getInternalFileId(), IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->orX(
+ $query->expr()->eq('p.userid', $query->createNamedParameter($this->user->getUID())),
+ $query->expr()->isNull('p.userid'),
+ ));
+ $result = $query->executeQuery();
+
+ $propsByPath = [];
+
+ while ($row = $result->fetch()) {
+ $childPath = $prefix . $row['name'];
+ if (!isset($propsByPath[$childPath])) {
+ $propsByPath[$childPath] = [];
+ }
+ if (isset($row['propertyname'])) {
+ $propsByPath[$childPath][$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
+ }
+ }
+ $this->userCache = array_merge($this->userCache, $propsByPath);
+ }
+
+ /**
* Returns a list of properties for the given path and current user
*
* @param string $path
@@ -271,7 +390,7 @@ class CustomPropertiesBackend implements BackendInterface {
// request only a subset
$sql .= ' AND `propertyname` in (?)';
$whereValues[] = $requestedProperties;
- $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
+ $whereTypes[] = IQueryBuilder::PARAM_STR_ARRAY;
}
$result = $this->connection->executeQuery(
@@ -282,7 +401,7 @@ class CustomPropertiesBackend implements BackendInterface {
$props = [];
while ($row = $result->fetch()) {
- $props[$row['propertyname']] = $row['propertyvalue'];
+ $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
}
$result->closeCursor();
@@ -292,68 +411,58 @@ class CustomPropertiesBackend implements BackendInterface {
}
/**
- * Update properties
- *
- * @param string $path path for which to update properties
- * @param array $properties array of properties to update
- *
- * @return bool
+ * @throws Exception
*/
- private function updateProperties(string $path, array $properties) {
- $deleteStatement = 'DELETE FROM `*PREFIX*properties`' .
- ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
-
- $insertStatement = 'INSERT INTO `*PREFIX*properties`' .
- ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)';
-
- $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' .
- ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
-
+ private function updateProperties(string $path, array $properties): bool {
// TODO: use "insert or update" strategy ?
$existing = $this->getUserProperties($path, []);
- $this->connection->beginTransaction();
- foreach ($properties as $propertyName => $propertyValue) {
- // If it was null, we need to delete the property
- if (is_null($propertyValue)) {
- if (array_key_exists($propertyName, $existing)) {
- $this->connection->executeUpdate($deleteStatement,
- [
- $this->user->getUID(),
- $this->formatPath($path),
- $propertyName,
- ]
- );
- }
- } else {
- if ($propertyValue instanceOf \Sabre\DAV\Xml\Property\Complex) {
- $propertyValue = $propertyValue->getXml();
- } elseif (!is_string($propertyValue)) {
- $propertyValue = (string)$propertyValue;
- }
- if (!array_key_exists($propertyName, $existing)) {
- $this->connection->executeUpdate($insertStatement,
- [
- $this->user->getUID(),
- $this->formatPath($path),
- $propertyName,
- $propertyValue,
- ]
- );
+ try {
+ $this->connection->beginTransaction();
+ foreach ($properties as $propertyName => $propertyValue) {
+ // common parameters for all queries
+ $dbParameters = [
+ 'userid' => $this->user->getUID(),
+ 'propertyPath' => $this->formatPath($path),
+ 'propertyName' => $propertyName,
+ ];
+
+ // If it was null, we need to delete the property
+ if (is_null($propertyValue)) {
+ if (array_key_exists($propertyName, $existing)) {
+ $deleteQuery = $deleteQuery ?? $this->createDeleteQuery();
+ $deleteQuery
+ ->setParameters($dbParameters)
+ ->executeStatement();
+ }
} else {
- $this->connection->executeUpdate($updateStatement,
- [
- $propertyValue,
- $this->user->getUID(),
- $this->formatPath($path),
- $propertyName,
- ]
+ [$value, $valueType] = $this->encodeValueForDatabase(
+ $path,
+ $propertyName,
+ $propertyValue,
);
+ $dbParameters['propertyValue'] = $value;
+ $dbParameters['valueType'] = $valueType;
+
+ if (!array_key_exists($propertyName, $existing)) {
+ $insertQuery = $insertQuery ?? $this->createInsertQuery();
+ $insertQuery
+ ->setParameters($dbParameters)
+ ->executeStatement();
+ } else {
+ $updateQuery = $updateQuery ?? $this->createUpdateQuery();
+ $updateQuery
+ ->setParameters($dbParameters)
+ ->executeStatement();
+ }
}
}
- }
- $this->connection->commit();
- unset($this->userCache[$path]);
+ $this->connection->commit();
+ unset($this->userCache[$path]);
+ } catch (Exception $e) {
+ $this->connection->rollBack();
+ throw $e;
+ }
return true;
}
@@ -367,8 +476,122 @@ class CustomPropertiesBackend implements BackendInterface {
private function formatPath(string $path): string {
if (strlen($path) > 250) {
return sha1($path);
+ }
+
+ return $path;
+ }
+
+ /**
+ * @throws ParseException If parsing a \Sabre\DAV\Xml\Property\Complex value fails
+ * @throws DavException If the property value is invalid
+ */
+ private function encodeValueForDatabase(string $path, string $name, mixed $value): array {
+ // Try to parse a more specialized property type first
+ if ($value instanceof Complex) {
+ $xml = $this->xmlService->write($name, [$value], $this->server->getBaseUri());
+ $value = $this->xmlService->parse($xml, $this->server->getBaseUri()) ?? $value;
+ }
+
+ if ($name === '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL') {
+ $value = $this->encodeDefaultCalendarUrl($value);
+ }
+
+ try {
+ $this->validateProperty($path, $name, $value);
+ } catch (DavException $e) {
+ throw new DavException(
+ "Property \"$name\" has an invalid value: " . $e->getMessage(),
+ 0,
+ $e,
+ );
+ }
+
+ if (is_scalar($value)) {
+ $valueType = self::PROPERTY_TYPE_STRING;
+ } elseif ($value instanceof Complex) {
+ $valueType = self::PROPERTY_TYPE_XML;
+ $value = $value->getXml();
+ } elseif ($value instanceof Href) {
+ $valueType = self::PROPERTY_TYPE_HREF;
+ $value = $value->getHref();
} else {
- return $path;
+ $valueType = self::PROPERTY_TYPE_OBJECT;
+ // serialize produces null character
+ // these can not be properly stored in some databases and need to be replaced
+ $value = str_replace(chr(0), '\x00', serialize($value));
+ }
+ return [$value, $valueType];
+ }
+
+ /**
+ * @return mixed|Complex|string
+ */
+ private function decodeValueFromDatabase(string $value, int $valueType) {
+ switch ($valueType) {
+ case self::PROPERTY_TYPE_XML:
+ return new Complex($value);
+ case self::PROPERTY_TYPE_HREF:
+ return new Href($value);
+ case self::PROPERTY_TYPE_OBJECT:
+ // some databases can not handel null characters, these are custom encoded during serialization
+ // this custom encoding needs to be first reversed before unserializing
+ return unserialize(str_replace('\x00', chr(0), $value));
+ case self::PROPERTY_TYPE_STRING:
+ default:
+ return $value;
+ }
+ }
+
+ private function encodeDefaultCalendarUrl(Href $value): Href {
+ $href = $value->getHref();
+ if ($href === null) {
+ return $value;
+ }
+
+ if (!str_starts_with($href, '/')) {
+ return $value;
+ }
+
+ try {
+ // Build path relative to the dav base URI to be used later to find the node
+ $value = new LocalHref($this->server->calculateUri($href) . '/');
+ } catch (DavException\Forbidden) {
+ // Not existing calendars will be handled later when the value is validated
}
+
+ return $value;
+ }
+
+ private function createDeleteQuery(): IQueryBuilder {
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete('properties')
+ ->where($deleteQuery->expr()->eq('userid', $deleteQuery->createParameter('userid')))
+ ->andWhere($deleteQuery->expr()->eq('propertypath', $deleteQuery->createParameter('propertyPath')))
+ ->andWhere($deleteQuery->expr()->eq('propertyname', $deleteQuery->createParameter('propertyName')));
+ return $deleteQuery;
+ }
+
+ private function createInsertQuery(): IQueryBuilder {
+ $insertQuery = $this->connection->getQueryBuilder();
+ $insertQuery->insert('properties')
+ ->values([
+ 'userid' => $insertQuery->createParameter('userid'),
+ 'propertypath' => $insertQuery->createParameter('propertyPath'),
+ 'propertyname' => $insertQuery->createParameter('propertyName'),
+ 'propertyvalue' => $insertQuery->createParameter('propertyValue'),
+ 'valuetype' => $insertQuery->createParameter('valueType'),
+ ]);
+ return $insertQuery;
+ }
+
+ private function createUpdateQuery(): IQueryBuilder {
+ $updateQuery = $this->connection->getQueryBuilder();
+ $updateQuery->update('properties')
+ ->set('propertyvalue', $updateQuery->createParameter('propertyValue'))
+ ->set('valuetype', $updateQuery->createParameter('valueType'))
+ ->where($updateQuery->expr()->eq('userid', $updateQuery->createParameter('userid')))
+ ->andWhere($updateQuery->expr()->eq('propertypath', $updateQuery->createParameter('propertyPath')))
+ ->andWhere($updateQuery->expr()->eq('propertyname', $updateQuery->createParameter('propertyName')));
+ return $updateQuery;
}
}
diff --git a/apps/dav/lib/DAV/GroupPrincipalBackend.php b/apps/dav/lib/DAV/GroupPrincipalBackend.php
index f1f15fd61a6..77ba45182c9 100644
--- a/apps/dav/lib/DAV/GroupPrincipalBackend.php
+++ b/apps/dav/lib/DAV/GroupPrincipalBackend.php
@@ -1,30 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @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: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV;
@@ -42,32 +21,17 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class GroupPrincipalBackend implements BackendInterface {
public const PRINCIPAL_PREFIX = 'principals/groups';
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IShareManager */
- private $shareManager;
- /** @var IConfig */
- private $config;
-
/**
- * @param IGroupManager $IGroupManager
+ * @param IGroupManager $groupManager
* @param IUserSession $userSession
* @param IShareManager $shareManager
*/
public function __construct(
- IGroupManager $IGroupManager,
- IUserSession $userSession,
- IShareManager $shareManager,
- IConfig $config
+ private IGroupManager $groupManager,
+ private IUserSession $userSession,
+ private IShareManager $shareManager,
+ private IConfig $config,
) {
- $this->groupManager = $IGroupManager;
- $this->userSession = $userSession;
- $this->shareManager = $shareManager;
- $this->config = $config;
}
/**
@@ -87,8 +51,10 @@ class GroupPrincipalBackend implements BackendInterface {
$principals = [];
if ($prefixPath === self::PRINCIPAL_PREFIX) {
- foreach ($this->groupManager->search('') as $user) {
- $principals[] = $this->groupToPrincipal($user);
+ foreach ($this->groupManager->search('') as $group) {
+ if (!$group->hideFromCollaboration()) {
+ $principals[] = $this->groupToPrincipal($group);
+ }
}
}
@@ -104,7 +70,7 @@ class GroupPrincipalBackend implements BackendInterface {
* @return array
*/
public function getPrincipalByPath($path) {
- $elements = explode('/', $path, 3);
+ $elements = explode('/', $path, 3);
if ($elements[0] !== 'principals') {
return null;
}
@@ -114,7 +80,7 @@ class GroupPrincipalBackend implements BackendInterface {
$name = urldecode($elements[2]);
$group = $this->groupManager->get($name);
- if (!is_null($group)) {
+ if ($group !== null && !$group->hideFromCollaboration()) {
return $this->groupToPrincipal($group);
}
@@ -223,6 +189,10 @@ class GroupPrincipalBackend implements BackendInterface {
$groups = $this->groupManager->search($value, $searchLimit);
$results[] = array_reduce($groups, function (array $carry, IGroup $group) use ($restrictGroups) {
+ if ($group->hideFromCollaboration()) {
+ return $carry;
+ }
+
$gid = $group->getGID();
// is sharing restricted to groups only?
if ($restrictGroups !== false) {
@@ -288,7 +258,7 @@ class GroupPrincipalBackend implements BackendInterface {
$restrictGroups = $this->groupManager->getUserGroupIds($user);
}
- if (strpos($uri, 'principal:principals/groups/') === 0) {
+ if (str_starts_with($uri, 'principal:principals/groups/')) {
$name = urlencode(substr($uri, 28));
if ($restrictGroups !== false && !\in_array($name, $restrictGroups, true)) {
return null;
diff --git a/apps/dav/lib/DAV/PublicAuth.php b/apps/dav/lib/DAV/PublicAuth.php
index 83874ab0d4d..c2b4ada173a 100644
--- a/apps/dav/lib/DAV/PublicAuth.php
+++ b/apps/dav/lib/DAV/PublicAuth.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @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\DAV;
@@ -68,9 +53,9 @@ class PublicAuth implements BackendInterface {
*/
public function check(RequestInterface $request, ResponseInterface $response) {
if ($this->isRequestPublic($request)) {
- return [true, "principals/system/public"];
+ return [true, 'principals/system/public'];
}
- return [false, "No public access to this resource."];
+ return [false, 'No public access to this resource.'];
}
/**
@@ -86,7 +71,7 @@ class PublicAuth implements BackendInterface {
private function isRequestPublic(RequestInterface $request) {
$url = $request->getPath();
$matchingUrls = array_filter($this->publicURLs, function ($publicUrl) use ($url) {
- return strpos($url, $publicUrl, 0) === 0;
+ return str_starts_with($url, $publicUrl);
});
return !empty($matchingUrls);
}
diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php
index 0f675ea4c15..d60f5cca7c6 100644
--- a/apps/dav/lib/DAV/Sharing/Backend.php
+++ b/apps/dav/lib/DAV/Sharing/Backend.php
@@ -1,178 +1,106 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @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\DAV\Sharing;
use OCA\DAV\Connector\Sabre\Principal;
-use OCP\IDBConnection;
+use OCP\AppFramework\Db\TTransactional;
+use OCP\ICache;
+use OCP\ICacheFactory;
use OCP\IGroupManager;
use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
-class Backend {
-
- /** @var IDBConnection */
- private $db;
- /** @var IUserManager */
- private $userManager;
- /** @var IGroupManager */
- private $groupManager;
- /** @var Principal */
- private $principalBackend;
- /** @var string */
- private $resourceType;
-
+abstract class Backend {
+ use TTransactional;
public const ACCESS_OWNER = 1;
+
public const ACCESS_READ_WRITE = 2;
public const ACCESS_READ = 3;
-
- /**
- * @param IDBConnection $db
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param Principal $principalBackend
- * @param string $resourceType
- */
- public function __construct(IDBConnection $db, IUserManager $userManager, IGroupManager $groupManager, Principal $principalBackend, $resourceType) {
- $this->db = $db;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->principalBackend = $principalBackend;
- $this->resourceType = $resourceType;
+ // 4 is already in use for public calendars
+ public const ACCESS_UNSHARED = 5;
+
+ private ICache $shareCache;
+
+ public function __construct(
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private Principal $principalBackend,
+ private ICacheFactory $cacheFactory,
+ private SharingService $service,
+ private LoggerInterface $logger,
+ ) {
+ $this->shareCache = $this->cacheFactory->createInMemory();
}
/**
- * @param IShareable $shareable
- * @param string[] $add
- * @param string[] $remove
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
*/
- public function updateShares(IShareable $shareable, array $add, array $remove) {
+ public function updateShares(IShareable $shareable, array $add, array $remove, array $oldShares = []): void {
+ $this->shareCache->clear();
foreach ($add as $element) {
$principal = $this->principalBackend->findByUri($element['href'], '');
- if ($principal !== '') {
- $this->shareWith($shareable, $element);
+ if (empty($principal)) {
+ continue;
}
- }
- foreach ($remove as $element) {
- $principal = $this->principalBackend->findByUri($element, '');
- if ($principal !== '') {
- $this->unshare($shareable, $element);
+
+ // We need to validate manually because some principals are only virtual
+ // i.e. Group principals
+ $principalparts = explode('/', $principal, 3);
+ if (count($principalparts) !== 3 || $principalparts[0] !== 'principals' || !in_array($principalparts[1], ['users', 'groups', 'circles'], true)) {
+ // Invalid principal
+ continue;
}
- }
- }
- /**
- * @param IShareable $shareable
- * @param string $element
- */
- private function shareWith($shareable, $element) {
- $user = $element['href'];
- $parts = explode(':', $user, 2);
- if ($parts[0] !== 'principal') {
- return;
- }
+ // Don't add share for owner
+ if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
+ continue;
+ }
- // don't share with owner
- if ($shareable->getOwner() === $parts[1]) {
- return;
- }
+ $principalparts[2] = urldecode($principalparts[2]);
+ if (($principalparts[1] === 'users' && !$this->userManager->userExists($principalparts[2]))
+ || ($principalparts[1] === 'groups' && !$this->groupManager->groupExists($principalparts[2]))) {
+ // User or group does not exist
+ continue;
+ }
- $principal = explode('/', $parts[1], 3);
- if (count($principal) !== 3 || $principal[0] !== 'principals' || !in_array($principal[1], ['users', 'groups', 'circles'], true)) {
- // Invalid principal
- return;
- }
+ $access = Backend::ACCESS_READ;
+ if (isset($element['readOnly'])) {
+ $access = $element['readOnly'] ? Backend::ACCESS_READ : Backend::ACCESS_READ_WRITE;
+ }
- $principal[2] = urldecode($principal[2]);
- if (($principal[1] === 'users' && !$this->userManager->userExists($principal[2])) ||
- ($principal[1] === 'groups' && !$this->groupManager->groupExists($principal[2]))) {
- // User or group does not exist
- return;
+ $this->service->shareWith($shareable->getResourceId(), $principal, $access);
}
+ foreach ($remove as $element) {
+ $principal = $this->principalBackend->findByUri($element, '');
+ if (empty($principal)) {
+ continue;
+ }
- // remove the share if it already exists
- $this->unshare($shareable, $element['href']);
- $access = self::ACCESS_READ;
- if (isset($element['readOnly'])) {
- $access = $element['readOnly'] ? self::ACCESS_READ : self::ACCESS_READ_WRITE;
- }
+ // Don't add unshare for owner
+ if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
+ continue;
+ }
- $query = $this->db->getQueryBuilder();
- $query->insert('dav_shares')
- ->values([
- 'principaluri' => $query->createNamedParameter($parts[1]),
- 'type' => $query->createNamedParameter($this->resourceType),
- 'access' => $query->createNamedParameter($access),
- 'resourceid' => $query->createNamedParameter($shareable->getResourceId())
- ]);
- $query->execute();
+ // Delete any possible direct shares (since the frontend does not separate between them)
+ $this->service->deleteShare($shareable->getResourceId(), $principal);
+ }
}
- /**
- * @param $resourceId
- */
- public function deleteAllShares($resourceId) {
- $query = $this->db->getQueryBuilder();
- $query->delete('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId)))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->execute();
+ public function deleteAllShares(int $resourceId): void {
+ $this->shareCache->clear();
+ $this->service->deleteAllShares($resourceId);
}
- public function deleteAllSharesByUser($principaluri) {
- $query = $this->db->getQueryBuilder();
- $query->delete('dav_shares')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principaluri)))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->execute();
- }
-
- /**
- * @param IShareable $shareable
- * @param string $element
- */
- private function unshare($shareable, $element) {
- $parts = explode(':', $element, 2);
- if ($parts[0] !== 'principal') {
- return;
- }
-
- // don't share with owner
- if ($shareable->getOwner() === $parts[1]) {
- return;
- }
-
- $query = $this->db->getQueryBuilder();
- $query->delete('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($shareable->getResourceId())))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1])))
- ;
- $query->execute();
+ public function deleteAllSharesByUser(string $principaluri): void {
+ $this->shareCache->clear();
+ $this->service->deleteAllSharesByUser($principaluri);
}
/**
@@ -183,45 +111,67 @@ class Backend {
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
- * * summary - Optional, a description for the share
*
* @param int $resourceId
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares($resourceId) {
- $query = $this->db->getQueryBuilder();
- $result = $query->select(['principaluri', 'access'])
- ->from('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId)))
- ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType)))
- ->groupBy(['principaluri', 'access'])
- ->execute();
+ public function getShares(int $resourceId): array {
+ $cached = $this->shareCache->get((string)$resourceId);
+ if ($cached) {
+ return $cached;
+ }
+ $rows = $this->service->getShares($resourceId);
$shares = [];
- while ($row = $result->fetch()) {
+ foreach ($rows as $row) {
$p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
$shares[] = [
- 'href' => "principal:${row['principaluri']}",
- 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '',
+ 'href' => "principal:{$row['principaluri']}",
+ 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '',
'status' => 1,
- 'readOnly' => (int) $row['access'] === self::ACCESS_READ,
- '{http://owncloud.org/ns}principal' => $row['principaluri'],
- '{http://owncloud.org/ns}group-share' => is_null($p)
+ 'readOnly' => (int)$row['access'] === Backend::ACCESS_READ,
+ '{http://owncloud.org/ns}principal' => (string)$row['principaluri'],
+ '{http://owncloud.org/ns}group-share' => isset($p['uri']) && (str_starts_with($p['uri'], 'principals/groups') || str_starts_with($p['uri'], 'principals/circles'))
];
}
-
+ $this->shareCache->set((string)$resourceId, $shares);
return $shares;
}
+ public function preloadShares(array $resourceIds): void {
+ $resourceIds = array_filter($resourceIds, function (int $resourceId) {
+ return empty($this->shareCache->get((string)$resourceId));
+ });
+ if (empty($resourceIds)) {
+ return;
+ }
+
+ $rows = $this->service->getSharesForIds($resourceIds);
+ $sharesByResource = array_fill_keys($resourceIds, []);
+ foreach ($rows as $row) {
+ $resourceId = (int)$row['resourceid'];
+ $p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
+ $sharesByResource[$resourceId][] = [
+ 'href' => "principal:{$row['principaluri']}",
+ 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '',
+ 'status' => 1,
+ 'readOnly' => (int)$row['access'] === self::ACCESS_READ,
+ '{http://owncloud.org/ns}principal' => (string)$row['principaluri'],
+ '{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups')
+ ];
+ $this->shareCache->set((string)$resourceId, $sharesByResource[$resourceId]);
+ }
+ }
+
/**
* For shared resources the sharee is set in the ACL of the resource
*
* @param int $resourceId
- * @param array $acl
- * @return array
+ * @param list<array{privilege: string, principal: string, protected: bool}> $acl
+ * @param list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> $shares
+ * @return list<array{principal: string, privilege: string, protected: bool}>
*/
- public function applyShareAcl($resourceId, $acl) {
- $shares = $this->getShares($resourceId);
+ public function applyShareAcl(array $shares, array $acl): array {
foreach ($shares as $share) {
$acl[] = [
'privilege' => '{DAV:}read',
@@ -234,7 +184,7 @@ class Backend {
'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'],
'protected' => true,
];
- } elseif ($this->resourceType === 'calendar') {
+ } elseif (in_array($this->service->getResourceType(), ['calendar','addressbook'])) {
// Allow changing the properties of read only calendars,
// so users can change the visibility.
$acl[] = [
@@ -246,4 +196,45 @@ class Backend {
}
return $acl;
}
+
+ public function unshare(IShareable $shareable, string $principalUri): bool {
+ $this->shareCache->clear();
+
+ $principal = $this->principalBackend->findByUri($principalUri, '');
+ if (empty($principal)) {
+ return false;
+ }
+
+ if ($shareable->getOwner() === $principal) {
+ return false;
+ }
+
+ // Delete any possible direct shares (since the frontend does not separate between them)
+ $this->service->deleteShare($shareable->getResourceId(), $principal);
+
+ $needsUnshare = $this->hasAccessByGroupOrCirclesMembership(
+ $shareable->getResourceId(),
+ $principal
+ );
+
+ if ($needsUnshare) {
+ $this->service->unshare($shareable->getResourceId(), $principal);
+ }
+
+ return true;
+ }
+
+ private function hasAccessByGroupOrCirclesMembership(int $resourceId, string $principal) {
+ $memberships = array_merge(
+ $this->principalBackend->getGroupMembership($principal, true),
+ $this->principalBackend->getCircleMembership($principal)
+ );
+
+ $shares = array_column(
+ $this->service->getShares($resourceId),
+ 'principaluri'
+ );
+
+ return count(array_intersect($memberships, $shares)) > 0;
+ }
}
diff --git a/apps/dav/lib/DAV/Sharing/IShareable.php b/apps/dav/lib/DAV/Sharing/IShareable.php
index 3833e026696..d83079f6975 100644
--- a/apps/dav/lib/DAV/Sharing/IShareable.php
+++ b/apps/dav/lib/DAV/Sharing/IShareable.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @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\DAV\Sharing;
@@ -40,16 +24,14 @@ interface IShareable extends INode {
* Every element in the add array has the following properties:
* * href - A url. Usually a mailto: address
* * commonName - Usually a first and last name, or false
- * * summary - A description of the share, can also be false
* * readOnly - A boolean value
*
* Every element in the remove array is just the address string.
*
- * @param array $add
- * @param array $remove
- * @return void
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
*/
- public function updateShares(array $add, array $remove);
+ public function updateShares(array $add, array $remove): void;
/**
* Returns the list of people whom this resource is shared with.
@@ -59,19 +41,15 @@ interface IShareable extends INode {
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
- * * summary - Optional, a description for the share
*
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares();
+ public function getShares(): array;
- /**
- * @return int
- */
- public function getResourceId();
+ public function getResourceId(): int;
/**
- * @return string
+ * @return ?string
*/
public function getOwner();
}
diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php
index a4b2cd3681c..03e63813bab 100644
--- a/apps/dav/lib/DAV/Sharing/Plugin.php
+++ b/apps/dav/lib/DAV/Sharing/Plugin.php
@@ -1,32 +1,18 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @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\DAV\Sharing;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\DAV\Sharing\Xml\Invite;
use OCA\DAV\DAV\Sharing\Xml\ShareRequest;
+use OCP\AppFramework\Http;
use OCP\IConfig;
use OCP\IRequest;
use Sabre\DAV\Exception\NotFound;
@@ -41,26 +27,18 @@ class Plugin extends ServerPlugin {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.com/ns';
- /** @var Auth */
- private $auth;
-
- /** @var IRequest */
- private $request;
-
- /** @var IConfig */
- private $config;
-
/**
* Plugin constructor.
*
- * @param Auth $authBackEnd
+ * @param Auth $auth
* @param IRequest $request
* @param IConfig $config
*/
- public function __construct(Auth $authBackEnd, IRequest $request, IConfig $config) {
- $this->auth = $authBackEnd;
- $this->request = $request;
- $this->config = $config;
+ public function __construct(
+ private Auth $auth,
+ private IRequest $request,
+ private IConfig $config,
+ ) {
}
/**
@@ -111,7 +89,7 @@ class Plugin extends ServerPlugin {
$this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}invite'] = Invite::class;
$this->server->on('method:POST', [$this, 'httpPost']);
- $this->server->on('propFind', [$this, 'propFind']);
+ $this->server->on('propFind', [$this, 'propFind']);
}
/**
@@ -125,8 +103,8 @@ class Plugin extends ServerPlugin {
$path = $request->getPath();
// Only handling xml
- $contentType = $request->getHeader('Content-Type');
- if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) {
+ $contentType = (string)$request->getHeader('Content-Type');
+ if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -180,7 +158,7 @@ class Plugin extends ServerPlugin {
$node->updateShares($message->set, $message->remove);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
@@ -201,6 +179,20 @@ class Plugin extends ServerPlugin {
* @return void
*/
public function propFind(PropFind $propFind, INode $node) {
+ if ($node instanceof CalendarHome && $propFind->getDepth() === 1) {
+ $backend = $node->getCalDAVBackend();
+ if ($backend instanceof CalDavBackend) {
+ $calendars = $node->getChildren();
+ $calendars = array_filter($calendars, function (INode $node) {
+ return $node instanceof IShareable;
+ });
+ /** @var int[] $resourceIds */
+ $resourceIds = array_map(function (IShareable $node) {
+ return $node->getResourceId();
+ }, $calendars);
+ $backend->preloadShares($resourceIds);
+ }
+ }
if ($node instanceof IShareable) {
$propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function () use ($node) {
return new Invite(
diff --git a/apps/dav/lib/DAV/Sharing/SharingMapper.php b/apps/dav/lib/DAV/Sharing/SharingMapper.php
new file mode 100644
index 00000000000..e4722208189
--- /dev/null
+++ b/apps/dav/lib/DAV/Sharing/SharingMapper.php
@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\DAV\Sharing;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+class SharingMapper {
+ public function __construct(
+ private IDBConnection $db,
+ ) {
+ }
+
+ protected function getSharesForIdByAccess(int $resourceId, string $resourceType, bool $sharesWithAccess): array {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['principaluri', 'access'])
+ ->from('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType, IQueryBuilder::PARAM_STR)))
+ ->groupBy(['principaluri', 'access']);
+
+ if ($sharesWithAccess) {
+ $query->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)));
+ } else {
+ $query->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)));
+ }
+
+ $result = $query->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+ return $rows;
+ }
+
+ public function getSharesForId(int $resourceId, string $resourceType): array {
+ return $this->getSharesForIdByAccess($resourceId, $resourceType, true);
+ }
+
+ public function getUnsharesForId(int $resourceId, string $resourceType): array {
+ return $this->getSharesForIdByAccess($resourceId, $resourceType, false);
+ }
+
+ public function getSharesForIds(array $resourceIds, string $resourceType): array {
+ $query = $this->db->getQueryBuilder();
+ $result = $query->select(['resourceid', 'principaluri', 'access'])
+ ->from('dav_shares')
+ ->where($query->expr()->in('resourceid', $query->createNamedParameter($resourceIds, IQueryBuilder::PARAM_INT_ARRAY)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)))
+ ->groupBy(['principaluri', 'access', 'resourceid'])
+ ->executeQuery();
+
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+ return $rows;
+ }
+
+ public function unshare(int $resourceId, string $resourceType, string $principal): void {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($principal),
+ 'type' => $query->createNamedParameter($resourceType),
+ 'access' => $query->createNamedParameter(Backend::ACCESS_UNSHARED),
+ 'resourceid' => $query->createNamedParameter($resourceId)
+ ]);
+ $query->executeStatement();
+ }
+
+ public function share(int $resourceId, string $resourceType, int $access, string $principal): void {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($principal),
+ 'type' => $query->createNamedParameter($resourceType),
+ 'access' => $query->createNamedParameter($access),
+ 'resourceid' => $query->createNamedParameter($resourceId)
+ ]);
+ $query->executeStatement();
+ }
+
+ public function deleteShare(int $resourceId, string $resourceType, string $principal): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares');
+ $query->where(
+ $query->expr()->eq('resourceid', $query->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT)),
+ $query->expr()->eq('type', $query->createNamedParameter($resourceType)),
+ $query->expr()->eq('principaluri', $query->createNamedParameter($principal))
+ );
+ $query->executeStatement();
+
+ }
+
+ public function deleteAllShares(int $resourceId, string $resourceType): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->executeStatement();
+ }
+
+ public function deleteAllSharesByUser(string $principaluri, string $resourceType): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principaluri)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->executeStatement();
+ }
+
+ public function getSharesByPrincipals(array $principals, string $resourceType): array {
+ $query = $this->db->getQueryBuilder();
+ $result = $query->select(['id', 'principaluri', 'type', 'access', 'resourceid'])
+ ->from('dav_shares')
+ ->where($query->expr()->in('principaluri', $query->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->orderBy('id')
+ ->executeQuery();
+
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ return $rows;
+ }
+
+ public function deleteUnsharesByPrincipal(string $principal, string $resourceType): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)))
+ ->executeStatement();
+ }
+}
diff --git a/apps/dav/lib/DAV/Sharing/SharingService.php b/apps/dav/lib/DAV/Sharing/SharingService.php
new file mode 100644
index 00000000000..11459e12d74
--- /dev/null
+++ b/apps/dav/lib/DAV/Sharing/SharingService.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\DAV\Sharing;
+
+abstract class SharingService {
+ protected string $resourceType = '';
+ public function __construct(
+ protected SharingMapper $mapper,
+ ) {
+ }
+
+ public function getResourceType(): string {
+ return $this->resourceType;
+ }
+ public function shareWith(int $resourceId, string $principal, int $access): void {
+ // remove the share if it already exists
+ $this->mapper->deleteShare($resourceId, $this->getResourceType(), $principal);
+ $this->mapper->share($resourceId, $this->getResourceType(), $access, $principal);
+ }
+
+ public function unshare(int $resourceId, string $principal): void {
+ $this->mapper->unshare($resourceId, $this->getResourceType(), $principal);
+ }
+
+ public function deleteShare(int $resourceId, string $principal): void {
+ $this->mapper->deleteShare($resourceId, $this->getResourceType(), $principal);
+ }
+
+ public function deleteAllShares(int $resourceId): void {
+ $this->mapper->deleteAllShares($resourceId, $this->getResourceType());
+ }
+
+ public function deleteAllSharesByUser(string $principaluri): void {
+ $this->mapper->deleteAllSharesByUser($principaluri, $this->getResourceType());
+ }
+
+ public function getShares(int $resourceId): array {
+ return $this->mapper->getSharesForId($resourceId, $this->getResourceType());
+ }
+
+ public function getUnshares(int $resourceId): array {
+ return $this->mapper->getUnsharesForId($resourceId, $this->getResourceType());
+ }
+
+ public function getSharesForIds(array $resourceIds): array {
+ return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType());
+ }
+}
diff --git a/apps/dav/lib/DAV/Sharing/Xml/Invite.php b/apps/dav/lib/DAV/Sharing/Xml/Invite.php
index 161a8dd0ebf..7a20dbe6df7 100644
--- a/apps/dav/lib/DAV/Sharing/Xml/Invite.php
+++ b/apps/dav/lib/DAV/Sharing/Xml/Invite.php
@@ -1,28 +1,10 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
- * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Robin Appelman <robin@icewind.nl>
- * @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-FileCopyrightText: fruux GmbH (https://fruux.com/)
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV\Sharing\Xml;
@@ -45,21 +27,6 @@ use Sabre\Xml\XmlSerializable;
class Invite implements XmlSerializable {
/**
- * The list of users a calendar has been shared to.
- *
- * @var array
- */
- protected $users;
-
- /**
- * The organizer contains information about the person who shared the
- * object.
- *
- * @var array|null
- */
- protected $organizer;
-
- /**
* Creates the property.
*
* Users is an array. Each element of the array has the following
@@ -85,9 +52,17 @@ class Invite implements XmlSerializable {
*
* @param array $users
*/
- public function __construct(array $users, array $organizer = null) {
- $this->users = $users;
- $this->organizer = $organizer;
+ public function __construct(
+ /**
+ * The list of users a calendar has been shared to.
+ */
+ protected array $users,
+ /**
+ * The organizer contains information about the person who shared the
+ * object.
+ */
+ protected ?array $organizer = null,
+ ) {
}
/**
@@ -100,7 +75,7 @@ class Invite implements XmlSerializable {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php
index eb5d7d4661d..aefb39c5701 100644
--- a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php
+++ b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @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\DAV\Sharing\Xml;
@@ -27,24 +12,21 @@ use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class ShareRequest implements XmlDeserializable {
- public $set = [];
-
- public $remove = [];
-
/**
* Constructor
*
* @param array $set
* @param array $remove
*/
- public function __construct(array $set, array $remove) {
- $this->set = $set;
- $this->remove = $remove;
+ public function __construct(
+ public array $set,
+ public array $remove,
+ ) {
}
public static function xmlDeserialize(Reader $reader) {
$elements = $reader->parseInnerTree([
- '{' . Plugin::NS_OWNCLOUD. '}set' => 'Sabre\\Xml\\Element\\KeyValue',
+ '{' . Plugin::NS_OWNCLOUD . '}set' => 'Sabre\\Xml\\Element\\KeyValue',
'{' . Plugin::NS_OWNCLOUD . '}remove' => 'Sabre\\Xml\\Element\\KeyValue',
]);
@@ -62,8 +44,8 @@ class ShareRequest implements XmlDeserializable {
$set[] = [
'href' => $sharee['{DAV:}href'],
- 'commonName' => isset($sharee[$commonName]) ? $sharee[$commonName] : null,
- 'summary' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null,
+ 'commonName' => $sharee[$commonName] ?? null,
+ 'summary' => $sharee[$sumElem] ?? null,
'readOnly' => !array_key_exists('{' . Plugin::NS_OWNCLOUD . '}read-write', $sharee),
];
break;
diff --git a/apps/dav/lib/DAV/SystemPrincipalBackend.php b/apps/dav/lib/DAV/SystemPrincipalBackend.php
index e5b9a20037f..9760d68f05f 100644
--- a/apps/dav/lib/DAV/SystemPrincipalBackend.php
+++ b/apps/dav/lib/DAV/SystemPrincipalBackend.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @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\DAV;
@@ -60,7 +45,7 @@ class SystemPrincipalBackend extends AbstractBackend {
}
/**
- * Returns a specific principal, specified by it's path.
+ * Returns a specific principal, specified by its path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
@@ -87,7 +72,7 @@ class SystemPrincipalBackend extends AbstractBackend {
}
/**
- * Updates one ore more webdav properties on a principal.
+ * Updates one or more webdav properties on a principal.
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object.
* To do the actual updates, you must tell this object which properties
diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php
new file mode 100644
index 00000000000..9b9615b8063
--- /dev/null
+++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2019 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\DAV;
+
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCA\DAV\Connector\Sabre\File as DavFile;
+use OCA\Files_Versions\Sabre\VersionFile;
+use OCP\Files\Folder;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\ISharedStorage;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+
+/**
+ * Sabre plugin for restricting file share receiver download:
+ */
+class ViewOnlyPlugin extends ServerPlugin {
+ private ?Server $server = null;
+
+ public function __construct(
+ private ?Folder $userFolder,
+ ) {
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ */
+ public function initialize(Server $server): void {
+ $this->server = $server;
+ //priority 90 to make sure the plugin is called before
+ //Sabre\DAV\CorePlugin::httpGet
+ $this->server->on('method:GET', [$this, 'checkViewOnly'], 90);
+ $this->server->on('method:COPY', [$this, 'checkViewOnly'], 90);
+ $this->server->on('method:MOVE', [$this, 'checkViewOnly'], 90);
+ }
+
+ /**
+ * Disallow download via DAV Api in case file being received share
+ * and having special permission
+ *
+ * @throws Forbidden
+ * @throws NotFoundException
+ */
+ public function checkViewOnly(RequestInterface $request): bool {
+ $path = $request->getPath();
+
+ try {
+ assert($this->server !== null);
+ $davNode = $this->server->tree->getNodeForPath($path);
+ if ($davNode instanceof DavFile) {
+ // Restrict view-only to nodes which are shared
+ $node = $davNode->getNode();
+ } elseif ($davNode instanceof VersionFile) {
+ $node = $davNode->getVersion()->getSourceFile();
+ $currentUserId = $this->userFolder?->getOwner()?->getUID();
+ // The version source file is relative to the owner storage.
+ // But we need the node from the current user perspective.
+ if ($node->getOwner()->getUID() !== $currentUserId) {
+ $nodes = $this->userFolder->getById($node->getId());
+ $node = array_pop($nodes);
+ if (!$node) {
+ throw new NotFoundException('Version file not accessible by current user');
+ }
+ }
+ } else {
+ return true;
+ }
+
+ $storage = $node->getStorage();
+
+ if (!$storage->instanceOfStorage(ISharedStorage::class)) {
+ return true;
+ }
+
+ // Extract extra permissions
+ /** @var ISharedStorage $storage */
+ $share = $storage->getShare();
+ $attributes = $share->getAttributes();
+ if ($attributes === null) {
+ return true;
+ }
+
+ // We have two options here, if download is disabled, but viewing is allowed,
+ // we still allow the GET request to return the file content.
+ $canDownload = $attributes->getAttribute('permissions', 'download');
+ if (!$share->canSeeContent()) {
+ throw new Forbidden('Access to this shared resource has been denied because its download permission is disabled.');
+ }
+
+ // If download is disabled, we disable the COPY and MOVE methods even if the
+ // shareapi_allow_view_without_download is set to true.
+ if ($request->getMethod() !== 'GET' && ($canDownload !== null && !$canDownload)) {
+ throw new Forbidden('Access to this shared resource has been denied because its download permission is disabled.');
+ }
+ } catch (NotFound $e) {
+ // File not found
+ }
+
+ return true;
+ }
+}