diff options
26 files changed, 1219 insertions, 264 deletions
diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index 8ea9c5dcb4e..d8b4feb4252 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -69,11 +69,11 @@ $calDavBackend = new CalDavBackend( $db, $principalBackend, $userManager, - \OC::$server->getGroupManager(), $random, $logger, $dispatcher, $config, + OC::$server->get(\OCA\DAV\CalDAV\Sharing\Backend::class), true ); diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php index e7faa9314e2..70e5de1b481 100644 --- a/apps/dav/appinfo/v1/carddav.php +++ b/apps/dav/appinfo/v1/carddav.php @@ -64,7 +64,13 @@ $principalBackend = new Principal( 'principals/' ); $db = \OC::$server->getDatabaseConnection(); -$cardDavBackend = new CardDavBackend($db, $principalBackend, \OC::$server->getUserManager(), \OC::$server->getGroupManager(), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class)); +$cardDavBackend = new CardDavBackend( + $db, + $principalBackend, + \OC::$server->getUserManager(), + \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class), + \OC::$server->get(\OCA\DAV\CardDAV\Sharing\Backend::class), +); $debugging = \OC::$server->getConfig()->getSystemValue('debug', false); diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index d4305195d46..b7d7fd38a9a 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -99,6 +99,8 @@ return array( 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', + 'OCA\\DAV\\CalDAV\\Sharing\\Backend' => $baseDir . '/../lib/CalDAV/Sharing/Backend.php', + 'OCA\\DAV\\CalDAV\\Sharing\\Service' => $baseDir . '/../lib/CalDAV/Sharing/Service.php', 'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php', 'OCA\\DAV\\CalDAV\\TimezoneService' => $baseDir . '/../lib/CalDAV/TimezoneService.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', @@ -129,6 +131,8 @@ return array( 'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => $baseDir . '/../lib/CardDAV/MultiGetExportPlugin.php', 'OCA\\DAV\\CardDAV\\PhotoCache' => $baseDir . '/../lib/CardDAV/PhotoCache.php', 'OCA\\DAV\\CardDAV\\Plugin' => $baseDir . '/../lib/CardDAV/Plugin.php', + 'OCA\\DAV\\CardDAV\\Sharing\\Backend' => $baseDir . '/../lib/CardDAV/Sharing/Backend.php', + 'OCA\\DAV\\CardDAV\\Sharing\\Service' => $baseDir . '/../lib/CardDAV/Sharing/Service.php', 'OCA\\DAV\\CardDAV\\SyncService' => $baseDir . '/../lib/CardDAV/SyncService.php', 'OCA\\DAV\\CardDAV\\SystemAddressbook' => $baseDir . '/../lib/CardDAV/SystemAddressbook.php', 'OCA\\DAV\\CardDAV\\UserAddressBooks' => $baseDir . '/../lib/CardDAV/UserAddressBooks.php', @@ -203,6 +207,8 @@ return array( 'OCA\\DAV\\DAV\\Sharing\\Backend' => $baseDir . '/../lib/DAV/Sharing/Backend.php', 'OCA\\DAV\\DAV\\Sharing\\IShareable' => $baseDir . '/../lib/DAV/Sharing/IShareable.php', 'OCA\\DAV\\DAV\\Sharing\\Plugin' => $baseDir . '/../lib/DAV/Sharing/Plugin.php', + 'OCA\\DAV\\DAV\\Sharing\\SharingMapper' => $baseDir . '/../lib/DAV/Sharing/SharingMapper.php', + 'OCA\\DAV\\DAV\\Sharing\\SharingService' => $baseDir . '/../lib/DAV/Sharing/SharingService.php', 'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => $baseDir . '/../lib/DAV/Sharing/Xml/Invite.php', 'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => $baseDir . '/../lib/DAV/Sharing/Xml/ShareRequest.php', 'OCA\\DAV\\DAV\\SystemPrincipalBackend' => $baseDir . '/../lib/DAV/SystemPrincipalBackend.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 9afd73635ff..627213786a1 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -114,6 +114,8 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', + 'OCA\\DAV\\CalDAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Sharing/Backend.php', + 'OCA\\DAV\\CalDAV\\Sharing\\Service' => __DIR__ . '/..' . '/../lib/CalDAV/Sharing/Service.php', 'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php', 'OCA\\DAV\\CalDAV\\TimezoneService' => __DIR__ . '/..' . '/../lib/CalDAV/TimezoneService.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', @@ -144,6 +146,8 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/MultiGetExportPlugin.php', 'OCA\\DAV\\CardDAV\\PhotoCache' => __DIR__ . '/..' . '/../lib/CardDAV/PhotoCache.php', 'OCA\\DAV\\CardDAV\\Plugin' => __DIR__ . '/..' . '/../lib/CardDAV/Plugin.php', + 'OCA\\DAV\\CardDAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/CardDAV/Sharing/Backend.php', + 'OCA\\DAV\\CardDAV\\Sharing\\Service' => __DIR__ . '/..' . '/../lib/CardDAV/Sharing/Service.php', 'OCA\\DAV\\CardDAV\\SyncService' => __DIR__ . '/..' . '/../lib/CardDAV/SyncService.php', 'OCA\\DAV\\CardDAV\\SystemAddressbook' => __DIR__ . '/..' . '/../lib/CardDAV/SystemAddressbook.php', 'OCA\\DAV\\CardDAV\\UserAddressBooks' => __DIR__ . '/..' . '/../lib/CardDAV/UserAddressBooks.php', @@ -218,6 +222,8 @@ class ComposerStaticInitDAV 'OCA\\DAV\\DAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Backend.php', 'OCA\\DAV\\DAV\\Sharing\\IShareable' => __DIR__ . '/..' . '/../lib/DAV/Sharing/IShareable.php', 'OCA\\DAV\\DAV\\Sharing\\Plugin' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Plugin.php', + 'OCA\\DAV\\DAV\\Sharing\\SharingMapper' => __DIR__ . '/..' . '/../lib/DAV/Sharing/SharingMapper.php', + 'OCA\\DAV\\DAV\\Sharing\\SharingService' => __DIR__ . '/..' . '/../lib/DAV/Sharing/SharingService.php', 'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/Invite.php', 'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/ShareRequest.php', 'OCA\\DAV\\DAV\\SystemPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/SystemPrincipalBackend.php', diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 034dccba1e0..c694892089e 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -42,8 +42,8 @@ namespace OCA\DAV\CalDAV; use DateTime; use DateTimeInterface; use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; -use OCA\DAV\DAV\Sharing\Backend; use OCA\DAV\DAV\Sharing\IShareable; use OCA\DAV\Events\CachedCalendarObjectCreatedEvent; use OCA\DAV\Events\CachedCalendarObjectDeletedEvent; @@ -72,7 +72,6 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; -use OCP\IGroupManager; use OCP\IUserManager; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; @@ -208,7 +207,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ protected array $userDisplayNames; - private Backend $calendarSharingBackend; private string $dbObjectPropertiesTable = 'calendarobjects_props'; private array $cachedObjects = []; @@ -216,14 +214,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription private IDBConnection $db, private Principal $principalBackend, private IUserManager $userManager, - IGroupManager $groupManager, private ISecureRandom $random, private LoggerInterface $logger, private IEventDispatcher $dispatcher, private IConfig $config, + private Sharing\Backend $calendarSharingBackend, private bool $legacyEndpoint = false, ) { - $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar'); } /** @@ -361,10 +358,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription // query for shared calendars $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); - $principals[] = $principalUri; $fields = array_column($this->propertyMap, 0); + $fields = array_map(function (string $field) { + return 'a.'.$field; + }, $fields); $fields[] = 'a.id'; $fields[] = 'a.uri'; $fields[] = 'a.synctoken'; @@ -372,19 +371,26 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $fields[] = 'a.principaluri'; $fields[] = 'a.transparent'; $fields[] = 's.access'; - $query = $this->db->getQueryBuilder(); - $query->select($fields) + + $select = $this->db->getQueryBuilder(); + $subSelect = $this->db->getQueryBuilder(); + + $subSelect->select('resourceid') + ->from('dav_shares', 'd') + ->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY)); + + $select->select($fields) ->from('dav_shares', 's') - ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) - ->setParameter('type', 'calendar') - ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); + ->join('s', 'calendars', 'a', $select->expr()->eq('s.resourceid', 'a.id', IQueryBuilder::PARAM_INT)) + ->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY)) + ->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('calendar', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)) + ->andWhere($select->expr()->notIn('a.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY)); - $result = $query->executeQuery(); + $results = $select->executeQuery(); $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; - while ($row = $result->fetch()) { + while ($row = $results->fetch()) { $row['principaluri'] = (string) $row['principaluri']; if ($row['principaluri'] === $principalUri) { continue; @@ -393,7 +399,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $readOnly = (int) $row['access'] === Backend::ACCESS_READ; if (isset($calendars[$row['id']])) { if ($readOnly) { - // New share can not have more permissions then the old one. + // New share can not have more permissions than the old one. continue; } if (isset($calendars[$row['id']][$readOnlyPropertyName]) && @@ -2891,7 +2897,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } $oldShares = $this->getShares($calendarId); - $this->calendarSharingBackend->updateShares($shareable, $add, $remove); + $this->calendarSharingBackend->updateShares($shareable, $add, $remove, $oldShares); $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove)); }, $this->db); @@ -2967,7 +2973,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return list<array{privilege: string, principal: string, protected: bool}> */ public function applyShareAcl(int $resourceId, array $acl): array { - return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl); + $shares = $this->calendarSharingBackend->getShares($resourceId); + return $this->calendarSharingBackend->applyShareAcl($shares, $acl); } /** diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index 92ad3242d78..fbfbdf652ec 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -236,14 +236,6 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) && $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) { $principal = 'principal:' . parent::getOwner(); - $shares = $this->caldavBackend->getShares($this->getResourceId()); - $shares = array_filter($shares, function ($share) use ($principal) { - return $share['href'] === $principal; - }); - if (empty($shares)) { - throw new Forbidden(); - } - $this->caldavBackend->updateShares($this, [], [ $principal ]); diff --git a/apps/dav/lib/CalDAV/Sharing/Backend.php b/apps/dav/lib/CalDAV/Sharing/Backend.php new file mode 100644 index 00000000000..7a87f0353e7 --- /dev/null +++ b/apps/dav/lib/CalDAV/Sharing/Backend.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\DAV\CalDAV\Sharing; + +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\Backend as SharingBackend; +use OCP\ICacheFactory; +use OCP\IGroupManager; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; + +class Backend extends SharingBackend { + + public function __construct(private IUserManager $userManager, + private IGroupManager $groupManager, + private Principal $principalBackend, + private ICacheFactory $cacheFactory, + private Service $service, + private LoggerInterface $logger, + ) { + parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->cacheFactory, $this->service, $this->logger); + } +} diff --git a/apps/dav/lib/CalDAV/Sharing/Service.php b/apps/dav/lib/CalDAV/Sharing/Service.php new file mode 100644 index 00000000000..cdf8c892ab5 --- /dev/null +++ b/apps/dav/lib/CalDAV/Sharing/Service.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\DAV\CalDAV\Sharing; + +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCA\DAV\DAV\Sharing\SharingService; + +class Service extends SharingService { + protected string $resourceType = 'calendar'; + public function __construct(protected SharingMapper $mapper) { + parent::__construct($mapper); + } +} diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index c1f0fe0c93c..bb7031caeab 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -52,7 +52,6 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; -use OCP\IGroupManager; use OCP\IUserManager; use PDO; use Sabre\CardDAV\Backend\BackendInterface; @@ -64,15 +63,11 @@ use Sabre\VObject\Reader; class CardDavBackend implements BackendInterface, SyncSupport { use TTransactional; - public const PERSONAL_ADDRESSBOOK_URI = 'contacts'; public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts'; - private Principal $principalBackend; private string $dbCardsTable = 'cards'; private string $dbCardsPropertiesTable = 'cards_properties'; - private IDBConnection $db; - private Backend $sharingBackend; /** @var array properties to index */ public static array $indexProperties = [ @@ -84,29 +79,15 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @var string[] Map of uid => display name */ protected array $userDisplayNames; - private IUserManager $userManager; - private IEventDispatcher $dispatcher; private array $etagCache = []; - /** - * CardDavBackend constructor. - * - * @param IDBConnection $db - * @param Principal $principalBackend - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param IEventDispatcher $dispatcher - */ - public function __construct(IDBConnection $db, - Principal $principalBackend, - IUserManager $userManager, - IGroupManager $groupManager, - IEventDispatcher $dispatcher) { - $this->db = $db; - $this->principalBackend = $principalBackend; - $this->userManager = $userManager; - $this->dispatcher = $dispatcher; - $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook'); + public function __construct( + private IDBConnection $db, + private Principal $principalBackend, + private IUserManager $userManager, + private IEventDispatcher $dispatcher, + private Sharing\Backend $sharingBackend, + ) { } /** @@ -149,14 +130,14 @@ class CardDavBackend implements BackendInterface, SyncSupport { return $this->atomic(function () use ($principalUri) { $principalUriOriginal = $principalUri; $principalUri = $this->convertPrincipal($principalUri, true); - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + $select = $this->db->getQueryBuilder(); + $select->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) ->from('addressbooks') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); + ->where($select->expr()->eq('principaluri', $select->createNamedParameter($principalUri))); $addressBooks = []; - $result = $query->execute(); + $result = $select->executeQuery(); while ($row = $result->fetch()) { $addressBooks[$row['id']] = [ 'id' => $row['id'], @@ -178,15 +159,22 @@ class CardDavBackend implements BackendInterface, SyncSupport { $principals[] = $principalUri; - $query = $this->db->getQueryBuilder(); - $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) + $select = $this->db->getQueryBuilder(); + $subSelect = $this->db->getQueryBuilder(); + + $subSelect->select('id') + ->from('dav_shares', 'd') + ->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(\OCA\DAV\CardDAV\Sharing\Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY)); + + + $select->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) ->from('dav_shares', 's') - ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id')) - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) - ->setParameter('type', 'addressbook') - ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) - ->execute(); + ->join('s', 'addressbooks', 'a', $select->expr()->eq('s.resourceid', 'a.id')) + ->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('addressbook', IQueryBuilder::PARAM_STR))) + ->andWhere($select->expr()->notIn('s.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY)); + $result = $select->executeQuery(); $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; while ($row = $result->fetch()) { @@ -1056,7 +1044,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $addressBookData = $this->getAddressBookById($addressBookId); $oldShares = $this->getShares($addressBookId); - $this->sharingBackend->updateShares($shareable, $add, $remove); + $this->sharingBackend->updateShares($shareable, $add, $remove, $oldShares); $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove)); }, $this->db); @@ -1418,7 +1406,8 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return list<array{privilege: string, principal: string, protected: bool}> */ public function applyShareAcl(int $addressBookId, array $acl): array { - return $this->sharingBackend->applyShareAcl($addressBookId, $acl); + $shares = $this->sharingBackend->getShares($addressBookId); + return $this->sharingBackend->applyShareAcl($shares, $acl); } /** diff --git a/apps/dav/lib/CardDAV/Sharing/Backend.php b/apps/dav/lib/CardDAV/Sharing/Backend.php new file mode 100644 index 00000000000..f0f53ba9cfa --- /dev/null +++ b/apps/dav/lib/CardDAV/Sharing/Backend.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\DAV\CardDAV\Sharing; + +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\Backend as SharingBackend; +use OCP\ICacheFactory; +use OCP\IGroupManager; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; + +class Backend extends SharingBackend { + public function __construct(private IUserManager $userManager, + private IGroupManager $groupManager, + private Principal $principalBackend, + private ICacheFactory $cacheFactory, + private Service $service, + private LoggerInterface $logger, + ) { + parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->cacheFactory, $this->service, $this->logger); + } +} diff --git a/apps/dav/lib/CardDAV/Sharing/Service.php b/apps/dav/lib/CardDAV/Sharing/Service.php new file mode 100644 index 00000000000..5da71defb5e --- /dev/null +++ b/apps/dav/lib/CardDAV/Sharing/Service.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\DAV\CardDAV\Sharing; + +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCA\DAV\DAV\Sharing\SharingService; + +class Service extends SharingService { + protected string $resourceType = 'addressbook'; + public function __construct(protected SharingMapper $mapper) { + parent::__construct($mapper); + } +} diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php index d571f113177..9acc1e147f8 100644 --- a/apps/dav/lib/Command/CreateCalendar.php +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -28,6 +28,7 @@ namespace OCA\DAV\Command; use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Proxy\ProxyMapper; +use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; use OCP\Accounts\IAccountManager; use OCP\EventDispatcher\IEventDispatcher; @@ -83,17 +84,16 @@ class CreateCalendar extends Command { $logger = \OC::$server->get(LoggerInterface::class); $dispatcher = \OC::$server->get(IEventDispatcher::class); $config = \OC::$server->get(IConfig::class); - $name = $input->getArgument('name'); $caldav = new CalDavBackend( $this->dbConnection, $principalBackend, $this->userManager, - $this->groupManager, $random, $logger, $dispatcher, - $config + $config, + \OC::$server->get(Backend::class), ); $caldav->createCalendar("principals/users/$user", $name, []); return self::SUCCESS; diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php index f574cec00c6..e643304ecec 100644 --- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php +++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php @@ -26,6 +26,7 @@ */ namespace OCA\DAV\Connector\Sabre; +use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CardDAV\AddressBook; use Sabre\CalDAV\Principal\User; use Sabre\DAV\Exception\NotFound; @@ -58,6 +59,9 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin { case AddressBook::class: $type = 'Addressbook'; break; + case Calendar::class: + $type = 'Calendar'; + break; default: $type = 'Node'; break; diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php index b115ef61313..f77be9211bf 100644 --- a/apps/dav/lib/DAV/Sharing/Backend.php +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -1,4 +1,6 @@ <?php + +declare(strict_types=1); /** * @copyright Copyright (c) 2016, ownCloud, Inc. * @@ -10,6 +12,7 @@ * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Thomas Citharel <nextcloud@tcit.fr> * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Anna Larch <anna.larch@gmx.net> * * @license AGPL-3.0 * @@ -30,142 +33,104 @@ namespace OCA\DAV\DAV\Sharing; use OCA\DAV\Connector\Sabre\Principal; use OCP\AppFramework\Db\TTransactional; -use OCP\Cache\CappedMemoryCache; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IGroupManager; use OCP\IUserManager; +use Psr\Log\LoggerInterface; -class Backend { +abstract class Backend { use TTransactional; - - private IDBConnection $db; - private IUserManager $userManager; - private IGroupManager $groupManager; - private Principal $principalBackend; - private string $resourceType; - public const ACCESS_OWNER = 1; + public const ACCESS_READ_WRITE = 2; public const ACCESS_READ = 3; - - private CappedMemoryCache $shareCache; - - public function __construct(IDBConnection $db, IUserManager $userManager, IGroupManager $groupManager, Principal $principalBackend, string $resourceType) { - $this->db = $db; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->principalBackend = $principalBackend; - $this->resourceType = $resourceType; - $this->shareCache = new CappedMemoryCache(); + // 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 list<array{href: string, commonName: string, readOnly: bool}> $add * @param list<string> $remove */ - public function updateShares(IShareable $shareable, array $add, array $remove): void { + public function updateShares(IShareable $shareable, array $add, array $remove, array $oldShares = []): void { $this->shareCache->clear(); - $this->atomic(function () use ($shareable, $add, $remove) { - foreach ($add as $element) { - $principal = $this->principalBackend->findByUri($element['href'], ''); - if ($principal !== '') { - $this->shareWith($shareable, $element); - } + foreach ($add as $element) { + $principal = $this->principalBackend->findByUri($element['href'], ''); + 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; } - }, $this->db); - } - /** - * @param array{href: string, commonName: string, readOnly: bool} $element - */ - private function shareWith(IShareable $shareable, array $element): void { - $this->shareCache->clear(); - $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; + } + + // Delete any possible direct shares (since the frontend does not separate between them) + $this->service->deleteShare($shareable->getResourceId(), $principal); - $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->executeStatement(); + // Check if a user has a groupshare that they're trying to free themselves from + // If so we need to add a self::ACCESS_UNSHARED row + if(!str_contains($principal, 'group') + && $this->service->hasGroupShare($oldShares) + ) { + $this->service->unshare($shareable->getResourceId(), $principal); + } + } } public function deleteAllShares(int $resourceId): void { $this->shareCache->clear(); - $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))) - ->executeStatement(); + $this->service->deleteAllShares($resourceId); } public function deleteAllSharesByUser(string $principaluri): void { $this->shareCache->clear(); - $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))) - ->executeStatement(); - } - - private function unshare(IShareable $shareable, string $element): void { - $this->shareCache->clear(); - $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->executeStatement(); + $this->service->deleteAllSharesByUser($principaluri); } /** @@ -181,52 +146,39 @@ class Backend { * @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(int $resourceId): array { - $cached = $this->shareCache->get($resourceId); + $cached = $this->shareCache->get((string)$resourceId); if ($cached) { return $cached; } - $query = $this->db->getQueryBuilder(); - $result = $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($this->resourceType))) - ->groupBy(['principaluri', 'access']) - ->executeQuery(); + $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']) ? (string)$p['{DAV:}displayname'] : '', 'status' => 1, - 'readOnly' => (int) $row['access'] === self::ACCESS_READ, + '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') : false + '{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups') ]; } - $this->shareCache->set((string)$resourceId, $shares); return $shares; } public function preloadShares(array $resourceIds): void { $resourceIds = array_filter($resourceIds, function (int $resourceId) { - return !isset($this->shareCache[$resourceId]); + return empty($this->shareCache->get((string)$resourceId)); }); - if (count($resourceIds) === 0) { + if (empty($resourceIds)) { return; } - $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($this->resourceType))) - ->groupBy(['principaluri', 'access', 'resourceid']) - ->executeQuery(); + $rows = $this->service->getSharesForIds($resourceIds); $sharesByResource = array_fill_keys($resourceIds, []); - while ($row = $result->fetch()) { + foreach($rows as $row) { $resourceId = (int)$row['resourceid']; $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); $sharesByResource[$resourceId][] = [ @@ -235,12 +187,9 @@ class Backend { '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') : false + '{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups') ]; - } - - foreach ($resourceIds as $resourceId) { - $this->shareCache->set($resourceId, $sharesByResource[$resourceId]); + $this->shareCache->set((string)$resourceId, $sharesByResource[$resourceId]); } } @@ -249,10 +198,10 @@ class Backend { * * @param int $resourceId * @param list<array{privilege: string, principal: string, protected: bool}> $acl - * @return list<array{privilege: string, principal: string, protected: bool}> + * @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(int $resourceId, array $acl): array { - $shares = $this->getShares($resourceId); + public function applyShareAcl(array $shares, array $acl): array { foreach ($shares as $share) { $acl[] = [ 'privilege' => '{DAV:}read', @@ -265,7 +214,7 @@ class Backend { 'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'], 'protected' => true, ]; - } elseif ($this->resourceType === 'calendar') { + } elseif ($this->service->getResourceType() === 'calendar') { // Allow changing the properties of read only calendars, // so users can change the visibility. $acl[] = [ diff --git a/apps/dav/lib/DAV/Sharing/SharingMapper.php b/apps/dav/lib/DAV/Sharing/SharingMapper.php new file mode 100644 index 00000000000..c0c939c7a5e --- /dev/null +++ b/apps/dav/lib/DAV/Sharing/SharingMapper.php @@ -0,0 +1,113 @@ +<?php + +declare(strict_types=1); +/* + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\DAV\DAV\Sharing; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class SharingMapper { + public function __construct(private IDBConnection $db) { + } + + public function getSharesForId(int $resourceId, string $resourceType): array { + $query = $this->db->getQueryBuilder(); + $result = $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))) + ->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT))) + ->groupBy(['principaluri', 'access']) + ->executeQuery(); + + $rows = $result->fetchAll(); + $result->closeCursor(); + return $rows; + } + + 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(); + } +} diff --git a/apps/dav/lib/DAV/Sharing/SharingService.php b/apps/dav/lib/DAV/Sharing/SharingService.php new file mode 100644 index 00000000000..4b2a0beed1c --- /dev/null +++ b/apps/dav/lib/DAV/Sharing/SharingService.php @@ -0,0 +1,71 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ +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 getSharesForIds(array $resourceIds): array { + return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType()); + } + + /** + * @param array $oldShares + * @return bool + */ + public function hasGroupShare(array $oldShares): bool { + return !empty(array_filter($oldShares, function (array $share) { + return $share['{http://owncloud.org/ns}group-share'] === true; + })); + } +} diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index c751a4babf5..0e9af0a4276 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -37,6 +37,7 @@ use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\PublicCalendarRoot; use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend; use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend; +use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\CardDAV\AddressBookRoot; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\Connector\Sabre\Principal; @@ -80,6 +81,7 @@ class RootCollection extends SimpleCollection { \OC::$server->getConfig(), \OC::$server->getL10NFactory() ); + $groupPrincipalBackend = new GroupPrincipalBackend($groupManager, $userSession, $shareManager, $config); $calendarResourcePrincipalBackend = new ResourcePrincipalBackend($db, $userSession, $groupManager, $logger, $proxyMapper); $calendarRoomPrincipalBackend = new RoomPrincipalBackend($db, $userSession, $groupManager, $logger, $proxyMapper); @@ -97,7 +99,7 @@ class RootCollection extends SimpleCollection { $calendarResourcePrincipals->disableListing = $disableListing; $calendarRoomPrincipals = new Collection($calendarRoomPrincipalBackend, 'principals/calendar-rooms'); $calendarRoomPrincipals->disableListing = $disableListing; - + $calendarSharingBackend = \OC::$server->get(Backend::class); $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); $filesCollection->disableListing = $disableListing; @@ -105,11 +107,12 @@ class RootCollection extends SimpleCollection { $db, $userPrincipalBackend, $userManager, - $groupManager, $random, $logger, $dispatcher, - $config + $config, + $calendarSharingBackend, + false, ); $userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $logger); $userCalendarRoot->disableListing = $disableListing; @@ -142,12 +145,26 @@ class RootCollection extends SimpleCollection { $logger ); + $contactsSharingBackend = \OC::$server->get(\OCA\DAV\CardDAV\Sharing\Backend::class); + $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); - $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); + $usersCardDavBackend = new CardDavBackend( + $db, + $userPrincipalBackend, + $userManager, + $dispatcher, + $contactsSharingBackend, + ); $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; - $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); + $systemCardDavBackend = new CardDavBackend( + $db, + $userPrincipalBackend, + $userManager, + $dispatcher, + $contactsSharingBackend, + ); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; diff --git a/apps/dav/tests/integration/DAV/Sharing/SharingMapperTest.php b/apps/dav/tests/integration/DAV/Sharing/SharingMapperTest.php new file mode 100644 index 00000000000..249f11c3780 --- /dev/null +++ b/apps/dav/tests/integration/DAV/Sharing/SharingMapperTest.php @@ -0,0 +1,109 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * @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/>. + */ + +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCP\IDBConnection; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + */ +class SharingMapperTest extends TestCase { + + private SharingMapper $mapper; + private IDBConnection $db; + + protected function setUp(): void { + parent::setUp(); + + $this->db = Server::get(IDBConnection::class); + $this->mapper = new SharingMapper($this->db); + $qb = $this->db->getQueryBuilder(); + $qb->delete('dav_shares')->executeStatement(); + } + + public function testShareAndGet(): void { + $resourceId = 42; + $resourceType = 'calendar'; + $access = 3; + $principal = 'principals/users/bob'; + $this->mapper->share($resourceId, $resourceType, $access, $principal); + $shares = $this->mapper->getSharesForId($resourceId, $resourceType); + $this->assertCount(1, $shares); + } + + public function testShareDelete(): void { + $resourceId = 42; + $resourceType = 'calendar'; + $access = 3; + $principal = 'principals/users/bob'; + $this->mapper->share($resourceId, $resourceType, $access, $principal); + $this->mapper->deleteShare($resourceId, $resourceType, $principal); + $shares = $this->mapper->getSharesForId($resourceId, $resourceType); + $this->assertEmpty($shares); + } + + public function testShareUnshare(): void { + $resourceId = 42; + $resourceType = 'calendar'; + $access = 3; + $principal = 'principals/groups/alicegroup'; + $userPrincipal = 'principals/users/alice'; + $this->mapper->share($resourceId, $resourceType, $access, $principal); + $this->mapper->unshare($resourceId, $resourceType, $userPrincipal); + $shares = $this->mapper->getSharesForId($resourceId, $resourceType); + $this->assertCount(1, $shares); + } + + public function testShareDeleteAll(): void { + $resourceId = 42; + $resourceType = 'calendar'; + $access = 3; + $principal = 'principals/groups/alicegroup'; + $userPrincipal = 'principals/users/alice'; + $this->mapper->share($resourceId, $resourceType, $access, $principal); + $this->mapper->unshare($resourceId, $resourceType, $userPrincipal); + $shares = $this->mapper->getSharesForId($resourceId, $resourceType); + $this->assertCount(1, $shares); + $this->mapper->deleteAllShares($resourceId, $resourceType); + $shares = $this->mapper->getSharesForId($resourceId, $resourceType); + $this->assertEmpty($shares); + } + + public function testShareDeleteAllForUser(): void { + $resourceId = 42; + $resourceType = 'calendar'; + $access = 3; + $principal = 'principals/groups/alicegroup'; + $this->mapper->share($resourceId, $resourceType, $access, $principal); + $shares = $this->mapper->getSharesForId($resourceId, $resourceType); + $this->assertCount(1, $shares); + $this->mapper->deleteAllSharesByUser($principal, $resourceType); + $shares = $this->mapper->getSharesForId($resourceId, $resourceType); + $this->assertEmpty($shares); + } + +} diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php index 4dd61c6e307..dbed804ea41 100644 --- a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php +++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php @@ -30,10 +30,14 @@ namespace OCA\DAV\Tests\unit\CalDAV; use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Proxy\ProxyMapper; +use OCA\DAV\CalDAV\Sharing\Backend as SharingBackend; +use OCA\DAV\CalDAV\Sharing\Service; use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\SharingMapper; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IGroupManager; use OCP\IUserManager; @@ -56,26 +60,16 @@ use Test\TestCase; */ abstract class AbstractCalDavBackend extends TestCase { - /** @var CalDavBackend */ - protected $backend; - - /** @var Principal | MockObject */ - protected $principal; - /** @var IUserManager|MockObject */ - protected $userManager; - /** @var IGroupManager|MockObject */ - protected $groupManager; - /** @var IEventDispatcher|MockObject */ - protected $dispatcher; - - - /** @var IConfig | MockObject */ - private $config; - /** @var ISecureRandom */ - private $random; - /** @var LoggerInterface*/ - private $logger; + protected CalDavBackend $backend; + protected Principal|MockObject $principal; + protected IUserManager|MockObject $userManager; + protected IGroupManager|MockObject $groupManager; + protected IEventDispatcher|MockObject $dispatcher; + private LoggerInterface|MockObject $logger; + private IConfig|MockObject $config; + private ISecureRandom $random; + protected SharingBackend $sharingBackend; public const UNIT_TEST_USER = 'principals/users/caldav-unit-test'; public const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1'; public const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group'; @@ -100,7 +94,7 @@ abstract class AbstractCalDavBackend extends TestCase { $this->createMock(IConfig::class), $this->createMock(IFactory::class) ]) - ->setMethods(['getPrincipalByPath', 'getGroupMembership']) + ->setMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) ->getMock(); $this->principal->expects($this->any())->method('getPrincipalByPath') ->willReturn([ @@ -115,15 +109,23 @@ abstract class AbstractCalDavBackend extends TestCase { $this->random = \OC::$server->getSecureRandom(); $this->logger = $this->createMock(LoggerInterface::class); $this->config = $this->createMock(IConfig::class); + $this->sharingBackend = new SharingBackend( + $this->userManager, + $this->groupManager, + $this->principal, + $this->createMock(ICacheFactory::class), + new Service(new SharingMapper($db)), + $this->logger); $this->backend = new CalDavBackend( $db, $this->principal, $this->userManager, - $this->groupManager, $this->random, $this->logger, $this->dispatcher, - $this->config + $this->config, + $this->sharingBackend, + false, ); $this->cleanUpBackend(); diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php index b23091275ef..112786eb987 100644 --- a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php +++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php @@ -93,6 +93,9 @@ class CalDavBackendTest extends AbstractCalDavBackend { 'href' => 'principal:' . self::UNIT_TEST_GROUP, 'readOnly' => true ] + ], [ + self::UNIT_TEST_USER1, + self::UNIT_TEST_GROUP, ]], [true, true, true, false, [ [ @@ -103,6 +106,9 @@ class CalDavBackendTest extends AbstractCalDavBackend { 'href' => 'principal:' . self::UNIT_TEST_GROUP2, 'readOnly' => false, ], + ], [ + self::UNIT_TEST_GROUP, + self::UNIT_TEST_GROUP2, ]], [true, true, true, true, [ [ @@ -113,12 +119,17 @@ class CalDavBackendTest extends AbstractCalDavBackend { 'href' => 'principal:' . self::UNIT_TEST_GROUP2, 'readOnly' => true, ], + ], [ + self::UNIT_TEST_GROUP, + self::UNIT_TEST_GROUP2, ]], [true, false, false, false, [ [ 'href' => 'principal:' . self::UNIT_TEST_USER1, 'readOnly' => true ], + ], [ + self::UNIT_TEST_USER1, ]], ]; @@ -127,27 +138,26 @@ class CalDavBackendTest extends AbstractCalDavBackend { /** * @dataProvider providesSharingData */ - public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add): void { - /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l10n */ + public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add, $principals): void { + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $config = $this->createMock(IConfig::class); + /** @var IL10N|MockObject $l10n */ $l10n = $this->createMock(IL10N::class); - $l10n - ->expects($this->any()) + $l10n->expects($this->any()) ->method('t') ->willReturnCallback(function ($text, $parameters = []) { return vsprintf($text, $parameters); }); - $logger = $this->createMock(\Psr\Log\LoggerInterface::class); - - $config = $this->createMock(IConfig::class); - $this->userManager->expects($this->any()) ->method('userExists') ->willReturn(true); - $this->groupManager->expects($this->any()) ->method('groupExists') ->willReturn(true); + $this->principal->expects(self::atLeastOnce()) + ->method('findByUri') + ->willReturnOnConsecutiveCalls(...$principals); $calendarId = $this->createTestCalendar(); $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); @@ -1250,6 +1260,9 @@ EOD; $this->groupManager->expects($this->any()) ->method('groupExists') ->willReturn(true); + $this->principal->expects(self::atLeastOnce()) + ->method('findByUri') + ->willReturn(self::UNIT_TEST_USER); $me = self::UNIT_TEST_USER; $sharer = self::UNIT_TEST_USER1; diff --git a/apps/dav/tests/unit/CalDAV/CalendarTest.php b/apps/dav/tests/unit/CalDAV/CalendarTest.php index d61e4b58478..68e6e5ef251 100644 --- a/apps/dav/tests/unit/CalDAV/CalendarTest.php +++ b/apps/dav/tests/unit/CalDAV/CalendarTest.php @@ -83,11 +83,9 @@ class CalendarTest extends TestCase { public function testDeleteFromGroup(): void { - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); - /** @var MockObject | CalDavBackend $backend */ $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock(); - $backend->expects($this->never())->method('updateShares'); + $backend->expects($this->once())->method('updateShares'); $backend->expects($this->any())->method('getShares')->willReturn([ ['href' => 'principal:group2'] ]); diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php index 6634a602e74..3460c548de4 100644 --- a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php +++ b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php @@ -84,6 +84,7 @@ class PublicCalendarRootTest extends TestCase { $this->logger = $this->createMock(LoggerInterface::class); $dispatcher = $this->createMock(IEventDispatcher::class); $config = $this->createMock(IConfig::class); + $sharingBackend = $this->createMock(\OCA\DAV\CalDAV\Sharing\Backend::class); $this->principal->expects($this->any())->method('getGroupMembership') ->withAnyParameters() @@ -97,11 +98,12 @@ class PublicCalendarRootTest extends TestCase { $db, $this->principal, $this->userManager, - $this->groupManager, $this->random, $this->logger, $dispatcher, - $config + $config, + $sharingBackend, + false, ); $this->l10n = $this->getMockBuilder(IL10N::class) ->disableOriginalConstructor()->getMock(); diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php index 425e7c44ba7..ea80187f554 100644 --- a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php +++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php @@ -37,11 +37,15 @@ use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CardDAV\AddressBook; use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\Sharing\Backend; +use OCA\DAV\CardDAV\Sharing\Service; use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\SharingMapper; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroupManager; @@ -50,6 +54,7 @@ use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; use OCP\Share\IManager as ShareManager; +use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\PropPatch; use Sabre\VObject\Component\VCard; @@ -78,7 +83,7 @@ class CardDavBackendTest extends TestCase { /** @var IEventDispatcher|MockObject */ private $dispatcher; - + private Backend $sharingBackend; /** @var IDBConnection */ private $db; @@ -141,7 +146,7 @@ class CardDavBackendTest extends TestCase { $this->createMock(IConfig::class), $this->createMock(IFactory::class) ]) - ->setMethods(['getPrincipalByPath', 'getGroupMembership']) + ->setMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) ->getMock(); $this->principal->method('getPrincipalByPath') ->willReturn([ @@ -154,8 +159,20 @@ class CardDavBackendTest extends TestCase { $this->dispatcher = $this->createMock(IEventDispatcher::class); $this->db = \OC::$server->getDatabaseConnection(); - - $this->backend = new CardDavBackend($this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher); + $this->sharingBackend = new Backend($this->userManager, + $this->groupManager, + $this->principal, + $this->createMock(ICacheFactory::class), + new Service(new SharingMapper($this->db)), + $this->createMock(LoggerInterface::class) + ); + + $this->backend = new CardDavBackend($this->db, + $this->principal, + $this->userManager, + $this->dispatcher, + $this->sharingBackend, + ); // start every test with a empty cards_properties and cards table $query = $this->db->getQueryBuilder(); $query->delete('cards_properties')->execute(); @@ -213,10 +230,12 @@ class CardDavBackendTest extends TestCase { $this->userManager->expects($this->any()) ->method('userExists') ->willReturn(true); - $this->groupManager->expects($this->any()) ->method('groupExists') ->willReturn(true); + $this->principal->expects(self::atLeastOnce()) + ->method('findByUri') + ->willReturnOnConsecutiveCalls(self::UNIT_TEST_USER1, self::UNIT_TEST_GROUP); $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); @@ -243,7 +262,7 @@ class CardDavBackendTest extends TestCase { public function testCardOperations(): void { /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */ $backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->onlyMethods(['updateProperties', 'purgeProperties'])->getMock(); // create a new address book @@ -298,7 +317,7 @@ class CardDavBackendTest extends TestCase { public function testMultiCard(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->setMethods(['updateProperties'])->getMock(); // create a new address book @@ -351,7 +370,7 @@ class CardDavBackendTest extends TestCase { public function testMultipleUIDOnDifferentAddressbooks(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->onlyMethods(['updateProperties'])->getMock(); // create 2 new address books @@ -373,7 +392,7 @@ class CardDavBackendTest extends TestCase { public function testMultipleUIDDenied(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->setMethods(['updateProperties'])->getMock(); // create a new address book @@ -394,7 +413,7 @@ class CardDavBackendTest extends TestCase { public function testNoValidUID(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->setMethods(['updateProperties'])->getMock(); // create a new address book @@ -411,7 +430,7 @@ class CardDavBackendTest extends TestCase { public function testDeleteWithoutCard(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->onlyMethods([ 'getCardId', 'addChange', @@ -451,7 +470,7 @@ class CardDavBackendTest extends TestCase { public function testSyncSupport(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->setMethods(['updateProperties'])->getMock(); // create a new address book @@ -477,10 +496,12 @@ class CardDavBackendTest extends TestCase { $this->userManager->expects($this->any()) ->method('userExists') ->willReturn(true); - $this->groupManager->expects($this->any()) ->method('groupExists') ->willReturn(true); + $this->principal->expects(self::any()) + ->method('findByUri') + ->willReturn(self::UNIT_TEST_USER1); $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); @@ -517,7 +538,7 @@ class CardDavBackendTest extends TestCase { $cardId = 2; $backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) ->onlyMethods(['getCardId'])->getMock(); $backend->expects($this->any())->method('getCardId')->willReturn($cardId); diff --git a/apps/dav/tests/unit/DAV/Sharing/BackendTest.php b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php new file mode 100644 index 00000000000..eaa8f3805a1 --- /dev/null +++ b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php @@ -0,0 +1,427 @@ +<?php + +declare(strict_types=1); +/* + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ +namespace OCA\DAV\Tests\unit\DAV\Sharing; + +use OCA\DAV\CalDAV\Sharing\Backend as CalendarSharingBackend; +use OCA\DAV\CalDAV\Sharing\Service; +use OCA\DAV\CardDAV\Sharing\Backend as ContactsSharingBackend; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\Backend; +use OCA\DAV\DAV\Sharing\IShareable; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class BackendTest extends TestCase { + + private IDBConnection|MockObject $db; + private IUserManager|MockObject $userManager; + private IGroupManager|MockObject $groupManager; + private MockObject|Principal $principalBackend; + private MockObject|ICache $shareCache; + private LoggerInterface|MockObject $logger; + private MockObject|ICacheFactory $cacheFactory; + private Service|MockObject $calendarService; + private CalendarSharingBackend $backend; + + protected function setUp(): void { + parent::setUp(); + $this->db = $this->createMock(IDBConnection::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->principalBackend = $this->createMock(Principal::class); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->shareCache = $this->createMock(ICache::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->calendarService = $this->createMock(Service::class); + $this->cacheFactory->expects(self::any()) + ->method('createInMemory') + ->willReturn($this->shareCache); + + $this->backend = new CalendarSharingBackend( + $this->userManager, + $this->groupManager, + $this->principalBackend, + $this->cacheFactory, + $this->calendarService, + $this->logger, + ); + } + + public function testUpdateShareCalendarBob(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/users/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/users/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::once()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects(self::never()) + ->method('groupExists'); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareCalendarGroup(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/groups/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/groups/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::never()) + ->method('userExists'); + $this->groupManager->expects(self::once()) + ->method('groupExists') + ->willReturn(true); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareContactsBob(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/users/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/users/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::once()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects(self::never()) + ->method('groupExists'); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareContactsGroup(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/groups/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/groups/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::never()) + ->method('userExists'); + $this->groupManager->expects(self::once()) + ->method('groupExists') + ->willReturn(true); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareCircle(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/circles/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/groups/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::never()) + ->method('userExists'); + $this->groupManager->expects(self::once()) + ->method('groupExists') + ->willReturn(true); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUnshareBob(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $remove = [ + [ + 'href' => 'principal:principals/users/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/users/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->calendarService->expects(self::once()) + ->method('deleteShare') + ->with($shareable->getResourceId(), $principal); + $this->calendarService->expects(self::once()) + ->method('hasGroupShare') + ->willReturn(false); + $this->calendarService->expects(self::never()) + ->method('unshare'); + + $this->backend->updateShares($shareable, [], $remove); + } + + public function testUnshareWithBobGroup(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $remove = [ + [ + 'href' => 'principal:principals/users/bob', + 'readOnly' => true, + ] + ]; + $oldShares = [ + [ + 'href' => 'principal:principals/groups/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => 'principals/groups/bob', + '{http://owncloud.org/ns}group-share' => true, + ] + ]; + + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn('principals/users/bob'); + $this->calendarService->expects(self::once()) + ->method('deleteShare') + ->with($shareable->getResourceId(), 'principals/users/bob'); + $this->calendarService->expects(self::once()) + ->method('hasGroupShare') + ->with($oldShares) + ->willReturn(true); + $this->calendarService->expects(self::once()) + ->method('unshare') + ->with($shareable->getResourceId(), 'principals/users/bob'); + + $this->backend->updateShares($shareable, [], $remove, $oldShares); + } + + public function testGetShares(): void { + $resourceId = 42; + $principal = 'principals/groups/bob'; + $rows = [ + [ + 'principaluri' => $principal, + 'access' => Backend::ACCESS_READ, + ] + ]; + $expected = [ + [ + 'href' => 'principal:principals/groups/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => $principal, + '{http://owncloud.org/ns}group-share' => true, + ] + ]; + + + $this->shareCache->expects(self::once()) + ->method('get') + ->with((string)$resourceId) + ->willReturn(null); + $this->calendarService->expects(self::once()) + ->method('getShares') + ->with($resourceId) + ->willReturn($rows); + $this->principalBackend->expects(self::once()) + ->method('getPrincipalByPath') + ->with($principal) + ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']); + $this->shareCache->expects(self::once()) + ->method('set') + ->with((string)$resourceId, $expected); + + $result = $this->backend->getShares($resourceId); + $this->assertEquals($expected, $result); + } + + public function testGetSharesAddressbooks(): void { + $service = $this->createMock(\OCA\DAV\CardDAV\Sharing\Service::class); + $backend = new ContactsSharingBackend( + $this->userManager, + $this->groupManager, + $this->principalBackend, + $this->cacheFactory, + $service, + $this->logger); + $resourceId = 42; + $principal = 'principals/groups/bob'; + $rows = [ + [ + 'principaluri' => $principal, + 'access' => Backend::ACCESS_READ, + ] + ]; + $expected = [ + [ + 'href' => 'principal:principals/groups/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => $principal, + '{http://owncloud.org/ns}group-share' => true, + ] + ]; + + $this->shareCache->expects(self::once()) + ->method('get') + ->with((string)$resourceId) + ->willReturn(null); + $service->expects(self::once()) + ->method('getShares') + ->with($resourceId) + ->willReturn($rows); + $this->principalBackend->expects(self::once()) + ->method('getPrincipalByPath') + ->with($principal) + ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']); + $this->shareCache->expects(self::once()) + ->method('set') + ->with((string)$resourceId, $expected); + + $result = $backend->getShares($resourceId); + $this->assertEquals($expected, $result); + } + + public function testPreloadShares(): void { + $resourceIds = [42, 99]; + $rows = [ + [ + 'resourceid' => 42, + 'principaluri' => 'principals/groups/bob', + 'access' => Backend::ACCESS_READ, + ], + [ + 'resourceid' => 99, + 'principaluri' => 'principals/users/carlos', + 'access' => Backend::ACCESS_READ_WRITE, + ] + ]; + $principalResults = [ + ['uri' => 'principals/groups/bob', '{DAV:}displayname' => 'bob'], + ['uri' => 'principals/users/carlos', '{DAV:}displayname' => 'carlos'], + ]; + + $this->shareCache->expects(self::exactly(2)) + ->method('get') + ->willReturn(null); + $this->calendarService->expects(self::once()) + ->method('getSharesForIds') + ->with($resourceIds) + ->willReturn($rows); + $this->principalBackend->expects(self::exactly(2)) + ->method('getPrincipalByPath') + ->willReturnCallback(function (string $principal) use ($principalResults) { + switch ($principal) { + case 'principals/groups/bob': + return $principalResults[0]; + default: + return $principalResults[1]; + } + }); + $this->shareCache->expects(self::exactly(2)) + ->method('set'); + + $this->backend->preloadShares($resourceIds); + } +} diff --git a/apps/dav/tests/unit/DAV/Sharing/SharingServiceTest.php b/apps/dav/tests/unit/DAV/Sharing/SharingServiceTest.php new file mode 100644 index 00000000000..97cd3176d3b --- /dev/null +++ b/apps/dav/tests/unit/DAV/Sharing/SharingServiceTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); +/* + * @copyright 2024 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + */ +namespace OCA\DAV\Tests\unit\DAV\Sharing; + +use OCA\DAV\CalDAV\Sharing\Service; +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCA\DAV\DAV\Sharing\SharingService; +use Test\TestCase; + +class SharingServiceTest extends TestCase { + + private SharingService $service; + + protected function setUp(): void { + parent::setUp(); + $this->service = new Service($this->createMock(SharingMapper::class)); + } + + public function testHasGroupShare(): void { + $oldShares = [ + [ + 'href' => 'principal:principals/groups/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => 'principals/groups/bob', + '{http://owncloud.org/ns}group-share' => true, + ], + [ + 'href' => 'principal:principals/users/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => 'principals/users/bob', + '{http://owncloud.org/ns}group-share' => false, + ] + ]; + + $this->assertTrue($this->service->hasGroupShare($oldShares)); + + $oldShares = [ + [ + 'href' => 'principal:principals/users/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => 'principals/users/bob', + '{http://owncloud.org/ns}group-share' => false, + ] + ]; + $this->assertFalse($this->service->hasGroupShare($oldShares)); + } +} diff --git a/build/integration/dav_features/caldav.feature b/build/integration/dav_features/caldav.feature index e2cb4f8dc92..fffdd89d367 100644 --- a/build/integration/dav_features/caldav.feature +++ b/build/integration/dav_features/caldav.feature @@ -13,7 +13,7 @@ Feature: caldav When "user0" requests calendar "admin/MyCalendar" on the endpoint "/remote.php/dav/calendars/" Then The CalDAV HTTP status code should be "404" And The exception is "Sabre\DAV\Exception\NotFound" - And The error message is "Node with name 'MyCalendar' could not be found" + And The error message is "Calendar with name 'MyCalendar' could not be found" Scenario: Accessing a not shared calendar of another user via the legacy endpoint Given user "user0" exists @@ -22,7 +22,7 @@ Feature: caldav When "user0" requests calendar "admin/MyCalendar" on the endpoint "/remote.php/caldav/calendars/" Then The CalDAV HTTP status code should be "404" And The exception is "Sabre\DAV\Exception\NotFound" - And The error message is "Node with name 'MyCalendar' could not be found" + And The error message is "Calendar with name 'MyCalendar' could not be found" Scenario: Accessing a not existing calendar of another user Given user "user0" exists |