aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/CardDAV
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/CardDAV')
-rw-r--r--apps/dav/lib/CardDAV/Activity/Backend.php54
-rw-r--r--apps/dav/lib/CardDAV/Activity/Filter.php18
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php27
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Base.php30
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Card.php33
-rw-r--r--apps/dav/lib/CardDAV/Activity/Setting.php4
-rw-r--r--apps/dav/lib/CardDAV/AddressBook.php15
-rw-r--r--apps/dav/lib/CardDAV/AddressBookImpl.php95
-rw-r--r--apps/dav/lib/CardDAV/AddressBookRoot.php20
-rw-r--r--apps/dav/lib/CardDAV/Card.php2
-rw-r--r--apps/dav/lib/CardDAV/CardDavBackend.php178
-rw-r--r--apps/dav/lib/CardDAV/ContactsManager.php35
-rw-r--r--apps/dav/lib/CardDAV/Converter.php17
-rw-r--r--apps/dav/lib/CardDAV/ImageExportPlugin.php14
-rw-r--r--apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php10
-rw-r--r--apps/dav/lib/CardDAV/MultiGetExportPlugin.php5
-rw-r--r--apps/dav/lib/CardDAV/PhotoCache.php42
-rw-r--r--apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php9
-rw-r--r--apps/dav/lib/CardDAV/Sharing/Backend.php3
-rw-r--r--apps/dav/lib/CardDAV/Sharing/Service.php4
-rw-r--r--apps/dav/lib/CardDAV/SyncService.php249
-rw-r--r--apps/dav/lib/CardDAV/SystemAddressbook.php37
-rw-r--r--apps/dav/lib/CardDAV/UserAddressBooks.php29
-rw-r--r--apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php40
-rw-r--r--apps/dav/lib/CardDAV/Xml/Groups.php10
25 files changed, 546 insertions, 434 deletions
diff --git a/apps/dav/lib/CardDAV/Activity/Backend.php b/apps/dav/lib/CardDAV/Activity/Backend.php
index 34cf429022d..b08414d3b02 100644
--- a/apps/dav/lib/CardDAV/Activity/Backend.php
+++ b/apps/dav/lib/CardDAV/Activity/Backend.php
@@ -22,31 +22,13 @@ use Sabre\VObject\Reader;
class Backend {
- /** @var IActivityManager */
- protected $activityManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var IAppManager */
- protected $appManager;
-
- /** @var IUserManager */
- protected $userManager;
-
- public function __construct(IActivityManager $activityManager,
- IGroupManager $groupManager,
- IUserSession $userSession,
- IAppManager $appManager,
- IUserManager $userManager) {
- $this->activityManager = $activityManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
- $this->userManager = $userManager;
+ public function __construct(
+ protected IActivityManager $activityManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IAppManager $appManager,
+ protected IUserManager $userManager,
+ ) {
}
/**
@@ -111,7 +93,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
+ ->setObject('addressbook', (int)$addressbookData['id'])
->setType('contacts')
->setAuthor($currentUser);
@@ -139,7 +121,7 @@ class Backend {
[
'actor' => $currentUser,
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -170,7 +152,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
+ ->setObject('addressbook', (int)$addressbookData['id'])
->setType('contacts')
->setAuthor($currentUser);
@@ -195,7 +177,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -224,7 +206,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -266,7 +248,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -293,7 +275,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -371,7 +353,7 @@ class Backend {
[
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $properties['id'],
+ 'id' => (int)$properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
@@ -415,7 +397,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
+ ->setObject('addressbook', (int)$addressbookData['id'])
->setType('contacts')
->setAuthor($currentUser);
@@ -427,7 +409,7 @@ class Backend {
$params = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -454,7 +436,7 @@ class Backend {
*/
protected function getCardNameAndId(array $cardData): array {
$vObject = Reader::read($cardData['carddata']);
- return ['id' => (string) $vObject->UID, 'name' => (string) ($vObject->FN ?? '')];
+ return ['id' => (string)$vObject->UID, 'name' => (string)($vObject->FN ?? '')];
}
/**
diff --git a/apps/dav/lib/CardDAV/Activity/Filter.php b/apps/dav/lib/CardDAV/Activity/Filter.php
index 84e933fec7c..8b221a29ff0 100644
--- a/apps/dav/lib/CardDAV/Activity/Filter.php
+++ b/apps/dav/lib/CardDAV/Activity/Filter.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -11,15 +12,10 @@ use OCP\IURLGenerator;
class Filter implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
@@ -38,8 +34,8 @@ class Filter implements IFilter {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
*/
public function getPriority(): int {
return 40;
diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php
index 189c6a8e9ed..cdb9769401f 100644
--- a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php
+++ b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php
@@ -8,6 +8,7 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CardDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -26,25 +27,15 @@ class Addressbook extends Base {
public const SUBJECT_UNSHARE_USER = 'addressbook_user_unshare';
public const SUBJECT_UNSHARE_GROUP = 'addressbook_group_unshare';
- /** @var IFactory */
- protected $languageFactory;
-
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
- public function __construct(IFactory $languageFactory,
+ public function __construct(
+ protected IFactory $languageFactory,
IURLGenerator $url,
- IManager $activityManager,
+ protected IManager $activityManager,
IUserManager $userManager,
IGroupManager $groupManager,
- IEventMerger $eventMerger) {
+ protected IEventMerger $eventMerger,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
}
/**
@@ -52,11 +43,11 @@ class Addressbook extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent {
if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$l = $this->languageFactory->get('dav', $language);
@@ -102,7 +93,7 @@ class Addressbook extends Base {
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') {
$subject = $l->t('{actor} unshared address book {addressbook} from group {group}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event, $l);
diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Base.php b/apps/dav/lib/CardDAV/Activity/Provider/Base.php
index 0a1b55efb08..ea7680aed60 100644
--- a/apps/dav/lib/CardDAV/Activity/Provider/Base.php
+++ b/apps/dav/lib/CardDAV/Activity/Provider/Base.php
@@ -18,27 +18,17 @@ use OCP\IURLGenerator;
use OCP\IUserManager;
abstract class Base implements IProvider {
- /** @var IUserManager */
- protected $userManager;
-
- /** @var string[] */
+ /** @var string[] */
protected $userDisplayNames = [];
- /** @var IGroupManager */
- protected $groupManager;
-
/** @var string[] */
protected $groupDisplayNames = [];
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IUserManager $userManager,
- IGroupManager $groupManager,
- IURLGenerator $urlGenerator) {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->url = $urlGenerator;
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IGroupManager $groupManager,
+ protected IURLGenerator $url,
+ ) {
}
protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
@@ -51,18 +41,18 @@ abstract class Base implements IProvider {
* @return array
*/
protected function generateAddressbookParameter(array $data, IL10N $l): array {
- if ($data['uri'] === CardDavBackend::PERSONAL_ADDRESSBOOK_URI &&
- $data['name'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME) {
+ if ($data['uri'] === CardDavBackend::PERSONAL_ADDRESSBOOK_URI
+ && $data['name'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME) {
return [
'type' => 'addressbook',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $l->t('Personal'),
];
}
return [
'type' => 'addressbook',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $data['name'],
];
}
diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Card.php b/apps/dav/lib/CardDAV/Activity/Provider/Card.php
index 7e8bf9b0d4d..acf23c00531 100644
--- a/apps/dav/lib/CardDAV/Activity/Provider/Card.php
+++ b/apps/dav/lib/CardDAV/Activity/Provider/Card.php
@@ -8,6 +8,7 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CardDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -23,30 +24,16 @@ class Card extends Base {
public const SUBJECT_UPDATE = 'card_update';
public const SUBJECT_DELETE = 'card_delete';
- /** @var IFactory */
- protected $languageFactory;
-
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
- /** @var IAppManager */
- protected $appManager;
-
- public function __construct(IFactory $languageFactory,
+ public function __construct(
+ protected IFactory $languageFactory,
IURLGenerator $url,
- IManager $activityManager,
+ protected IManager $activityManager,
IUserManager $userManager,
IGroupManager $groupManager,
- IEventMerger $eventMerger,
- IAppManager $appManager) {
+ protected IEventMerger $eventMerger,
+ protected IAppManager $appManager,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
- $this->appManager = $appManager;
}
/**
@@ -54,11 +41,11 @@ class Card extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent {
if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$l = $this->languageFactory->get('dav', $language);
@@ -82,7 +69,7 @@ class Card extends Base {
} elseif ($event->getSubject() === self::SUBJECT_UPDATE . '_self') {
$subject = $l->t('You updated contact {card} in address book {addressbook}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event, $l);
diff --git a/apps/dav/lib/CardDAV/Activity/Setting.php b/apps/dav/lib/CardDAV/Activity/Setting.php
index aa25898065f..cc68cf87c83 100644
--- a/apps/dav/lib/CardDAV/Activity/Setting.php
+++ b/apps/dav/lib/CardDAV/Activity/Setting.php
@@ -27,8 +27,8 @@ class Setting extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
*/
public function getPriority(): int {
return 50;
diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php
index 2ec645f04d2..4d30d507a7d 100644
--- a/apps/dav/lib/CardDAV/AddressBook.php
+++ b/apps/dav/lib/CardDAV/AddressBook.php
@@ -8,7 +8,6 @@
namespace OCA\DAV\CardDAV;
use OCA\DAV\DAV\Sharing\IShareable;
-use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
use OCP\DB\Exception;
use OCP\IL10N;
use OCP\Server;
@@ -38,8 +37,8 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov
parent::__construct($carddavBackend, $addressBookInfo);
- if ($this->addressBookInfo['{DAV:}displayname'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME &&
- $this->getName() === CardDavBackend::PERSONAL_ADDRESSBOOK_URI) {
+ if ($this->addressBookInfo['{DAV:}displayname'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME
+ && $this->getName() === CardDavBackend::PERSONAL_ADDRESSBOOK_URI) {
$this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Contacts');
}
}
@@ -234,9 +233,6 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov
}
public function getChanges($syncToken, $syncLevel, $limit = null) {
- if (!$syncToken && $limit) {
- throw new UnsupportedLimitOnInitialSyncException();
- }
return parent::getChanges($syncToken, $syncLevel, $limit);
}
@@ -250,7 +246,12 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov
}
try {
- return $this->carddavBackend->moveCard($sourceNode->getAddressbookId(), (int)$this->addressBookInfo['id'], $sourceNode->getUri(), $sourceNode->getOwner());
+ return $this->carddavBackend->moveCard(
+ $sourceNode->getAddressbookId(),
+ $sourceNode->getUri(),
+ $this->getResourceId(),
+ $targetName,
+ );
} catch (Exception $e) {
// Avoid injecting LoggerInterface everywhere
Server::get(LoggerInterface::class)->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php
index 55f7f3b7639..ae77498539b 100644
--- a/apps/dav/lib/CardDAV/AddressBookImpl.php
+++ b/apps/dav/lib/CardDAV/AddressBookImpl.php
@@ -7,27 +7,16 @@
*/
namespace OCA\DAV\CardDAV;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Constants;
-use OCP\IAddressBook;
+use OCP\IAddressBookEnabled;
use OCP\IURLGenerator;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Sabre\VObject\UUIDUtil;
-class AddressBookImpl implements IAddressBook {
-
- /** @var CardDavBackend */
- private $backend;
-
- /** @var array */
- private $addressBookInfo;
-
- /** @var AddressBook */
- private $addressBook;
-
- /** @var IURLGenerator */
- private $urlGenerator;
+class AddressBookImpl implements IAddressBookEnabled {
/**
* AddressBookImpl constructor.
@@ -38,14 +27,13 @@ class AddressBookImpl implements IAddressBook {
* @param IUrlGenerator $urlGenerator
*/
public function __construct(
- AddressBook $addressBook,
- array $addressBookInfo,
- CardDavBackend $backend,
- IURLGenerator $urlGenerator) {
- $this->addressBook = $addressBook;
- $this->addressBookInfo = $addressBookInfo;
- $this->backend = $backend;
- $this->urlGenerator = $urlGenerator;
+ private AddressBook $addressBook,
+ private array $addressBookInfo,
+ private CardDavBackend $backend,
+ private IURLGenerator $urlGenerator,
+ private PropertyMapper $propertyMapper,
+ private ?string $userId,
+ ) {
}
/**
@@ -78,19 +66,19 @@ class AddressBookImpl implements IAddressBook {
* @param string $pattern which should match within the $searchProperties
* @param array $searchProperties defines the properties within the query pattern should match
* @param array $options Options to define the output format and search behavior
- * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
- * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']]
- * - 'escape_like_param' - If set to false wildcards _ and % are not escaped
- * - 'limit' - Set a numeric limit for the search results
- * - 'offset' - Set the offset for the limited search results
- * - 'wildcard' - Whether the search should use wildcards
+ * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
+ * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']]
+ * - 'escape_like_param' - If set to false wildcards _ and % are not escaped
+ * - 'limit' - Set a numeric limit for the search results
+ * - 'offset' - Set the offset for the limited search results
+ * - 'wildcard' - Whether the search should use wildcards
* @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
* @return array an array of contacts which are arrays of key-value-pairs
- * example result:
- * [
- * ['id' => 0, 'FN' => 'Thomas Müller', 'EMAIL' => 'a@b.c', 'GEO' => '37.386013;-122.082932'],
- * ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['d@e.f', 'g@h.i']]
- * ]
+ * example result:
+ * [
+ * ['id' => 0, 'FN' => 'Thomas Müller', 'EMAIL' => 'a@b.c', 'GEO' => '37.386013;-122.082932'],
+ * ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['d@e.f', 'g@h.i']]
+ * ]
* @since 5.0.0
*/
public function search($pattern, $searchProperties, $options) {
@@ -131,13 +119,13 @@ class AddressBookImpl implements IAddressBook {
if (is_string($entry)) {
$property = $vCard->createProperty($key, $entry);
} else {
- if (($key === "ADR" || $key === "PHOTO") && is_string($entry["value"])) {
- $entry["value"] = stripslashes($entry["value"]);
- $entry["value"] = explode(';', $entry["value"]);
+ if (($key === 'ADR' || $key === 'PHOTO') && is_string($entry['value'])) {
+ $entry['value'] = stripslashes($entry['value']);
+ $entry['value'] = explode(';', $entry['value']);
}
- $property = $vCard->createProperty($key, $entry["value"]);
- if (isset($entry["type"])) {
- $property->add('TYPE', $entry["type"]);
+ $property = $vCard->createProperty($key, $entry['value']);
+ if (isset($entry['type'])) {
+ $property->add('TYPE', $entry['type']);
}
}
$vCard->add($property);
@@ -164,6 +152,10 @@ class AddressBookImpl implements IAddressBook {
$permissions = $this->addressBook->getACL();
$result = 0;
foreach ($permissions as $permission) {
+ if ($this->addressBookInfo['principaluri'] !== $permission['principal']) {
+ continue;
+ }
+
switch ($permission['privilege']) {
case '{DAV:}read':
$result |= Constants::PERMISSION_READ;
@@ -319,8 +311,29 @@ class AddressBookImpl implements IAddressBook {
*/
public function isSystemAddressBook(): bool {
return $this->addressBookInfo['principaluri'] === 'principals/system/system' && (
- $this->addressBookInfo['uri'] === 'system' ||
- $this->addressBookInfo['{DAV:}displayname'] === $this->urlGenerator->getBaseUrl()
+ $this->addressBookInfo['uri'] === 'system'
+ || $this->addressBookInfo['{DAV:}displayname'] === $this->urlGenerator->getBaseUrl()
);
}
+
+ public function isEnabled(): bool {
+ if (!$this->userId) {
+ return true;
+ }
+
+ if ($this->isSystemAddressBook()) {
+ $user = $this->userId ;
+ $uri = 'z-server-generated--system';
+ } else {
+ $user = str_replace('principals/users/', '', $this->addressBookInfo['principaluri']);
+ $uri = $this->addressBookInfo['uri'];
+ }
+
+ $path = 'addressbooks/users/' . $user . '/' . $uri;
+ $properties = $this->propertyMapper->findPropertyByPathAndName($user, $path, '{http://owncloud.org/ns}enabled');
+ if (count($properties) > 0) {
+ return (bool)$properties[0]->getPropertyvalue();
+ }
+ return true;
+ }
}
diff --git a/apps/dav/lib/CardDAV/AddressBookRoot.php b/apps/dav/lib/CardDAV/AddressBookRoot.php
index ec6fa6c9fe1..5679a03545e 100644
--- a/apps/dav/lib/CardDAV/AddressBookRoot.php
+++ b/apps/dav/lib/CardDAV/AddressBookRoot.php
@@ -13,26 +13,20 @@ use OCP\IUser;
class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot {
- /** @var PluginManager */
- private $pluginManager;
- private ?IUser $user;
- private ?IGroupManager $groupManager;
-
/**
* @param \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend
* @param \Sabre\CardDAV\Backend\BackendInterface $carddavBackend
* @param string $principalPrefix
*/
- public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend,
+ public function __construct(
+ \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend,
\Sabre\CardDAV\Backend\BackendInterface $carddavBackend,
- PluginManager $pluginManager,
- ?IUser $user,
- ?IGroupManager $groupManager,
- string $principalPrefix = 'principals') {
+ private PluginManager $pluginManager,
+ private ?IUser $user,
+ private ?IGroupManager $groupManager,
+ string $principalPrefix = 'principals',
+ ) {
parent::__construct($principalBackend, $carddavBackend, $principalPrefix);
- $this->pluginManager = $pluginManager;
- $this->user = $user;
- $this->groupManager = $groupManager;
}
/**
diff --git a/apps/dav/lib/CardDAV/Card.php b/apps/dav/lib/CardDAV/Card.php
index 67f605d2fb4..8cd4fd7e5ee 100644
--- a/apps/dav/lib/CardDAV/Card.php
+++ b/apps/dav/lib/CardDAV/Card.php
@@ -10,7 +10,7 @@ namespace OCA\DAV\CardDAV;
class Card extends \Sabre\CardDAV\Card {
public function getId(): int {
- return (int) $this->cardData['id'];
+ return (int)$this->cardData['id'];
}
public function getUri(): string {
diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php
index cdbbc228047..a78686eb61d 100644
--- a/apps/dav/lib/CardDAV/CardDavBackend.php
+++ b/apps/dav/lib/CardDAV/CardDavBackend.php
@@ -23,6 +23,7 @@ use OCP\AppFramework\Db\TTransactional;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUserManager;
use PDO;
@@ -59,6 +60,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
private IUserManager $userManager,
private IEventDispatcher $dispatcher,
private Sharing\Backend $sharingBackend,
+ private IConfig $config,
) {
}
@@ -76,7 +78,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
$result = $query->executeQuery();
- $column = (int) $result->fetchOne();
+ $column = (int)$result->fetchOne();
$result->closeCursor();
return $column;
}
@@ -127,7 +129,6 @@ class CardDavBackend implements BackendInterface, SyncSupport {
// query for shared addressbooks
$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
- $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
$principals[] = $principalUri;
@@ -160,8 +161,8 @@ class CardDavBackend implements BackendInterface, SyncSupport {
// New share can not have more permissions then the old one.
continue;
}
- if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
- $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
+ if (isset($addressBooks[$row['id']][$readOnlyPropertyName])
+ && $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
// Old share is already read-write, no more permissions can be gained
continue;
}
@@ -200,7 +201,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$addressBooks = [];
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$addressBooks[$row['id']] = [
'id' => $row['id'],
@@ -330,7 +331,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
->executeStatement();
- $this->addChange($addressBookId, "", 2);
+ $this->addChange($addressBookId, '', 2);
$addressBookRow = $this->getAddressBookById((int)$addressBookId);
$shares = $this->getShares((int)$addressBookId);
@@ -351,6 +352,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param array $properties
* @return int
* @throws BadRequest
+ * @throws Exception
*/
public function createAddressBook($principalUri, $url, array $properties) {
if (strlen($url) > 255) {
@@ -395,7 +397,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
'synctoken' => $query->createParameter('synctoken'),
])
->setParameters($values)
- ->execute();
+ ->executeStatement();
$addressBookId = $query->getLastInsertId();
return [
@@ -416,7 +418,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return void
*/
public function deleteAddressBook($addressBookId) {
- $this->atomic(function () use ($addressBookId) {
+ $this->atomic(function () use ($addressBookId): void {
$addressBookId = (int)$addressBookId;
$addressBookData = $this->getAddressBookById($addressBookId);
$shares = $this->getShares($addressBookId);
@@ -473,13 +475,13 @@ class CardDavBackend implements BackendInterface, SyncSupport {
*/
public function getCards($addressbookId) {
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
+ $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
->from($this->dbCardsTable)
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressbookId)));
$cards = [];
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$row['etag'] = '"' . $row['etag'] . '"';
@@ -510,13 +512,13 @@ class CardDavBackend implements BackendInterface, SyncSupport {
*/
public function getCard($addressBookId, $cardUri) {
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
+ $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
->from($this->dbCardsTable)
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->setMaxResults(1);
- $result = $query->execute();
+ $result = $query->executeQuery();
$row = $result->fetch();
if (!$row) {
return false;
@@ -553,14 +555,14 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$cards = [];
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
+ $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
->from($this->dbCardsTable)
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
foreach ($chunks as $uris) {
$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$row['etag'] = '"' . $row['etag'] . '"';
@@ -634,7 +636,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
'etag' => $query->createNamedParameter($etag),
'uid' => $query->createNamedParameter($uid),
])
- ->execute();
+ ->executeStatement();
$etagCacheKey = "$addressBookId#$cardUri";
$this->etagCache[$etagCacheKey] = $etag;
@@ -697,7 +699,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->set('uid', $query->createNamedParameter($uid))
->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
- ->execute();
+ ->executeStatement();
$this->etagCache[$etagCacheKey] = $etag;
@@ -715,32 +717,33 @@ class CardDavBackend implements BackendInterface, SyncSupport {
/**
* @throws Exception
*/
- public function moveCard(int $sourceAddressBookId, int $targetAddressBookId, string $cardUri, string $oldPrincipalUri): bool {
- return $this->atomic(function () use ($sourceAddressBookId, $targetAddressBookId, $cardUri, $oldPrincipalUri) {
- $card = $this->getCard($sourceAddressBookId, $cardUri);
+ public function moveCard(int $sourceAddressBookId, string $sourceObjectUri, int $targetAddressBookId, string $tragetObjectUri): bool {
+ return $this->atomic(function () use ($sourceAddressBookId, $sourceObjectUri, $targetAddressBookId, $tragetObjectUri) {
+ $card = $this->getCard($sourceAddressBookId, $sourceObjectUri);
if (empty($card)) {
return false;
}
+ $sourceObjectId = (int)$card['id'];
$query = $this->db->getQueryBuilder();
$query->update('cards')
->set('addressbookid', $query->createNamedParameter($targetAddressBookId, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
+ ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR))
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($sourceObjectUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($sourceAddressBookId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
->executeStatement();
- $this->purgeProperties($sourceAddressBookId, (int)$card['id']);
- $this->updateProperties($sourceAddressBookId, $card['uri'], $card['carddata']);
+ $this->purgeProperties($sourceAddressBookId, $sourceObjectId);
+ $this->updateProperties($targetAddressBookId, $tragetObjectUri, $card['carddata']);
- $this->addChange($sourceAddressBookId, $card['uri'], 3);
- $this->addChange($targetAddressBookId, $card['uri'], 1);
+ $this->addChange($sourceAddressBookId, $sourceObjectUri, 3);
+ $this->addChange($targetAddressBookId, $tragetObjectUri, 1);
- $card = $this->getCard($targetAddressBookId, $cardUri);
+ $card = $this->getCard($targetAddressBookId, $tragetObjectUri);
// Card wasn't found - possibly because it was deleted in the meantime by a different client
if (empty($card)) {
return false;
}
-
$targetAddressBookRow = $this->getAddressBookById($targetAddressBookId);
// the address book this card is being moved to does not exist any longer
if (empty($targetAddressBookRow)) {
@@ -850,6 +853,8 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return array
*/
public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
+ $maxLimit = $this->config->getSystemValueInt('carddav_sync_request_truncation', 2500);
+ $limit = ($limit === null) ? $maxLimit : min($limit, $maxLimit);
// Current synctoken
return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) {
$qb = $this->db->getQueryBuilder();
@@ -872,10 +877,35 @@ class CardDavBackend implements BackendInterface, SyncSupport {
'modified' => [],
'deleted' => [],
];
-
- if ($syncToken) {
+ if (str_starts_with($syncToken, 'init_')) {
+ $syncValues = explode('_', $syncToken);
+ $lastID = $syncValues[1];
+ $initialSyncToken = $syncValues[2];
$qb = $this->db->getQueryBuilder();
- $qb->select('uri', 'operation')
+ $qb->select('id', 'uri')
+ ->from('cards')
+ ->where(
+ $qb->expr()->andX(
+ $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)),
+ $qb->expr()->gt('id', $qb->createNamedParameter($lastID)))
+ )->orderBy('id')
+ ->setMaxResults($limit);
+ $stmt = $qb->executeQuery();
+ $values = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ $stmt->closeCursor();
+ if (count($values) === 0) {
+ $result['syncToken'] = $initialSyncToken;
+ $result['result_truncated'] = false;
+ $result['added'] = [];
+ } else {
+ $lastID = $values[array_key_last($values)]['id'];
+ $result['added'] = array_column($values, 'uri');
+ $result['syncToken'] = count($result['added']) >= $limit ? "init_{$lastID}_$initialSyncToken" : $initialSyncToken ;
+ $result['result_truncated'] = count($result['added']) >= $limit;
+ }
+ } elseif ($syncToken) {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('uri', 'operation', 'synctoken')
->from('addressbookchanges')
->where(
$qb->expr()->andX(
@@ -885,22 +915,31 @@ class CardDavBackend implements BackendInterface, SyncSupport {
)
)->orderBy('synctoken');
- if (is_int($limit) && $limit > 0) {
+ if ($limit > 0) {
$qb->setMaxResults($limit);
}
// Fetching all changes
$stmt = $qb->executeQuery();
+ $rowCount = $stmt->rowCount();
$changes = [];
+ $highestSyncToken = 0;
// This loop ensures that any duplicates are overwritten, only the
// last change on a node is relevant.
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$changes[$row['uri']] = $row['operation'];
+ $highestSyncToken = $row['synctoken'];
}
+
$stmt->closeCursor();
+ // No changes found, use current token
+ if (empty($changes)) {
+ $result['syncToken'] = $currentToken;
+ }
+
foreach ($changes as $uri => $operation) {
switch ($operation) {
case 1:
@@ -914,16 +953,43 @@ class CardDavBackend implements BackendInterface, SyncSupport {
break;
}
}
+
+ /*
+ * The synctoken in oc_addressbooks is always the highest synctoken in oc_addressbookchanges for a given addressbook plus one (see addChange).
+ *
+ * For truncated results, it is expected that we return the highest token from the response, so the client can continue from the latest change.
+ *
+ * For non-truncated results, it is expected to return the currentToken. If we return the highest token, as with truncated results, the client will always think it is one change behind.
+ *
+ * Therefore, we differentiate between truncated and non-truncated results when returning the synctoken.
+ */
+ if ($rowCount === $limit && $highestSyncToken < $currentToken) {
+ $result['syncToken'] = $highestSyncToken;
+ $result['result_truncated'] = true;
+ }
} else {
$qb = $this->db->getQueryBuilder();
- $qb->select('uri')
+ $qb->select('id', 'uri')
->from('cards')
->where(
$qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
);
// No synctoken supplied, this is the initial sync.
+ $qb->setMaxResults($limit);
$stmt = $qb->executeQuery();
- $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ $values = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ if (empty($values)) {
+ $result['added'] = [];
+ return $result;
+ }
+ $lastID = $values[array_key_last($values)]['id'];
+ if (count($values) >= $limit) {
+ $result['syncToken'] = 'init_' . $lastID . '_' . $currentToken;
+ $result['result_truncated'] = true;
+ }
+
+ $result['added'] = array_column($values, 'uri');
+
$stmt->closeCursor();
}
return $result;
@@ -939,7 +1005,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return void
*/
protected function addChange(int $addressBookId, string $objectUri, int $operation): void {
- $this->atomic(function () use ($addressBookId, $objectUri, $operation) {
+ $this->atomic(function () use ($addressBookId, $objectUri, $operation): void {
$query = $this->db->getQueryBuilder();
$query->select('synctoken')
->from('addressbooks')
@@ -1014,7 +1080,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param list<string> $remove
*/
public function updateShares(IShareable $shareable, array $add, array $remove): void {
- $this->atomic(function () use ($shareable, $add, $remove) {
+ $this->atomic(function () use ($shareable, $add, $remove): void {
$addressBookId = $shareable->getResourceId();
$addressBookData = $this->getAddressBookById($addressBookId);
$oldShares = $this->getShares($addressBookId);
@@ -1032,11 +1098,11 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param string $pattern which should match within the $searchProperties
* @param array $searchProperties defines the properties within the query pattern should match
* @param array $options = array() to define the search behavior
- * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
- * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
- * - 'limit' - Set a numeric limit for the search results
- * - 'offset' - Set the offset for the limited search results
- * - 'wildcard' - Whether the search should use wildcards
+ * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
+ * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
+ * - 'limit' - Set a numeric limit for the search results
+ * - 'offset' - Set the offset for the limited search results
+ * - 'wildcard' - Whether the search should use wildcards
* @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
* @return array an array of contacts which are arrays of key-value-pairs
*/
@@ -1061,7 +1127,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
array $options = []): array {
return $this->atomic(function () use ($principalUri, $pattern, $searchProperties, $options) {
$addressBookIds = array_map(static function ($row):int {
- return (int) $row['id'];
+ return (int)$row['id'];
}, $this->getAddressBooksForUser($principalUri));
return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
@@ -1148,24 +1214,24 @@ class CardDavBackend implements BackendInterface, SyncSupport {
/**
* FIXME Find a way to match only 4 last digits
* BDAY can be --1018 without year or 20001019 with it
- * $bDayOr = $query2->expr()->orX();
+ * $bDayOr = [];
* if ($options['since'] instanceof DateTimeFilter) {
- * $bDayOr->add(
+ * $bDayOr[] =
* $query2->expr()->gte('SUBSTR(cp_bday.value, -4)',
- * $query2->createNamedParameter($options['since']->get()->format('md')))
+ * $query2->createNamedParameter($options['since']->get()->format('md'))
* );
* }
* if ($options['until'] instanceof DateTimeFilter) {
- * $bDayOr->add(
+ * $bDayOr[] =
* $query2->expr()->lte('SUBSTR(cp_bday.value, -4)',
- * $query2->createNamedParameter($options['until']->get()->format('md')))
+ * $query2->createNamedParameter($options['until']->get()->format('md'))
* );
* }
- * $query2->andWhere($bDayOr);
+ * $query2->andWhere($query2->expr()->orX(...$bDayOr));
*/
}
- $result = $query2->execute();
+ $result = $query2->executeQuery();
$matches = $result->fetchAll();
$result->closeCursor();
$matches = array_map(function ($match) {
@@ -1186,7 +1252,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
return array_map(function ($array) {
- $array['addressbookid'] = (int) $array['addressbookid'];
+ $array['addressbookid'] = (int)$array['addressbookid'];
$modified = false;
$array['carddata'] = $this->readBlob($array['carddata'], $modified);
if ($modified) {
@@ -1207,7 +1273,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->from($this->dbCardsPropertiesTable)
->where($query->expr()->eq('name', $query->createNamedParameter($name)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
- ->execute();
+ ->executeQuery();
$all = $result->fetchAll(PDO::FETCH_COLUMN);
$result->closeCursor();
@@ -1227,7 +1293,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('id', $query->createParameter('id')))
->setParameter('id', $id);
- $result = $query->execute();
+ $result = $query->executeQuery();
$uri = $result->fetch();
$result->closeCursor();
@@ -1251,7 +1317,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->select('*')->from($this->dbCardsTable)
->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
- $queryResult = $query->execute();
+ $queryResult = $query->executeQuery();
$contact = $queryResult->fetch();
$queryResult->closeCursor();
@@ -1292,7 +1358,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param string $vCardSerialized
*/
protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
- $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized) {
+ $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized): void {
$cardId = $this->getCardId($addressBookId, $cardUri);
$vCard = $this->readCard($vCardSerialized);
@@ -1324,7 +1390,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->setParameter('name', $property->name);
$query->setParameter('value', mb_strcut($property->getValue(), 0, 254));
$query->setParameter('preferred', $preferred);
- $query->execute();
+ $query->executeStatement();
}
}, $this->db);
}
@@ -1350,7 +1416,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->delete($this->dbCardsPropertiesTable)
->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
- $query->execute();
+ $query->executeStatement();
}
/**
@@ -1362,7 +1428,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
- $result = $query->execute();
+ $result = $query->executeQuery();
$cardIds = $result->fetch();
$result->closeCursor();
@@ -1398,7 +1464,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->from('addressbookchanges');
$result = $query->executeQuery();
- $maxId = (int) $result->fetchOne();
+ $maxId = (int)$result->fetchOne();
$result->closeCursor();
if (!$maxId || $maxId < $keep) {
return 0;
diff --git a/apps/dav/lib/CardDAV/ContactsManager.php b/apps/dav/lib/CardDAV/ContactsManager.php
index 6126ebc10ee..b35137c902d 100644
--- a/apps/dav/lib/CardDAV/ContactsManager.php
+++ b/apps/dav/lib/CardDAV/ContactsManager.php
@@ -7,26 +7,23 @@
*/
namespace OCA\DAV\CardDAV;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Contacts\IManager;
use OCP\IL10N;
use OCP\IURLGenerator;
class ContactsManager {
- /** @var CardDavBackend */
- private $backend;
-
- /** @var IL10N */
- private $l10n;
-
/**
* ContactsManager constructor.
*
* @param CardDavBackend $backend
* @param IL10N $l10n
*/
- public function __construct(CardDavBackend $backend, IL10N $l10n) {
- $this->backend = $backend;
- $this->l10n = $l10n;
+ public function __construct(
+ private CardDavBackend $backend,
+ private IL10N $l10n,
+ private PropertyMapper $propertyMapper,
+ ) {
}
/**
@@ -36,33 +33,37 @@ class ContactsManager {
*/
public function setupContactsProvider(IManager $cm, $userId, IURLGenerator $urlGenerator) {
$addressBooks = $this->backend->getAddressBooksForUser("principals/users/$userId");
- $this->register($cm, $addressBooks, $urlGenerator);
- $this->setupSystemContactsProvider($cm, $urlGenerator);
+ $this->register($cm, $addressBooks, $urlGenerator, $userId);
+ $this->setupSystemContactsProvider($cm, $userId, $urlGenerator);
}
/**
* @param IManager $cm
+ * @param ?string $userId
* @param IURLGenerator $urlGenerator
*/
- public function setupSystemContactsProvider(IManager $cm, IURLGenerator $urlGenerator) {
- $addressBooks = $this->backend->getAddressBooksForUser("principals/system/system");
- $this->register($cm, $addressBooks, $urlGenerator);
+ public function setupSystemContactsProvider(IManager $cm, ?string $userId, IURLGenerator $urlGenerator) {
+ $addressBooks = $this->backend->getAddressBooksForUser('principals/system/system');
+ $this->register($cm, $addressBooks, $urlGenerator, $userId);
}
/**
* @param IManager $cm
* @param $addressBooks
* @param IURLGenerator $urlGenerator
+ * @param ?string $userId
*/
- private function register(IManager $cm, $addressBooks, $urlGenerator) {
+ private function register(IManager $cm, $addressBooks, $urlGenerator, ?string $userId) {
foreach ($addressBooks as $addressBookInfo) {
- $addressBook = new \OCA\DAV\CardDAV\AddressBook($this->backend, $addressBookInfo, $this->l10n);
+ $addressBook = new AddressBook($this->backend, $addressBookInfo, $this->l10n);
$cm->registerAddressBook(
new AddressBookImpl(
$addressBook,
$addressBookInfo,
$this->backend,
- $urlGenerator
+ $urlGenerator,
+ $this->propertyMapper,
+ $userId,
)
);
}
diff --git a/apps/dav/lib/CardDAV/Converter.php b/apps/dav/lib/CardDAV/Converter.php
index a0a25716ed8..30dba99839e 100644
--- a/apps/dav/lib/CardDAV/Converter.php
+++ b/apps/dav/lib/CardDAV/Converter.php
@@ -20,21 +20,12 @@ use Sabre\VObject\Property\Text;
use Sabre\VObject\Property\VCard\Date;
class Converter {
- /** @var IURLGenerator */
- private $urlGenerator;
- /** @var IAccountManager */
- private $accountManager;
- private IUserManager $userManager;
-
public function __construct(
- IAccountManager $accountManager,
- IUserManager $userManager,
- IURLGenerator $urlGenerator,
+ private IAccountManager $accountManager,
+ private IUserManager $userManager,
+ private IURLGenerator $urlGenerator,
private LoggerInterface $logger,
) {
- $this->accountManager = $accountManager;
- $this->userManager = $userManager;
- $this->urlGenerator = $urlGenerator;
}
public function createCardFromUser(IUser $user): ?VCard {
@@ -85,7 +76,7 @@ class Converter {
new Text(
$vCard,
'X-SOCIALPROFILE',
- $this->urlGenerator->linkToRouteAbsolute('core.ProfilePage.index', ['targetUserId' => $user->getUID()]),
+ $this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $user->getUID()]),
[
'TYPE' => 'NEXTCLOUD',
'X-NC-SCOPE' => IAccountManager::SCOPE_PUBLISHED
diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php
index f8c2b6ee827..74a8b032e42 100644
--- a/apps/dav/lib/CardDAV/ImageExportPlugin.php
+++ b/apps/dav/lib/CardDAV/ImageExportPlugin.php
@@ -7,6 +7,7 @@
*/
namespace OCA\DAV\CardDAV;
+use OCP\AppFramework\Http;
use OCP\Files\NotFoundException;
use Sabre\CardDAV\Card;
use Sabre\DAV\Server;
@@ -18,16 +19,15 @@ class ImageExportPlugin extends ServerPlugin {
/** @var Server */
protected $server;
- /** @var PhotoCache */
- private $cache;
/**
* ImageExportPlugin constructor.
*
* @param PhotoCache $cache
*/
- public function __construct(PhotoCache $cache) {
- $this->cache = $cache;
+ public function __construct(
+ private PhotoCache $cache,
+ ) {
}
/**
@@ -60,7 +60,7 @@ class ImageExportPlugin extends ServerPlugin {
$path = $request->getPath();
$node = $this->server->tree->getNodeForPath($path);
- if (!($node instanceof Card)) {
+ if (!$node instanceof Card) {
return true;
}
@@ -87,11 +87,11 @@ class ImageExportPlugin extends ServerPlugin {
$response->setHeader('Content-Type', $file->getMimeType());
$fileName = $node->getName() . '.' . PhotoCache::ALLOWED_CONTENT_TYPES[$file->getMimeType()];
$response->setHeader('Content-Disposition', "attachment; filename=$fileName");
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($file->getContent());
} catch (NotFoundException $e) {
- $response->setStatus(404);
+ $response->setStatus(Http::STATUS_NO_CONTENT);
}
return false;
diff --git a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php
index 48e0b0d3036..372906a6ae8 100644
--- a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php
+++ b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php
@@ -32,12 +32,10 @@ abstract class ExternalAddressBook implements IAddressBook, DAV\IProperties {
*/
private const DELIMITER = '--';
- private string $appId;
- private string $uri;
-
- public function __construct(string $appId, string $uri) {
- $this->appId = $appId;
- $this->uri = $uri;
+ public function __construct(
+ private string $appId,
+ private string $uri,
+ ) {
}
/**
diff --git a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php
index 62ab16721b1..9d6b0df838e 100644
--- a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php
+++ b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php
@@ -8,6 +8,7 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CardDAV;
+use OCP\AppFramework\Http;
use Sabre\DAV;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
@@ -43,7 +44,7 @@ class MultiGetExportPlugin extends DAV\ServerPlugin {
}
// Only handling xml
- $contentType = (string) $response->getHeader('Content-Type');
+ $contentType = (string)$response->getHeader('Content-Type');
if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -65,7 +66,7 @@ class MultiGetExportPlugin extends DAV\ServerPlugin {
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
$response->setHeader('Content-Type', 'text/vcard');
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($output);
return true;
diff --git a/apps/dav/lib/CardDAV/PhotoCache.php b/apps/dav/lib/CardDAV/PhotoCache.php
index 3ea99c29a51..03c71f7e4a3 100644
--- a/apps/dav/lib/CardDAV/PhotoCache.php
+++ b/apps/dav/lib/CardDAV/PhotoCache.php
@@ -1,15 +1,19 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+
namespace OCA\DAV\CardDAV;
+use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\Image;
use Psr\Log\LoggerInterface;
use Sabre\CardDAV\Card;
use Sabre\VObject\Document;
@@ -18,24 +22,22 @@ use Sabre\VObject\Property\Binary;
use Sabre\VObject\Reader;
class PhotoCache {
+ private ?IAppData $photoCacheAppData = null;
- /** @var array */
+ /** @var array */
public const ALLOWED_CONTENT_TYPES = [
'image/png' => 'png',
'image/jpeg' => 'jpg',
'image/gif' => 'gif',
'image/vnd.microsoft.icon' => 'ico',
+ 'image/webp' => 'webp',
+ 'image/avif' => 'avif',
];
- protected IAppData $appData;
- protected LoggerInterface $logger;
-
- /**
- * PhotoCache constructor.
- */
- public function __construct(IAppData $appData, LoggerInterface $logger) {
- $this->appData = $appData;
- $this->logger = $logger;
+ public function __construct(
+ private IAppDataFactory $appDataFactory,
+ private LoggerInterface $logger,
+ ) {
}
/**
@@ -109,7 +111,7 @@ class PhotoCache {
throw new NotFoundException;
}
- $photo = new \OCP\Image();
+ $photo = new Image();
/** @var ISimpleFile $file */
$file = $folder->getFile('photo.' . $ext);
$photo->loadFromData($file->getContent());
@@ -119,7 +121,7 @@ class PhotoCache {
$ratio = 1 / $ratio;
}
- $size = (int) ($size * $ratio);
+ $size = (int)($size * $ratio);
if ($size !== -1) {
$photo->resize($size);
}
@@ -141,13 +143,12 @@ class PhotoCache {
private function getFolder(int $addressBookId, string $cardUri, bool $createIfNotExists = true): ISimpleFolder {
$hash = md5($addressBookId . ' ' . $cardUri);
try {
- return $this->appData->getFolder($hash);
+ return $this->getPhotoCacheAppData()->getFolder($hash);
} catch (NotFoundException $e) {
if ($createIfNotExists) {
- return $this->appData->newFolder($hash);
- } else {
- throw $e;
+ return $this->getPhotoCacheAppData()->newFolder($hash);
}
+ throw $e;
}
}
@@ -240,7 +241,7 @@ class PhotoCache {
if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) {
/** @var Parameter $typeParam */
$typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE'];
- $type = (string) $typeParam->getValue();
+ $type = (string)$typeParam->getValue();
if (str_starts_with($type, 'image/')) {
return $type;
@@ -264,4 +265,11 @@ class PhotoCache {
// that's OK, nothing to do
}
}
+
+ private function getPhotoCacheAppData(): IAppData {
+ if ($this->photoCacheAppData === null) {
+ $this->photoCacheAppData = $this->appDataFactory->get('dav-photocache');
+ }
+ return $this->photoCacheAppData;
+ }
}
diff --git a/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php
index 7f0f1e28623..3e18a1341b0 100644
--- a/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php
+++ b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php
@@ -23,20 +23,19 @@ use function count;
use function explode;
class CardDavRateLimitingPlugin extends ServerPlugin {
- private ?string $userId;
-
- public function __construct(private Limiter $limiter,
+ public function __construct(
+ private Limiter $limiter,
private IUserManager $userManager,
private CardDavBackend $cardDavBackend,
private LoggerInterface $logger,
private IAppConfig $config,
- ?string $userId) {
+ private ?string $userId,
+ ) {
$this->limiter = $limiter;
$this->userManager = $userManager;
$this->cardDavBackend = $cardDavBackend;
$this->config = $config;
$this->logger = $logger;
- $this->userId = $userId;
}
public function initialize(DAV\Server $server): void {
diff --git a/apps/dav/lib/CardDAV/Sharing/Backend.php b/apps/dav/lib/CardDAV/Sharing/Backend.php
index 5e03f7699fa..557115762fc 100644
--- a/apps/dav/lib/CardDAV/Sharing/Backend.php
+++ b/apps/dav/lib/CardDAV/Sharing/Backend.php
@@ -16,7 +16,8 @@ use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class Backend extends SharingBackend {
- public function __construct(private IUserManager $userManager,
+ public function __construct(
+ private IUserManager $userManager,
private IGroupManager $groupManager,
private Principal $principalBackend,
private ICacheFactory $cacheFactory,
diff --git a/apps/dav/lib/CardDAV/Sharing/Service.php b/apps/dav/lib/CardDAV/Sharing/Service.php
index 69bcecde227..1ab208f7ec3 100644
--- a/apps/dav/lib/CardDAV/Sharing/Service.php
+++ b/apps/dav/lib/CardDAV/Sharing/Service.php
@@ -13,7 +13,9 @@ use OCA\DAV\DAV\Sharing\SharingService;
class Service extends SharingService {
protected string $resourceType = 'addressbook';
- public function __construct(protected SharingMapper $mapper) {
+ public function __construct(
+ protected SharingMapper $mapper,
+ ) {
parent::__construct($mapper);
}
}
diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php
index e09cc36c8bb..e6da3ed5923 100644
--- a/apps/dav/lib/CardDAV/SyncService.php
+++ b/apps/dav/lib/CardDAV/SyncService.php
@@ -10,46 +10,44 @@ namespace OCA\DAV\CardDAV;
use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Http;
+use OCP\DB\Exception;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
+use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
-use Sabre\DAV\Client;
use Sabre\DAV\Xml\Response\MultiStatus;
use Sabre\DAV\Xml\Service;
-use Sabre\HTTP\ClientHttpException;
use Sabre\VObject\Reader;
+use Sabre\Xml\ParseException;
use function is_null;
class SyncService {
use TTransactional;
-
- private CardDavBackend $backend;
- private IUserManager $userManager;
- private IDBConnection $dbConnection;
- private LoggerInterface $logger;
private ?array $localSystemAddressBook = null;
- private Converter $converter;
protected string $certPath;
- public function __construct(CardDavBackend $backend,
- IUserManager $userManager,
- IDBConnection $dbConnection,
- LoggerInterface $logger,
- Converter $converter) {
- $this->backend = $backend;
- $this->userManager = $userManager;
- $this->logger = $logger;
- $this->converter = $converter;
+ public function __construct(
+ private CardDavBackend $backend,
+ private IUserManager $userManager,
+ private IDBConnection $dbConnection,
+ private LoggerInterface $logger,
+ private Converter $converter,
+ private IClientService $clientService,
+ private IConfig $config,
+ ) {
$this->certPath = '';
- $this->dbConnection = $dbConnection;
}
/**
+ * @psalm-return list{0: ?string, 1: boolean}
* @throws \Exception
*/
- public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string {
+ public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): array {
// 1. create addressbook
$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties);
$addressBookId = $book['id'];
@@ -57,7 +55,7 @@ class SyncService {
// 2. query changes
try {
$response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
- } catch (ClientHttpException $ex) {
+ } catch (ClientExceptionInterface $ex) {
if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
// remote server revoked access to the address book, remove it
$this->backend->deleteAddressBook($addressBookId);
@@ -74,12 +72,12 @@ class SyncService {
$cardUri = basename($resource);
if (isset($status[200])) {
$vCard = $this->download($url, $userName, $sharedSecret, $resource);
- $this->atomic(function () use ($addressBookId, $cardUri, $vCard) {
+ $this->atomic(function () use ($addressBookId, $cardUri, $vCard): void {
$existingCard = $this->backend->getCard($addressBookId, $cardUri);
if ($existingCard === false) {
- $this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
+ $this->backend->createCard($addressBookId, $cardUri, $vCard);
} else {
- $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
+ $this->backend->updateCard($addressBookId, $cardUri, $vCard);
}
}, $this->dbConnection);
} else {
@@ -87,82 +85,134 @@ class SyncService {
}
}
- return $response['token'];
+ return [
+ $response['token'],
+ $response['truncated'],
+ ];
}
/**
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array {
- return $this->atomic(function () use ($principal, $uri, $properties) {
- $book = $this->backend->getAddressBooksByUri($principal, $uri);
- if (!is_null($book)) {
- return $book;
- }
- $this->backend->createAddressBook($principal, $uri, $properties);
-
- return $this->backend->getAddressBooksByUri($principal, $uri);
- }, $this->dbConnection);
- }
+ try {
+ return $this->atomic(function () use ($principal, $uri, $properties) {
+ $book = $this->backend->getAddressBooksByUri($principal, $uri);
+ if (!is_null($book)) {
+ return $book;
+ }
+ $this->backend->createAddressBook($principal, $uri, $properties);
- /**
- * Check if there is a valid certPath we should use
- */
- protected function getCertPath(): string {
+ return $this->backend->getAddressBooksByUri($principal, $uri);
+ }, $this->dbConnection);
+ } catch (Exception $e) {
+ // READ COMMITTED doesn't prevent a nonrepeatable read above, so
+ // two processes might create an address book here. Ignore our
+ // failure and continue loading the entry written by the other process
+ if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ throw $e;
+ }
- // we already have a valid certPath
- if ($this->certPath !== '') {
- return $this->certPath;
+ // If this fails we might have hit a replication node that does not
+ // have the row written in the other process.
+ // TODO: find an elegant way to handle this
+ $ab = $this->backend->getAddressBooksByUri($principal, $uri);
+ if ($ab === null) {
+ throw new Exception('Could not create system address book', $e->getCode(), $e);
+ }
+ return $ab;
}
+ }
- $certManager = \OC::$server->getCertificateManager();
- $certPath = $certManager->getAbsoluteBundlePath();
- if (file_exists($certPath)) {
- $this->certPath = $certPath;
- }
+ public function ensureLocalSystemAddressBookExists(): ?array {
+ return $this->ensureSystemAddressBookExists('principals/system/system', 'system', [
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
+ ]);
+ }
- return $this->certPath;
+ private function prepareUri(string $host, string $path): string {
+ /*
+ * The trailing slash is important for merging the uris.
+ *
+ * $host is stored in oc_trusted_servers.url and usually without a trailing slash.
+ *
+ * Example for a report request
+ *
+ * $host = 'https://server.internal/cloud'
+ * $path = 'remote.php/dav/addressbooks/system/system/system'
+ *
+ * Without the trailing slash, the webroot is missing:
+ * https://server.internal/remote.php/dav/addressbooks/system/system/system
+ *
+ * Example for a download request
+ *
+ * $host = 'https://server.internal/cloud'
+ * $path = '/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf'
+ *
+ * The response from the remote usually contains the webroot already and must be normalized to:
+ * https://server.internal/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf
+ */
+ $host = rtrim($host, '/') . '/';
+
+ $uri = \GuzzleHttp\Psr7\UriResolver::resolve(
+ \GuzzleHttp\Psr7\Utils::uriFor($host),
+ \GuzzleHttp\Psr7\Utils::uriFor($path)
+ );
+
+ return (string)$uri;
}
- protected function getClient(string $url, string $userName, string $sharedSecret): Client {
- $settings = [
- 'baseUri' => $url . '/',
- 'userName' => $userName,
- 'password' => $sharedSecret,
+ /**
+ * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
+ * @throws ClientExceptionInterface
+ * @throws ParseException
+ */
+ protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
+ $client = $this->clientService->newClient();
+ $uri = $this->prepareUri($url, $addressBookUrl);
+
+ $options = [
+ 'auth' => [$userName, $sharedSecret],
+ 'body' => $this->buildSyncCollectionRequestBody($syncToken),
+ 'headers' => ['Content-Type' => 'application/xml'],
+ 'timeout' => $this->config->getSystemValueInt('carddav_sync_request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT),
+ 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
];
- $client = new Client($settings);
- $certPath = $this->getCertPath();
- $client->setThrowExceptions(true);
- if ($certPath !== '' && !str_starts_with($url, 'http://')) {
- $client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
- }
+ $response = $client->request(
+ 'REPORT',
+ $uri,
+ $options
+ );
- return $client;
- }
+ $body = $response->getBody();
+ assert(is_string($body));
- protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
- $client = $this->getClient($url, $userName, $sharedSecret);
+ return $this->parseMultiStatus($body, $addressBookUrl);
+ }
- $body = $this->buildSyncCollectionRequestBody($syncToken);
+ protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string {
+ $client = $this->clientService->newClient();
+ $uri = $this->prepareUri($url, $resourcePath);
- $response = $client->request('REPORT', $addressBookUrl, $body, [
- 'Content-Type' => 'application/xml'
- ]);
+ $options = [
+ 'auth' => [$userName, $sharedSecret],
+ 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
+ ];
- return $this->parseMultiStatus($response['body']);
- }
+ $response = $client->get(
+ $uri,
+ $options
+ );
- protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): array {
- $client = $this->getClient($url, $userName, $sharedSecret);
- return $client->request('GET', $resourcePath);
+ return (string)$response->getBody();
}
private function buildSyncCollectionRequestBody(?string $syncToken): string {
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$root = $dom->createElementNS('DAV:', 'd:sync-collection');
- $sync = $dom->createElement('d:sync-token', $syncToken);
+ $sync = $dom->createElement('d:sync-token', $syncToken ?? '');
$prop = $dom->createElement('d:prop');
$cont = $dom->createElement('d:getcontenttype');
$etag = $dom->createElement('d:getetag');
@@ -176,22 +226,50 @@ class SyncService {
}
/**
- * @param string $body
- * @return array
- * @throws \Sabre\Xml\ParseException
+ * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
+ * @throws ParseException
*/
- private function parseMultiStatus($body) {
- $xml = new Service();
-
+ private function parseMultiStatus(string $body, string $addressBookUrl): array {
/** @var MultiStatus $multiStatus */
- $multiStatus = $xml->expect('{DAV:}multistatus', $body);
+ $multiStatus = (new Service())->expect('{DAV:}multistatus', $body);
$result = [];
+ $truncated = false;
+
foreach ($multiStatus->getResponses() as $response) {
- $result[$response->getHref()] = $response->getResponseProperties();
+ $href = $response->getHref();
+ if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $addressBookUrl)) {
+ $truncated = true;
+ } else {
+ $result[$response->getHref()] = $response->getResponseProperties();
+ }
}
- return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
+ return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated];
+ }
+
+ /**
+ * Determines whether the provided response URI corresponds to the given request URI.
+ */
+ private function isResponseForRequestUri(string $responseUri, string $requestUri): bool {
+ /*
+ * Example response uri:
+ *
+ * /remote.php/dav/addressbooks/system/system/system/
+ * /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory)
+ *
+ * Example request uri:
+ *
+ * remote.php/dav/addressbooks/system/system/system
+ *
+ * References:
+ * https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174
+ * https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41
+ */
+ return str_ends_with(
+ rtrim($responseUri, '/'),
+ rtrim($requestUri, '/')
+ );
}
/**
@@ -203,7 +281,7 @@ class SyncService {
$cardId = self::getCardUri($user);
if ($user->isEnabled()) {
- $this->atomic(function () use ($addressBookId, $cardId, $user) {
+ $this->atomic(function () use ($addressBookId, $cardId, $user): void {
$card = $this->backend->getCard($addressBookId, $cardId);
if ($card === false) {
$vCard = $this->converter->createCardFromUser($user);
@@ -240,10 +318,7 @@ class SyncService {
*/
public function getLocalSystemAddressBook() {
if (is_null($this->localSystemAddressBook)) {
- $systemPrincipal = "principals/system/system";
- $this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [
- '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
- ]);
+ $this->localSystemAddressBook = $this->ensureLocalSystemAddressBookExists();
}
return $this->localSystemAddressBook;
@@ -254,7 +329,7 @@ class SyncService {
*/
public function syncInstance(?\Closure $progressCallback = null) {
$systemAddressBook = $this->getLocalSystemAddressBook();
- $this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback) {
+ $this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback): void {
$this->updateUser($user);
if (!is_null($progressCallback)) {
$progressCallback();
diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php
index 4ba06d0fc9c..912a2f1dcee 100644
--- a/apps/dav/lib/CardDAV/SystemAddressbook.php
+++ b/apps/dav/lib/CardDAV/SystemAddressbook.php
@@ -8,7 +8,6 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CardDAV;
-use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
use OCA\Federation\TrustedServers;
use OCP\Accounts\IAccountManager;
use OCP\IConfig;
@@ -30,27 +29,18 @@ use function in_array;
class SystemAddressbook extends AddressBook {
public const URI_SHARED = 'z-server-generated--system';
- /** @var IConfig */
- private $config;
- private IUserSession $userSession;
- private ?TrustedServers $trustedServers;
- private ?IRequest $request;
- private ?IGroupManager $groupManager;
- public function __construct(BackendInterface $carddavBackend,
+ public function __construct(
+ BackendInterface $carddavBackend,
array $addressBookInfo,
IL10N $l10n,
- IConfig $config,
- IUserSession $userSession,
- ?IRequest $request = null,
- ?TrustedServers $trustedServers = null,
- ?IGroupManager $groupManager = null) {
+ private IConfig $config,
+ private IUserSession $userSession,
+ private ?IRequest $request = null,
+ private ?TrustedServers $trustedServers = null,
+ private ?IGroupManager $groupManager = null,
+ ) {
parent::__construct($carddavBackend, $addressBookInfo, $l10n);
- $this->config = $config;
- $this->userSession = $userSession;
- $this->request = $request;
- $this->trustedServers = $trustedServers;
- $this->groupManager = $groupManager;
$this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Accounts');
$this->addressBookInfo['{' . Plugin::NS_CARDDAV . '}addressbook-description'] = $l10n->t('System address book which holds all accounts');
@@ -221,14 +211,7 @@ class SystemAddressbook extends AddressBook {
}
return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
}
-
- /**
- * @throws UnsupportedLimitOnInitialSyncException
- */
public function getChanges($syncToken, $syncLevel, $limit = null) {
- if (!$syncToken && $limit) {
- throw new UnsupportedLimitOnInitialSyncException();
- }
if (!$this->carddavBackend instanceof SyncSupport) {
return null;
@@ -254,7 +237,7 @@ class SystemAddressbook extends AddressBook {
try {
$this->getChild($uri);
$added[] = $uri;
- } catch (NotFound | Forbidden $e) {
+ } catch (NotFound|Forbidden $e) {
$deleted[] = $uri;
}
}
@@ -262,7 +245,7 @@ class SystemAddressbook extends AddressBook {
try {
$this->getChild($uri);
$modified[] = $uri;
- } catch (NotFound | Forbidden $e) {
+ } catch (NotFound|Forbidden $e) {
$deleted[] = $uri;
}
}
diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php
index d0719e02492..e29e52e77df 100644
--- a/apps/dav/lib/CardDAV/UserAddressBooks.php
+++ b/apps/dav/lib/CardDAV/UserAddressBooks.php
@@ -20,6 +20,7 @@ use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
+use OCP\Server;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Sabre\CardDAV\Backend;
@@ -35,20 +36,14 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
/** @var IConfig */
protected $config;
- /** @var PluginManager */
- private $pluginManager;
- private ?IUser $user;
- private ?IGroupManager $groupManager;
-
- public function __construct(Backend\BackendInterface $carddavBackend,
+ public function __construct(
+ Backend\BackendInterface $carddavBackend,
string $principalUri,
- PluginManager $pluginManager,
- ?IUser $user,
- ?IGroupManager $groupManager) {
+ private PluginManager $pluginManager,
+ private ?IUser $user,
+ private ?IGroupManager $groupManager,
+ ) {
parent::__construct($carddavBackend, $principalUri);
- $this->pluginManager = $pluginManager;
- $this->user = $user;
- $this->groupManager = $groupManager;
}
/**
@@ -61,7 +56,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
$this->l10n = \OC::$server->getL10N('dav');
}
if ($this->config === null) {
- $this->config = \OC::$server->getConfig();
+ $this->config = Server::get(IConfig::class);
}
/** @var string|array $principal */
@@ -87,9 +82,9 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
$trustedServers = null;
$request = null;
try {
- $trustedServers = \OC::$server->get(TrustedServers::class);
- $request = \OC::$server->get(IRequest::class);
- } catch (QueryException | NotFoundExceptionInterface | ContainerExceptionInterface $e) {
+ $trustedServers = Server::get(TrustedServers::class);
+ $request = Server::get(IRequest::class);
+ } catch (QueryException|NotFoundExceptionInterface|ContainerExceptionInterface $e) {
// nothing to do, the request / trusted servers don't exist
}
if ($addressBook['principaluri'] === 'principals/system/system') {
@@ -98,7 +93,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
$addressBook,
$this->l10n,
$this->config,
- \OCP\Server::get(IUserSession::class),
+ Server::get(IUserSession::class),
$request,
$trustedServers,
$this->groupManager
diff --git a/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php b/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php
new file mode 100644
index 00000000000..a5fd80ec124
--- /dev/null
+++ b/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CardDAV\Validation;
+
+use OCA\DAV\AppInfo\Application;
+use OCP\IAppConfig;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class CardDavValidatePlugin extends ServerPlugin {
+
+ public function __construct(
+ private IAppConfig $config,
+ ) {
+ }
+
+ public function initialize(Server $server): void {
+ $server->on('beforeMethod:PUT', [$this, 'beforePut']);
+ }
+
+ public function beforePut(RequestInterface $request, ResponseInterface $response): bool {
+ // evaluate if card size exceeds defined limit
+ $cardSizeLimit = $this->config->getValueInt(Application::APP_ID, 'card_size_limit', 5242880);
+ if ((int)$request->getRawServerValue('CONTENT_LENGTH') > $cardSizeLimit) {
+ throw new Forbidden("VCard object exceeds $cardSizeLimit bytes");
+ }
+ // all tests passed return true
+ return true;
+ }
+
+}
diff --git a/apps/dav/lib/CardDAV/Xml/Groups.php b/apps/dav/lib/CardDAV/Xml/Groups.php
index 6100bac1aea..07aeecb3fa2 100644
--- a/apps/dav/lib/CardDAV/Xml/Groups.php
+++ b/apps/dav/lib/CardDAV/Xml/Groups.php
@@ -13,14 +13,12 @@ use Sabre\Xml\XmlSerializable;
class Groups implements XmlSerializable {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
- /** @var string[] of TYPE:CHECKSUM */
- private $groups;
-
/**
- * @param string $groups
+ * @param list<string> $groups
*/
- public function __construct($groups) {
- $this->groups = $groups;
+ public function __construct(
+ private array $groups,
+ ) {
}
public function xmlSerialize(Writer $writer) {