2018-12-04 13:32:04 +00:00
< ? php
2019-12-03 18:57:53 +00:00
2018-12-04 13:32:04 +00:00
declare ( strict_types = 1 );
2019-12-03 18:57:53 +00:00
2018-12-04 13:32:04 +00:00
/**
2024-05-27 15:39:07 +00:00
* SPDX - FileCopyrightText : 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2018-12-04 13:32:04 +00:00
*/
namespace OCA\DAV\CardDAV ;
2023-05-09 17:17:39 +00:00
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException ;
use OCA\Federation\TrustedServers ;
use OCP\Accounts\IAccountManager ;
2018-12-04 13:32:04 +00:00
use OCP\IConfig ;
2023-05-11 16:59:30 +00:00
use OCP\IGroupManager ;
2018-12-04 13:32:04 +00:00
use OCP\IL10N ;
2023-05-09 17:17:39 +00:00
use OCP\IRequest ;
2023-05-11 16:59:30 +00:00
use OCP\IUserSession ;
2018-12-04 13:32:04 +00:00
use Sabre\CardDAV\Backend\BackendInterface ;
2023-05-09 17:17:39 +00:00
use Sabre\CardDAV\Backend\SyncSupport ;
use Sabre\CardDAV\Card ;
use Sabre\DAV\Exception\Forbidden ;
use Sabre\DAV\Exception\NotFound ;
use Sabre\VObject\Component\VCard ;
use Sabre\VObject\Reader ;
2023-05-15 08:27:53 +00:00
use function array_filter ;
2023-05-23 15:51:44 +00:00
use function array_intersect ;
2023-05-11 16:59:30 +00:00
use function array_unique ;
2023-05-23 15:51:44 +00:00
use function in_array ;
2018-12-04 13:32:04 +00:00
class SystemAddressbook extends AddressBook {
2023-05-11 16:59:30 +00:00
public const URI_SHARED = 'z-server-generated--system' ;
2018-12-04 13:32:04 +00:00
2024-10-18 10:04:22 +00:00
public function __construct (
BackendInterface $carddavBackend ,
2023-05-11 16:59:30 +00:00
array $addressBookInfo ,
IL10N $l10n ,
2024-10-18 10:04:22 +00:00
private IConfig $config ,
private IUserSession $userSession ,
private ? IRequest $request = null ,
private ? TrustedServers $trustedServers = null ,
private ? IGroupManager $groupManager = null ,
) {
2018-12-04 13:32:04 +00:00
parent :: __construct ( $carddavBackend , $addressBookInfo , $l10n );
2023-05-11 16:59:30 +00:00
$this -> addressBookInfo [ '{DAV:}displayname' ] = $l10n -> t ( 'Accounts' );
$this -> addressBookInfo [ '{' . Plugin :: NS_CARDDAV . '}addressbook-description' ] = $l10n -> t ( 'System address book which holds all accounts' );
2018-12-04 13:32:04 +00:00
}
2023-05-11 16:59:30 +00:00
/**
* No checkbox checked -> Show only the same user
* 'Allow username autocompletion in share dialog' -> show everyone
* 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' -> show only users in intersecting groups
* 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users based on phone number integration' -> show only the same user
* 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' + 'Allow username autocompletion to users based on phone number integration' -> show only users in intersecting groups
*/
public function getChildren () {
2020-03-10 15:19:23 +00:00
$shareEnumeration = $this -> config -> getAppValue ( 'core' , 'shareapi_allow_share_dialog_user_enumeration' , 'yes' ) === 'yes' ;
2021-03-09 20:48:48 +00:00
$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' ;
2023-05-11 16:59:30 +00:00
$user = $this -> userSession -> getUser ();
if ( ! $user ) {
// Should never happen because we don't allow anonymous access
2018-12-04 13:32:04 +00:00
return [];
}
2023-05-24 20:27:51 +00:00
if ( $user -> getBackendClassName () === 'Guests' || ! $shareEnumeration || ( ! $shareEnumerationGroup && $shareEnumerationPhone )) {
2023-05-11 16:59:30 +00:00
$name = SyncService :: getCardUri ( $user );
try {
return [ parent :: getChild ( $name )];
} catch ( NotFound $e ) {
return [];
}
}
if ( $shareEnumerationGroup ) {
if ( $this -> groupManager === null ) {
// Group manager is not available, so we can't determine which data is safe
return [];
}
$groups = $this -> groupManager -> getUserGroups ( $user );
$names = [];
foreach ( $groups as $group ) {
$users = $group -> getUsers ();
foreach ( $users as $groupUser ) {
if ( $groupUser -> getBackendClassName () === 'Guests' ) {
continue ;
}
$names [] = SyncService :: getCardUri ( $groupUser );
}
}
return parent :: getMultipleChildren ( array_unique ( $names ));
}
2018-12-04 13:32:04 +00:00
2023-05-11 16:59:30 +00:00
$children = parent :: getChildren ();
return array_filter ( $children , function ( Card $child ) {
// check only for URIs that begin with Guests:
2023-06-02 12:08:19 +00:00
return ! str_starts_with ( $child -> getName (), 'Guests:' );
2023-05-11 16:59:30 +00:00
});
2018-12-04 13:32:04 +00:00
}
2023-05-09 17:17:39 +00:00
/**
* @ param array $paths
* @ return Card []
* @ throws NotFound
*/
public function getMultipleChildren ( $paths ) : array {
2023-05-23 15:51:44 +00:00
$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' ;
2023-05-24 20:27:51 +00:00
$user = $this -> userSession -> getUser ();
if (( $user !== null && $user -> getBackendClassName () === 'Guests' ) || ! $shareEnumeration || ( ! $shareEnumerationGroup && $shareEnumerationPhone )) {
2023-05-23 15:51:44 +00:00
// No user or cards with no access
if ( $user === null || ! in_array ( SyncService :: getCardUri ( $user ), $paths , true )) {
return [];
}
// Only return the own card
try {
return [ parent :: getChild ( SyncService :: getCardUri ( $user ))];
} catch ( NotFound $e ) {
return [];
}
}
if ( $shareEnumerationGroup ) {
if ( $this -> groupManager === null || $user === null ) {
// Group manager or user is not available, so we can't determine which data is safe
return [];
}
$groups = $this -> groupManager -> getUserGroups ( $user );
$allowedNames = [];
foreach ( $groups as $group ) {
$users = $group -> getUsers ();
foreach ( $users as $groupUser ) {
if ( $groupUser -> getBackendClassName () === 'Guests' ) {
continue ;
}
$allowedNames [] = SyncService :: getCardUri ( $groupUser );
}
}
return parent :: getMultipleChildren ( array_intersect ( $paths , $allowedNames ));
}
2023-05-09 17:17:39 +00:00
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 {
2023-05-24 20:27:51 +00:00
$user = $this -> userSession -> getUser ();
2023-05-23 15:51:44 +00:00
$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' ;
2023-05-24 20:27:51 +00:00
if (( $user !== null && $user -> getBackendClassName () === 'Guests' ) || ! $shareEnumeration || ( ! $shareEnumerationGroup && $shareEnumerationPhone )) {
$ownName = $user !== null ? SyncService :: getCardUri ( $user ) : null ;
2023-05-23 15:51:44 +00:00
if ( $ownName === $name ) {
return parent :: getChild ( $name );
}
throw new Forbidden ();
}
if ( $shareEnumerationGroup ) {
if ( $user === null || $this -> groupManager === null ) {
// Group manager is not available, so we can't determine which data is safe
throw new Forbidden ();
}
$groups = $this -> groupManager -> getUserGroups ( $user );
foreach ( $groups as $group ) {
foreach ( $group -> getUsers () as $groupUser ) {
if ( $groupUser -> getBackendClassName () === 'Guests' ) {
continue ;
}
$otherName = SyncService :: getCardUri ( $groupUser );
if ( $otherName === $name ) {
return parent :: getChild ( $name );
}
}
}
throw new Forbidden ();
}
2023-05-09 17:17:39 +00:00
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 */
2023-05-16 11:09:26 +00:00
$server = $this -> request -> server ;
if ( ! isset ( $server [ 'PHP_AUTH_USER' ]) || $server [ 'PHP_AUTH_USER' ] !== 'system' ) {
2023-05-09 17:17:39 +00:00
return false ;
}
/** @psalm-suppress NoInterfaceProperties */
2023-05-16 11:09:26 +00:00
$sharedSecret = $server [ 'PHP_AUTH_PW' ] ? ? null ;
2023-05-09 17:17:39 +00:00
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 ();
}
2023-05-11 16:59:30 +00:00
/**
* @ return mixed
* @ throws Forbidden
*/
public function delete () {
if ( $this -> isFederation ()) {
parent :: delete ();
}
throw new Forbidden ();
}
2023-05-15 08:27:53 +00:00
public function getACL () {
2023-05-16 11:09:26 +00:00
return array_filter ( parent :: getACL (), function ( $acl ) {
2023-05-15 08:27:53 +00:00
if ( in_array ( $acl [ 'privilege' ], [ '{DAV:}write' , '{DAV:}all' ], true )) {
return false ;
}
return true ;
});
}
2018-12-04 13:32:04 +00:00
}