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
/**
* @ copyright Copyright ( c ) 2018 , Roeland Jago Douma < roeland @ famdouma . nl >
*
2021-06-04 19:52:51 +00:00
* @ author Joas Schilling < coding @ schilljs . com >
2020-03-31 08:49:10 +00:00
* @ author Julius Härtl < jus @ bitgrid . net >
2018-12-04 13:32:04 +00:00
* @ author Roeland Jago Douma < roeland @ famdouma . nl >
2023-05-11 16:59:30 +00:00
* @ author Anna Larch < anna . larch @ gmx . net >
2018-12-04 13:32:04 +00:00
*
* @ 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
2021-06-04 19:52:51 +00:00
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2018-12-04 13:32:04 +00:00
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
2019-12-03 18:57:53 +00:00
* along with this program . If not , see < http :// www . gnu . org / licenses />.
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 ;
2023-05-09 17:17:39 +00:00
use Sabre\CardDAV\Backend\SyncSupport ;
2018-12-04 13:32:04 +00:00
use Sabre\CardDAV\Backend\BackendInterface ;
2023-05-09 17:17:39 +00:00
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
/** @var IConfig */
private $config ;
2023-05-11 16:59:30 +00:00
private IUserSession $userSession ;
2023-05-09 17:17:39 +00:00
private ? TrustedServers $trustedServers ;
private ? IRequest $request ;
2023-05-11 16:59:30 +00:00
private ? IGroupManager $groupManager ;
2018-12-04 13:32:04 +00:00
2023-05-11 16:59:30 +00:00
public function __construct ( BackendInterface $carddavBackend ,
array $addressBookInfo ,
IL10N $l10n ,
IConfig $config ,
IUserSession $userSession ,
? IRequest $request = null ,
? TrustedServers $trustedServers = null ,
? IGroupManager $groupManager ) {
2018-12-04 13:32:04 +00:00
parent :: __construct ( $carddavBackend , $addressBookInfo , $l10n );
$this -> config = $config ;
2023-05-11 16:59:30 +00:00
$this -> userSession = $userSession ;
2023-05-09 17:17:39 +00:00
$this -> request = $request ;
$this -> trustedServers = $trustedServers ;
2023-05-11 16:59:30 +00:00
$this -> groupManager = $groupManager ;
$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-23 15:51:44 +00:00
if ( ! $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:
return strpos ( $child -> getName (), 'Guests:' ) !== 0 ;
});
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' ;
if ( ! $shareEnumeration || ( ! $shareEnumerationGroup && $shareEnumerationPhone )) {
$user = $this -> userSession -> getUser ();
// 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 ) {
$user = $this -> userSession -> getUser ();
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-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' ;
if ( ! $shareEnumeration || ( ! $shareEnumerationGroup && $shareEnumerationPhone )) {
$currentUser = $this -> userSession -> getUser ();
$ownName = $currentUser !== null ? SyncService :: getCardUri ( $currentUser ) : null ;
if ( $ownName === $name ) {
return parent :: getChild ( $name );
}
throw new Forbidden ();
}
if ( $shareEnumerationGroup ) {
$user = $this -> userSession -> getUser ();
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
}