diff options
Diffstat (limited to 'apps/dav/lib/CardDAV')
28 files changed, 725 insertions, 980 deletions
diff --git a/apps/dav/lib/CardDAV/Activity/Backend.php b/apps/dav/lib/CardDAV/Activity/Backend.php index f0a5ee05e82..b08414d3b02 100644 --- a/apps/dav/lib/CardDAV/Activity/Backend.php +++ b/apps/dav/lib/CardDAV/Activity/Backend.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity; @@ -39,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, + ) { } /** @@ -128,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); @@ -156,7 +121,7 @@ class Backend { [ 'actor' => $currentUser, 'addressbook' => [ - 'id' => (int) $addressbookData['id'], + 'id' => (int)$addressbookData['id'], 'uri' => $addressbookData['uri'], 'name' => $addressbookData['{DAV:}displayname'], ], @@ -187,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); @@ -212,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'], ], @@ -241,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'], ], @@ -283,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'], ], @@ -310,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'], ], @@ -388,7 +353,7 @@ class Backend { [ 'actor' => $event->getAuthor(), 'addressbook' => [ - 'id' => (int) $properties['id'], + 'id' => (int)$properties['id'], 'uri' => $properties['uri'], 'name' => $properties['{DAV:}displayname'], ], @@ -432,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); @@ -444,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'], ], @@ -471,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 3ca4c3367d5..8b221a29ff0 100644 --- a/apps/dav/lib/CardDAV/Activity/Filter.php +++ b/apps/dav/lib/CardDAV/Activity/Filter.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity; @@ -28,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, + ) { } /** @@ -55,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 a404dde4448..cdb9769401f 100644 --- a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php +++ b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php @@ -3,28 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity\Provider; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IEventMerger; use OCP\Activity\IManager; @@ -43,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; } /** @@ -69,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); @@ -119,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 f475f9d76b7..ea7680aed60 100644 --- a/apps/dav/lib/CardDAV/Activity/Provider/Base.php +++ b/apps/dav/lib/CardDAV/Activity/Provider/Base.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity\Provider; @@ -35,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 { @@ -68,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 7f49a428cae..acf23c00531 100644 --- a/apps/dav/lib/CardDAV/Activity/Provider/Card.php +++ b/apps/dav/lib/CardDAV/Activity/Provider/Card.php @@ -3,28 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity\Provider; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IEventMerger; use OCP\Activity\IManager; @@ -40,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; } /** @@ -71,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); @@ -99,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 a8a83111dde..cc68cf87c83 100644 --- a/apps/dav/lib/CardDAV/Activity/Setting.php +++ b/apps/dav/lib/CardDAV/Activity/Setting.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity; @@ -44,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 4f589031f06..4d30d507a7d 100644 --- a/apps/dav/lib/CardDAV/AddressBook.php +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -1,33 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ 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; @@ -57,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'); } } @@ -253,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); } @@ -269,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 79720429479..ae77498539b 100644 --- a/apps/dav/lib/CardDAV/AddressBookImpl.php +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -1,58 +1,22 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arne Hamann <kontakt+github@arne.email> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Björn Schießle <bjoern@schiessle.org> - * @author call-me-matt <nextcloud@matthiasheinisch.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\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. @@ -63,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, + ) { } /** @@ -103,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) { @@ -156,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); @@ -189,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; @@ -344,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 f450dbe2ef9..5679a03545e 100644 --- a/apps/dav/lib/CardDAV/AddressBookRoot.php +++ b/apps/dav/lib/CardDAV/AddressBookRoot.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Anna Larch <anna.larch@gmx.net> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV; @@ -30,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 093255392e0..8cd4fd7e5ee 100644 --- a/apps/dav/lib/CardDAV/Card.php +++ b/apps/dav/lib/CardDAV/Card.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023, Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ 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 f887e3b32b7..a78686eb61d 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -1,37 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arne Hamann <kontakt+github@arne.email> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Chih-Hsuan Yen <yan12125@gmail.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author matt <34400929+call-me-matt@users.noreply.github.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Stefan Weil <sw@weilnetz.de> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV; @@ -51,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; @@ -87,6 +60,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { private IUserManager $userManager, private IEventDispatcher $dispatcher, private Sharing\Backend $sharingBackend, + private IConfig $config, ) { } @@ -104,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; } @@ -155,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; @@ -188,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; } @@ -228,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'], @@ -358,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); @@ -379,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) { @@ -423,7 +397,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { 'synctoken' => $query->createParameter('synctoken'), ]) ->setParameters($values) - ->execute(); + ->executeStatement(); $addressBookId = $query->getLastInsertId(); return [ @@ -444,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); @@ -501,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'] . '"'; @@ -538,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; @@ -581,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'] . '"'; @@ -662,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; @@ -725,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; @@ -743,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)) { @@ -878,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(); @@ -900,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('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') + $qb->select('uri', 'operation', 'synctoken') ->from('addressbookchanges') ->where( $qb->expr()->andX( @@ -913,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: @@ -942,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; @@ -967,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') @@ -1042,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); @@ -1060,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 */ @@ -1089,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); @@ -1176,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) { @@ -1214,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) { @@ -1235,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(); @@ -1255,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(); @@ -1279,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(); @@ -1320,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); @@ -1352,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); } @@ -1378,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(); } /** @@ -1390,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(); @@ -1426,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 bed1e676337..b35137c902d 100644 --- a/apps/dav/lib/CardDAV/ContactsManager.php +++ b/apps/dav/lib/CardDAV/ContactsManager.php @@ -1,50 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tobia De Koninck <tobia@ledfan.be> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\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, + ) { } /** @@ -54,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 8ea75fbef74..30dba99839e 100644 --- a/apps/dav/lib/CardDAV/Converter.php +++ b/apps/dav/lib/CardDAV/Converter.php @@ -1,53 +1,31 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV; +use DateTimeImmutable; use Exception; use OCP\Accounts\IAccountManager; use OCP\IImage; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; +use Psr\Log\LoggerInterface; use Sabre\VObject\Component\VCard; 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) { - $this->accountManager = $accountManager; - $this->userManager = $userManager; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IAccountManager $accountManager, + private IUserManager $userManager, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + ) { } public function createCardFromUser(IUser $user): ?VCard { @@ -98,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 @@ -134,6 +112,24 @@ class Converter { case IAccountManager::PROPERTY_ROLE: $vCard->add(new Text($vCard, 'TITLE', $property->getValue(), ['X-NC-SCOPE' => $scope])); break; + case IAccountManager::PROPERTY_BIOGRAPHY: + $vCard->add(new Text($vCard, 'NOTE', $property->getValue(), ['X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_BIRTHDATE: + try { + $birthdate = new DateTimeImmutable($property->getValue()); + } catch (Exception $e) { + // Invalid date -> just skip the property + $this->logger->info("Failed to parse user's birthdate for the SAB: " . $property->getValue(), [ + 'exception' => $e, + 'userId' => $user->getUID(), + ]); + break; + } + $dateProperty = new Date($vCard, 'BDAY', null, ['X-NC-SCOPE' => $scope]); + $dateProperty->setDateTime($birthdate); + $vCard->add($dateProperty); + break; } } diff --git a/apps/dav/lib/CardDAV/HasPhotoPlugin.php b/apps/dav/lib/CardDAV/HasPhotoPlugin.php index 310649bdae9..6e2e0423910 100644 --- a/apps/dav/lib/CardDAV/HasPhotoPlugin.php +++ b/apps/dav/lib/CardDAV/HasPhotoPlugin.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV; diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php index 3ebc91e5533..74a8b032e42 100644 --- a/apps/dav/lib/CardDAV/ImageExportPlugin.php +++ b/apps/dav/lib/CardDAV/ImageExportPlugin.php @@ -1,29 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jacob Neplokh <me@jacobneplokh.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV; +use OCP\AppFramework\Http; use OCP\Files\NotFoundException; use Sabre\CardDAV\Card; use Sabre\DAV\Server; @@ -35,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, + ) { } /** @@ -77,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; } @@ -104,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 6f05bb6ce6a..372906a6ae8 100644 --- a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php +++ b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Integration; @@ -50,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/Integration/IAddressBookProvider.php b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php index 0560c13c05c..a8fa074f635 100644 --- a/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php +++ b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Integration; diff --git a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php index a56faad8413..9d6b0df838e 100644 --- a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php +++ b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php @@ -3,29 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV; +use OCP\AppFramework\Http; use Sabre\DAV; use Sabre\DAV\Server; use Sabre\HTTP\RequestInterface; @@ -61,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; } @@ -83,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 9f05ec2354a..03c71f7e4a3 100644 --- a/apps/dav/lib/CardDAV/PhotoCache.php +++ b/apps/dav/lib/CardDAV/PhotoCache.php @@ -1,39 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Jacob Neplokh <me@jacobneplokh.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\DAV\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; @@ -42,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, + ) { } /** @@ -133,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()); @@ -143,7 +121,7 @@ class PhotoCache { $ratio = 1 / $ratio; } - $size = (int) ($size * $ratio); + $size = (int)($size * $ratio); if ($size !== -1) { $photo->resize($size); } @@ -165,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; } } @@ -264,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; @@ -288,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/Plugin.php b/apps/dav/lib/CardDAV/Plugin.php index df8f7e6a436..0ec10306ceb 100644 --- a/apps/dav/lib/CardDAV/Plugin.php +++ b/apps/dav/lib/CardDAV/Plugin.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV; diff --git a/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php new file mode 100644 index 00000000000..3e18a1341b0 --- /dev/null +++ b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php @@ -0,0 +1,86 @@ +<?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\Security; + +use OC\Security\RateLimiting\Exception\RateLimitExceededException; +use OC\Security\RateLimiting\Limiter; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Connector\Sabre\Exception\TooManyRequests; +use OCP\IAppConfig; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; +use Sabre\DAV; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\ServerPlugin; +use function count; +use function explode; + +class CardDavRateLimitingPlugin extends ServerPlugin { + public function __construct( + private Limiter $limiter, + private IUserManager $userManager, + private CardDavBackend $cardDavBackend, + private LoggerInterface $logger, + private IAppConfig $config, + private ?string $userId, + ) { + $this->limiter = $limiter; + $this->userManager = $userManager; + $this->cardDavBackend = $cardDavBackend; + $this->config = $config; + $this->logger = $logger; + } + + public function initialize(DAV\Server $server): void { + $server->on('beforeBind', [$this, 'beforeBind'], 1); + } + + public function beforeBind(string $path): void { + if ($this->userId === null) { + // We only care about authenticated users here + return; + } + $user = $this->userManager->get($this->userId); + if ($user === null) { + // We only care about authenticated users here + return; + } + + $pathParts = explode('/', $path); + if (count($pathParts) === 4 && $pathParts[0] === 'addressbooks') { + // Path looks like addressbooks/users/username/addressbooksname so a new addressbook is created + try { + $this->limiter->registerUserRequest( + 'carddav-create-address-book', + $this->config->getValueInt('dav', 'rateLimitAddressBookCreation', 10), + $this->config->getValueInt('dav', 'rateLimitPeriodAddressBookCreation', 3600), + $user + ); + } catch (RateLimitExceededException $e) { + throw new TooManyRequests('Too many addressbooks created', 0, $e); + } + + $addressBookLimit = $this->config->getValueInt('dav', 'maximumAdressbooks', 10); + if ($addressBookLimit === -1) { + return; + } + $numAddressbooks = $this->cardDavBackend->getAddressBooksForUserCount('principals/users/' . $user->getUID()); + + if ($numAddressbooks >= $addressBookLimit) { + $this->logger->warning('Maximum number of address books reached', [ + 'addressbooks' => $numAddressbooks, + 'addressBookLimit' => $addressBookLimit, + ]); + throw new Forbidden('AddressBook limit reached', 0); + } + } + } + +} diff --git a/apps/dav/lib/CardDAV/Sharing/Backend.php b/apps/dav/lib/CardDAV/Sharing/Backend.php index f0f53ba9cfa..557115762fc 100644 --- a/apps/dav/lib/CardDAV/Sharing/Backend.php +++ b/apps/dav/lib/CardDAV/Sharing/Backend.php @@ -2,22 +2,8 @@ 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/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Sharing; @@ -30,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 5da71defb5e..1ab208f7ec3 100644 --- a/apps/dav/lib/CardDAV/Sharing/Service.php +++ b/apps/dav/lib/CardDAV/Sharing/Service.php @@ -2,22 +2,8 @@ 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/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Sharing; @@ -27,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 01747a9b105..e6da3ed5923 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -1,77 +1,53 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @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 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\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']; @@ -79,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); @@ -96,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 { @@ -109,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'); @@ -198,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, '/') + ); } /** @@ -225,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); @@ -262,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; @@ -276,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 dc5ee0e1f21..912a2f1dcee 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -3,32 +3,11 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCA\Federation\TrustedServers; use OCP\Accounts\IAccountManager; use OCP\IConfig; @@ -50,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'); @@ -241,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; @@ -274,7 +237,7 @@ class SystemAddressbook extends AddressBook { try { $this->getChild($uri); $added[] = $uri; - } catch (NotFound | Forbidden $e) { + } catch (NotFound|Forbidden $e) { $deleted[] = $uri; } } @@ -282,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 2d129410067..e29e52e77df 100644 --- a/apps/dav/lib/CardDAV/UserAddressBooks.php +++ b/apps/dav/lib/CardDAV/UserAddressBooks.php @@ -3,28 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Anna Larch <anna.larch@gmx.net> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV; @@ -39,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; @@ -54,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; } /** @@ -80,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 */ @@ -106,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') { @@ -117,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 bde36129382..07aeecb3fa2 100644 --- a/apps/dav/lib/CardDAV/Xml/Groups.php +++ b/apps/dav/lib/CardDAV/Xml/Groups.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV\Xml; @@ -29,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) { |