229 lines
6.3 KiB
PHP
229 lines
6.3 KiB
PHP
<?php
|
|
|
|
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>
|
|
*
|
|
* @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\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();
|
|
}
|
|
}
|