* * @author Joas Schilling * @author Julius Härtl * @author Roeland Jago Douma * * @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 . * */ 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, ?IRequest $request = null, ?TrustedServers $trustedServers = null) { parent::__construct($carddavBackend, $addressBookInfo, $l10n); $this->config = $config; $this->request = $request; $this->trustedServers = $trustedServers; } 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'; if (!$shareEnumeration || $shareEnumerationGroup || $shareEnumerationPhone) { return []; } 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(); } }