Skip to content

Commit

Permalink
Merge pull request #38048 from nextcloud/enh/add-x-nc-scope-property
Browse files Browse the repository at this point in the history
feat(dav): store scopes for properties and filter locally scoped properties for federated address book sync
  • Loading branch information
ChristophWurst authored May 10, 2023
2 parents 5993a4b + bd80a1b commit 1fd8f41
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 65 deletions.
2 changes: 1 addition & 1 deletion apps/dav/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<name>WebDAV</name>
<summary>WebDAV endpoint</summary>
<description>WebDAV endpoint</description>
<version>1.26.0</version>
<version>1.27.0</version>
<licence>agpl</licence>
<author>owncloud.org</author>
<namespace>DAV</namespace>
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => $baseDir . '/../lib/Migration/Version1017Date20210216083742.php',
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => $baseDir . '/../lib/Migration/Version1018Date20210312100735.php',
'OCA\\DAV\\Migration\\Version1024Date20211221144219' => $baseDir . '/../lib/Migration/Version1024Date20211221144219.php',
'OCA\\DAV\\Migration\\Version1027Date20230504122946' => $baseDir . '/../lib/Migration/Version1027Date20230504122946.php',
'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20210216083742.php',
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => __DIR__ . '/..' . '/../lib/Migration/Version1018Date20210312100735.php',
'OCA\\DAV\\Migration\\Version1024Date20211221144219' => __DIR__ . '/..' . '/../lib/Migration/Version1024Date20211221144219.php',
'OCA\\DAV\\Migration\\Version1027Date20230504122946' => __DIR__ . '/..' . '/../lib/Migration/Version1027Date20230504122946.php',
'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
Expand Down
95 changes: 44 additions & 51 deletions apps/dav/lib/CardDAV/Converter.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,12 @@

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;

Expand All @@ -46,13 +43,7 @@ public function __construct(IAccountManager $accountManager) {
}

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();
Expand All @@ -65,47 +56,49 @@ public function createCardFromUser(IUser $user): ?VCard {
$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;
}
}

Expand Down
179 changes: 177 additions & 2 deletions apps/dav/lib/CardDAV/SystemAddressbook.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,34 @@
*/
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';
Expand All @@ -50,4 +64,165 @@ public function getChildren() {

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();
}
}
15 changes: 14 additions & 1 deletion apps/dav/lib/CardDAV/UserAddressBooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@
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;
Expand Down Expand Up @@ -73,7 +78,15 @@ public function getChildren() {
/** @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);
Expand Down
Loading

0 comments on commit 1fd8f41

Please sign in to comment.