diff options
author | Christoph Wurst <ChristophWurst@users.noreply.github.com> | 2023-05-10 11:05:41 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-10 11:05:41 +0200 |
commit | 1fd8f41e331de1f27d0b77cba120bad25a01c877 (patch) | |
tree | 5d586762b576a4cd1912b7ef395a8df06cb3dd40 /apps/dav/lib | |
parent | 5993a4b3e3f70e8d768c36c8e16402c67e124eb4 (diff) | |
parent | bd80a1b2dd4ee46a21fc17dada0cf9c438f92a6d (diff) | |
download | nextcloud-server-1fd8f41e331de1f27d0b77cba120bad25a01c877.tar.gz nextcloud-server-1fd8f41e331de1f27d0b77cba120bad25a01c877.zip |
Merge pull request #38048 from nextcloud/enh/add-x-nc-scope-property
feat(dav): store scopes for properties and filter locally scoped properties for federated address book sync
Diffstat (limited to 'apps/dav/lib')
-rw-r--r-- | apps/dav/lib/CardDAV/Converter.php | 95 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/SystemAddressbook.php | 179 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/UserAddressBooks.php | 15 | ||||
-rw-r--r-- | apps/dav/lib/Migration/Version1027Date20230504122946.php | 54 |
4 files changed, 289 insertions, 54 deletions
diff --git a/apps/dav/lib/CardDAV/Converter.php b/apps/dav/lib/CardDAV/Converter.php index 409fce62105..e35bc41abd2 100644 --- a/apps/dav/lib/CardDAV/Converter.php +++ b/apps/dav/lib/CardDAV/Converter.php @@ -29,15 +29,12 @@ namespace OCA\DAV\CardDAV; use Exception; use OCP\Accounts\IAccountManager; -use OCP\Accounts\PropertyDoesNotExistException; use OCP\IImage; use OCP\IUser; use Sabre\VObject\Component\VCard; use Sabre\VObject\Property\Text; -use function array_merge; class Converter { - /** @var IAccountManager */ private $accountManager; @@ -46,13 +43,7 @@ class Converter { } public function createCardFromUser(IUser $user): ?VCard { - $account = $this->accountManager->getAccount($user); - $userProperties = $account->getProperties(); - try { - $additionalEmailsCollection = $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL); - $userProperties = array_merge($userProperties, $additionalEmailsCollection->getProperties()); - } catch (PropertyDoesNotExistException $e) { - } + $userProperties = $this->accountManager->getAccount($user)->getAllProperties(); $uid = $user->getUID(); $cloudId = $user->getCloudId(); @@ -65,47 +56,49 @@ class Converter { $publish = false; foreach ($userProperties as $property) { - $shareWithTrustedServers = - $property->getScope() === IAccountManager::SCOPE_FEDERATED || - $property->getScope() === IAccountManager::SCOPE_PUBLISHED; - - $emptyValue = $property->getValue() === ''; - - if ($shareWithTrustedServers && !$emptyValue) { - $publish = true; - switch ($property->getName()) { - case IAccountManager::PROPERTY_DISPLAYNAME: - $vCard->add(new Text($vCard, 'FN', $property->getValue())); - $vCard->add(new Text($vCard, 'N', $this->splitFullName($property->getValue()))); - break; - case IAccountManager::PROPERTY_AVATAR: - if ($image !== null) { - $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); - } - break; - case IAccountManager::COLLECTION_EMAIL: - case IAccountManager::PROPERTY_EMAIL: - $vCard->add(new Text($vCard, 'EMAIL', $property->getValue(), ['TYPE' => 'OTHER'])); - break; - case IAccountManager::PROPERTY_WEBSITE: - $vCard->add(new Text($vCard, 'URL', $property->getValue())); - break; - case IAccountManager::PROPERTY_PHONE: - $vCard->add(new Text($vCard, 'TEL', $property->getValue(), ['TYPE' => 'OTHER'])); - break; - case IAccountManager::PROPERTY_ADDRESS: - $vCard->add(new Text($vCard, 'ADR', $property->getValue(), ['TYPE' => 'OTHER'])); - break; - case IAccountManager::PROPERTY_TWITTER: - $vCard->add(new Text($vCard, 'X-SOCIALPROFILE', $property->getValue(), ['TYPE' => 'TWITTER'])); - break; - case IAccountManager::PROPERTY_ORGANISATION: - $vCard->add(new Text($vCard, 'ORG', $property->getValue())); - break; - case IAccountManager::PROPERTY_ROLE: - $vCard->add(new Text($vCard, 'TITLE', $property->getValue())); - break; - } + if (empty($property->getValue())) { + continue; + } + + $scope = $property->getScope(); + // Do not write private data to the system address book at all + if ($scope === IAccountManager::SCOPE_PRIVATE || empty($scope)) { + continue; + } + + $publish = true; + switch ($property->getName()) { + case IAccountManager::PROPERTY_DISPLAYNAME: + $vCard->add(new Text($vCard, 'FN', $property->getValue(), ['X-NC-SCOPE' => $scope])); + $vCard->add(new Text($vCard, 'N', $this->splitFullName($property->getValue()), ['X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_AVATAR: + if ($image !== null) { + $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType(), ['X-NC-SCOPE' => $scope]]); + } + break; + case IAccountManager::COLLECTION_EMAIL: + case IAccountManager::PROPERTY_EMAIL: + $vCard->add(new Text($vCard, 'EMAIL', $property->getValue(), ['TYPE' => 'OTHER', 'X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_WEBSITE: + $vCard->add(new Text($vCard, 'URL', $property->getValue(), ['X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_PHONE: + $vCard->add(new Text($vCard, 'TEL', $property->getValue(), ['TYPE' => 'OTHER', 'X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_ADDRESS: + $vCard->add(new Text($vCard, 'ADR', $property->getValue(), ['TYPE' => 'OTHER', 'X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_TWITTER: + $vCard->add(new Text($vCard, 'X-SOCIALPROFILE', $property->getValue(), ['TYPE' => 'TWITTER', 'X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_ORGANISATION: + $vCard->add(new Text($vCard, 'ORG', $property->getValue(), ['X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_ROLE: + $vCard->add(new Text($vCard, 'TITLE', $property->getValue(), ['X-NC-SCOPE' => $scope])); + break; } } diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php index 502e353acb3..a803a1e6b24 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -27,20 +27,34 @@ declare(strict_types=1); */ namespace OCA\DAV\CardDAV; +use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; +use OCA\Federation\TrustedServers; +use OCP\Accounts\IAccountManager; use OCP\IConfig; use OCP\IL10N; +use OCP\IRequest; +use Sabre\CardDAV\Backend\SyncSupport; use Sabre\CardDAV\Backend\BackendInterface; +use Sabre\CardDAV\Card; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Reader; class SystemAddressbook extends AddressBook { /** @var IConfig */ private $config; + private ?TrustedServers $trustedServers; + private ?IRequest $request; - public function __construct(BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n, IConfig $config) { + public function __construct(BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n, IConfig $config, ?IRequest $request = null, ?TrustedServers $trustedServers = null) { parent::__construct($carddavBackend, $addressBookInfo, $l10n); $this->config = $config; + $this->request = $request; + $this->trustedServers = $trustedServers; } - public function getChildren() { + public function getChildren(): array { $shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; @@ -50,4 +64,165 @@ class SystemAddressbook extends AddressBook { return parent::getChildren(); } + + /** + * @param array $paths + * @return Card[] + * @throws NotFound + */ + public function getMultipleChildren($paths): array { + if (!$this->isFederation()) { + return parent::getMultipleChildren($paths); + } + + $objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths); + $children = []; + /** @var array $obj */ + foreach ($objs as $obj) { + if (empty($obj)) { + continue; + } + $carddata = $this->extractCarddata($obj); + if (empty($carddata)) { + continue; + } else { + $obj['carddata'] = $carddata; + } + $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + return $children; + } + + /** + * @param string $name + * @return Card + * @throws NotFound + * @throws Forbidden + */ + public function getChild($name): Card { + if (!$this->isFederation()) { + return parent::getChild($name); + } + + $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name); + if (!$obj) { + throw new NotFound('Card not found'); + } + $carddata = $this->extractCarddata($obj); + if (empty($carddata)) { + throw new Forbidden(); + } else { + $obj['carddata'] = $carddata; + } + 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; + } + + if (!$this->isFederation()) { + return parent::getChanges($syncToken, $syncLevel, $limit); + } + + $changed = $this->carddavBackend->getChangesForAddressBook( + $this->addressBookInfo['id'], + $syncToken, + $syncLevel, + $limit + ); + + if (empty($changed)) { + return $changed; + } + + $added = $modified = $deleted = []; + foreach ($changed['added'] as $uri) { + try { + $this->getChild($uri); + $added[] = $uri; + } catch (NotFound | Forbidden $e) { + $deleted[] = $uri; + } + } + foreach ($changed['modified'] as $uri) { + try { + $this->getChild($uri); + $modified[] = $uri; + } catch (NotFound | Forbidden $e) { + $deleted[] = $uri; + } + } + $changed['added'] = $added; + $changed['modified'] = $modified; + $changed['deleted'] = $deleted; + return $changed; + } + + private function isFederation(): bool { + if ($this->trustedServers === null || $this->request === null) { + return false; + } + + /** @psalm-suppress NoInterfaceProperties */ + if ($this->request->server['PHP_AUTH_USER'] !== 'system') { + return false; + } + + /** @psalm-suppress NoInterfaceProperties */ + $sharedSecret = $this->request->server['PHP_AUTH_PW']; + if ($sharedSecret === null) { + return false; + } + + $servers = $this->trustedServers->getServers(); + $trusted = array_filter($servers, function ($trustedServer) use ($sharedSecret) { + return $trustedServer['shared_secret'] === $sharedSecret; + }); + // Authentication is fine, but it's not for a federated share + if (empty($trusted)) { + return false; + } + + return true; + } + + /** + * If the validation doesn't work the card is "not found" so we + * return empty carddata even if the carddata might exist in the local backend. + * This can happen when a user sets the required properties + * FN, N to a local scope only but the request is from + * a federated share. + * + * @see https://github.com/nextcloud/server/issues/38042 + * + * @param array $obj + * @return string|null + */ + private function extractCarddata(array $obj): ?string { + $obj['acl'] = $this->getChildACL(); + $cardData = $obj['carddata']; + /** @var VCard $vCard */ + $vCard = Reader::read($cardData); + foreach ($vCard->children() as $child) { + $scope = $child->offsetGet('X-NC-SCOPE'); + if ($scope !== null && $scope->getValue() === IAccountManager::SCOPE_LOCAL) { + $vCard->remove($child); + } + } + $messages = $vCard->validate(); + if (!empty($messages)) { + return null; + } + + return $vCard->serialize(); + } } diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php index 98957301120..85795604f28 100644 --- a/apps/dav/lib/CardDAV/UserAddressBooks.php +++ b/apps/dav/lib/CardDAV/UserAddressBooks.php @@ -30,8 +30,13 @@ namespace OCA\DAV\CardDAV; use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CardDAV\Integration\IAddressBookProvider; use OCA\DAV\CardDAV\Integration\ExternalAddressBook; +use OCA\Federation\TrustedServers; +use OCP\AppFramework\QueryException; use OCP\IConfig; use OCP\IL10N; +use OCP\IRequest; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use Sabre\CardDAV\Backend; use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\CardDAV\IAddressBook; @@ -73,7 +78,15 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { /** @var IAddressBook[] $objects */ $objects = array_map(function (array $addressBook) { if ($addressBook['principaluri'] === 'principals/system/system') { - return new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config); + $trustedServers = null; + $request = null; + try { + $trustedServers = \OC::$server->get(TrustedServers::class); + $request = \OC::$server->get(IRequest::class); + } catch (NotFoundExceptionInterface | ContainerExceptionInterface $e) { + // nothing to do, the request / trusted servers don't exist + } + return new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config, $request, $trustedServers); } return new AddressBook($this->carddavBackend, $addressBook, $this->l10n); diff --git a/apps/dav/lib/Migration/Version1027Date20230504122946.php b/apps/dav/lib/Migration/Version1027Date20230504122946.php new file mode 100644 index 00000000000..e9ae174f56e --- /dev/null +++ b/apps/dav/lib/Migration/Version1027Date20230504122946.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Migration; + +use Closure; +use OCA\DAV\CardDAV\SyncService; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; + +class Version1027Date20230504122946 extends SimpleMigrationStep { + private SyncService $syncService; + private LoggerInterface $logger; + + public function __construct(SyncService $syncService, LoggerInterface $logger) { + $this->syncService = $syncService; + $this->logger = $logger; + } + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $this->syncService->syncInstance(); + } +} |