enh(sharing): enable unsharing for sharees for DAV shares (addressbooks and calendars)

Signed-off-by: Anna Larch <anna@nextcloud.com>
pull/43117/head
Anna Larch 4 months ago
parent 1f0cba5f99
commit dce69154ba

@ -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
);

@ -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);

@ -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',

@ -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',

@ -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);
}
/**

@ -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
]);

@ -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);
}
}

@ -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);
}
}

@ -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);
}
/**

@ -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);
}
}

@ -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);
}
}

@ -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;

@ -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;

@ -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[] = [

@ -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();
}
}

@ -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;
}));
}
}

@ -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;

@ -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);
}
}

@ -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();

@ -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;

@ -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']
]);

@ -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();

@ -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);

@ -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);
}
}

@ -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));
}
}

@ -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

Loading…
Cancel
Save