2015-10-30 18:05:25 +03:00
< ? php
/**
2016-07-21 17:49:16 +03:00
* @ copyright Copyright ( c ) 2016 , ownCloud , Inc .
*
2019-12-03 21:57:53 +03:00
* @ author Arne Hamann < kontakt + github @ arne . email >
2016-05-26 20:56:05 +03:00
* @ author Arthur Schiwon < blizzz @ arthur - schiwon . de >
2016-07-21 17:49:16 +03:00
* @ author Bjoern Schiessle < bjoern @ schiessle . org >
2016-05-26 20:56:05 +03:00
* @ author Björn Schießle < bjoern @ schiessle . org >
2020-03-31 11:49:10 +03:00
* @ author Christoph Wurst < christoph @ winzerhof - wurst . at >
2017-11-06 22:15:27 +03:00
* @ author Georg Ehrke < oc . list @ georgehrke . com >
2016-07-21 17:49:16 +03:00
* @ author Joas Schilling < coding @ schilljs . com >
2017-11-06 17:56:42 +03:00
* @ author John Molakvoæ ( skjnldsv ) < skjnldsv @ protonmail . com >
2020-04-29 12:57:22 +03:00
* @ author Julius Härtl < jus @ bitgrid . net >
2017-11-06 17:56:42 +03:00
* @ author Lukas Reschke < lukas @ statuscode . ch >
2019-12-03 21:57:53 +03:00
* @ author Morris Jobke < hey @ morrisjobke . de >
2017-11-06 17:56:42 +03:00
* @ author Robin Appelman < robin @ icewind . nl >
* @ author Roeland Jago Douma < roeland @ famdouma . nl >
2016-05-26 20:56:05 +03:00
* @ author Stefan Weil < sw @ weilnetz . de >
2020-03-31 11:49:10 +03:00
* @ author Thomas Citharel < nextcloud @ tcit . fr >
2015-10-30 18:05:25 +03:00
* @ author Thomas Müller < thomas . mueller @ tmit . eu >
*
* @ license AGPL - 3.0
*
* This code is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License , version 3 ,
* as published by the Free Software Foundation .
*
* 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 , version 3 ,
2019-12-03 21:57:53 +03:00
* along with this program . If not , see < http :// www . gnu . org / licenses />
2015-10-30 18:05:25 +03:00
*
*/
namespace OCA\DAV\CardDAV ;
2015-11-05 18:46:37 +03:00
use OCA\DAV\Connector\Sabre\Principal ;
2016-01-26 00:49:26 +03:00
use OCA\DAV\DAV\Sharing\Backend ;
use OCA\DAV\DAV\Sharing\IShareable ;
2020-07-28 10:35:51 +03:00
use OCA\DAV\Events\AddressBookCreatedEvent ;
use OCA\DAV\Events\AddressBookDeletedEvent ;
use OCA\DAV\Events\AddressBookShareUpdatedEvent ;
use OCA\DAV\Events\AddressBookUpdatedEvent ;
use OCA\DAV\Events\CardCreatedEvent ;
use OCA\DAV\Events\CardDeletedEvent ;
use OCA\DAV\Events\CardUpdatedEvent ;
2019-11-22 22:52:10 +03:00
use OCP\DB\QueryBuilder\IQueryBuilder ;
2020-07-28 10:35:51 +03:00
use OCP\EventDispatcher\IEventDispatcher ;
2015-11-25 17:24:50 +03:00
use OCP\IDBConnection ;
2017-10-05 13:32:46 +03:00
use OCP\IGroupManager ;
2016-08-18 16:14:04 +03:00
use OCP\IUser ;
use OCP\IUserManager ;
2016-02-10 19:06:13 +03:00
use PDO ;
2015-10-30 18:05:25 +03:00
use Sabre\CardDAV\Backend\BackendInterface ;
use Sabre\CardDAV\Backend\SyncSupport ;
use Sabre\CardDAV\Plugin ;
use Sabre\DAV\Exception\BadRequest ;
2015-12-14 13:48:31 +03:00
use Sabre\VObject\Component\VCard ;
2015-11-25 17:24:50 +03:00
use Sabre\VObject\Reader ;
2016-02-03 17:43:45 +03:00
use Symfony\Component\EventDispatcher\EventDispatcherInterface ;
use Symfony\Component\EventDispatcher\GenericEvent ;
2015-10-30 18:05:25 +03:00
class CardDavBackend implements BackendInterface , SyncSupport {
2020-04-10 17:54:27 +03:00
public const PERSONAL_ADDRESSBOOK_URI = 'contacts' ;
public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts' ;
2016-09-20 15:15:23 +03:00
2015-11-05 18:46:37 +03:00
/** @var Principal */
private $principalBackend ;
2015-11-25 17:24:50 +03:00
/** @var string */
private $dbCardsTable = 'cards' ;
/** @var string */
private $dbCardsPropertiesTable = 'cards_properties' ;
/** @var IDBConnection */
private $db ;
2016-01-26 00:49:26 +03:00
/** @var Backend */
private $sharingBackend ;
2015-11-25 17:24:50 +03:00
/** @var array properties to index */
2020-03-26 11:30:18 +03:00
public static $indexProperties = [
2020-04-09 10:22:29 +03:00
'BDAY' , 'UID' , 'N' , 'FN' , 'TITLE' , 'ROLE' , 'NOTE' , 'NICKNAME' ,
'ORG' , 'CATEGORIES' , 'EMAIL' , 'TEL' , 'IMPP' , 'ADR' , 'URL' , 'GEO' , 'CLOUD' ];
2015-11-25 17:24:50 +03:00
2016-08-18 16:14:04 +03:00
/**
* @ var string [] Map of uid => display name
*/
protected $userDisplayNames ;
/** @var IUserManager */
private $userManager ;
2020-07-28 10:35:51 +03:00
/** @var IEventDispatcher */
2016-02-03 17:43:45 +03:00
private $dispatcher ;
2020-07-28 10:35:51 +03:00
/** @var EventDispatcherInterface */
private $legacyDispatcher ;
2020-07-07 15:54:26 +03:00
private $etagCache = [];
2015-11-25 17:24:50 +03:00
/**
* CardDavBackend constructor .
*
* @ param IDBConnection $db
* @ param Principal $principalBackend
2016-08-18 16:14:04 +03:00
* @ param IUserManager $userManager
2017-10-05 13:32:46 +03:00
* @ param IGroupManager $groupManager
2020-07-28 10:35:51 +03:00
* @ param IEventDispatcher $dispatcher
* @ param EventDispatcherInterface $legacyDispatcher
2015-11-25 17:24:50 +03:00
*/
2016-02-03 17:43:45 +03:00
public function __construct ( IDBConnection $db ,
Principal $principalBackend ,
2016-08-18 16:14:04 +03:00
IUserManager $userManager ,
2017-10-05 13:32:46 +03:00
IGroupManager $groupManager ,
2020-07-28 10:35:51 +03:00
IEventDispatcher $dispatcher ,
EventDispatcherInterface $legacyDispatcher ) {
2015-10-30 18:05:25 +03:00
$this -> db = $db ;
2015-11-05 18:46:37 +03:00
$this -> principalBackend = $principalBackend ;
2016-08-18 16:14:04 +03:00
$this -> userManager = $userManager ;
2016-02-03 17:43:45 +03:00
$this -> dispatcher = $dispatcher ;
2020-07-28 10:35:51 +03:00
$this -> legacyDispatcher = $legacyDispatcher ;
2017-10-05 13:32:46 +03:00
$this -> sharingBackend = new Backend ( $this -> db , $this -> userManager , $groupManager , $principalBackend , 'addressbook' );
2015-10-30 18:05:25 +03:00
}
2016-08-30 16:11:33 +03:00
/**
* Return the number of address books for a principal
*
* @ param $principalUri
* @ return int
*/
public function getAddressBooksForUserCount ( $principalUri ) {
$principalUri = $this -> convertPrincipal ( $principalUri , true );
$query = $this -> db -> getQueryBuilder ();
2018-10-19 17:44:28 +03:00
$query -> select ( $query -> func () -> count ( '*' ))
2016-08-30 16:11:33 +03:00
-> from ( 'addressbooks' )
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )));
2016-09-09 20:15:27 +03:00
return ( int ) $query -> execute () -> fetchColumn ();
2016-08-30 16:11:33 +03:00
}
2015-10-30 18:05:25 +03:00
/**
2016-01-13 13:55:50 +03:00
* Returns the list of address books for a specific user .
2015-10-30 18:05:25 +03:00
*
* Every addressbook should have the following properties :
* id - an arbitrary unique id
* uri - the 'basename' part of the url
* principaluri - Same as the passed parameter
*
* Any additional clark - notation property may be passed besides this . Some
* common ones are :
* { DAV : } displayname
* { urn : ietf : params : xml : ns : carddav } addressbook - description
* { http :// calendarserver . org / ns / } getctag
*
* @ param string $principalUri
* @ return array
*/
2020-04-10 17:51:06 +03:00
public function getAddressBooksForUser ( $principalUri ) {
2016-03-24 19:11:07 +03:00
$principalUriOriginal = $principalUri ;
2016-01-20 23:08:23 +03:00
$principalUri = $this -> convertPrincipal ( $principalUri , true );
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
$query -> select ([ 'id' , 'uri' , 'displayname' , 'principaluri' , 'description' , 'synctoken' ])
-> from ( 'addressbooks' )
2016-01-20 23:08:23 +03:00
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )));
2015-10-30 18:05:25 +03:00
$addressBooks = [];
$result = $query -> execute ();
2020-04-10 15:19:56 +03:00
while ( $row = $result -> fetch ()) {
2016-01-28 19:40:59 +03:00
$addressBooks [ $row [ 'id' ]] = [
2020-07-30 22:18:20 +03:00
'id' => $row [ 'id' ],
2015-10-30 18:05:25 +03:00
'uri' => $row [ 'uri' ],
2016-01-20 23:08:23 +03:00
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], false ),
2015-10-30 18:05:25 +03:00
'{DAV:}displayname' => $row [ 'displayname' ],
'{' . Plugin :: NS_CARDDAV . '}addressbook-description' => $row [ 'description' ],
'{http://calendarserver.org/ns/}getctag' => $row [ 'synctoken' ],
2020-08-03 12:25:21 +03:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2015-10-30 18:05:25 +03:00
];
2017-04-19 17:18:44 +03:00
$this -> addOwnerPrincipal ( $addressBooks [ $row [ 'id' ]]);
2015-10-30 18:05:25 +03:00
}
$result -> closeCursor ();
2019-03-01 15:02:30 +03:00
// query for shared addressbooks
2016-03-24 19:11:07 +03:00
$principals = $this -> principalBackend -> getGroupMembership ( $principalUriOriginal , true );
2019-03-01 15:02:30 +03:00
$principals = array_merge ( $principals , $this -> principalBackend -> getCircleMembership ( $principalUriOriginal ));
2020-04-09 14:53:40 +03:00
$principals = array_map ( function ( $principal ) {
2017-06-08 10:08:24 +03:00
return urldecode ( $principal );
}, $principals );
2020-07-30 22:18:20 +03:00
$principals [] = $principalUri ;
2016-01-11 19:29:01 +03:00
2015-11-05 18:46:37 +03:00
$query = $this -> db -> getQueryBuilder ();
2016-01-21 18:20:52 +03:00
$result = $query -> select ([ 'a.id' , 'a.uri' , 'a.displayname' , 'a.principaluri' , 'a.description' , 'a.synctoken' , 's.access' ])
2016-01-12 15:23:50 +03:00
-> from ( 'dav_shares' , 's' )
2016-01-12 17:44:50 +03:00
-> join ( 's' , 'addressbooks' , 'a' , $query -> expr () -> eq ( 's.resourceid' , 'a.id' ))
2016-01-12 15:23:50 +03:00
-> where ( $query -> expr () -> in ( 's.principaluri' , $query -> createParameter ( 'principaluri' )))
-> andWhere ( $query -> expr () -> eq ( 's.type' , $query -> createParameter ( 'type' )))
2015-11-05 18:46:37 +03:00
-> setParameter ( 'type' , 'addressbook' )
2016-01-25 19:17:36 +03:00
-> setParameter ( 'principaluri' , $principals , IQueryBuilder :: PARAM_STR_ARRAY )
2015-11-05 18:46:37 +03:00
-> execute ();
2017-03-02 13:34:27 +03:00
$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}read-only' ;
2020-04-10 15:19:56 +03:00
while ( $row = $result -> fetch ()) {
2017-03-26 01:07:09 +03:00
if ( $row [ 'principaluri' ] === $principalUri ) {
continue ;
}
2020-07-30 22:18:20 +03:00
$readOnly = ( int ) $row [ 'access' ] === Backend :: ACCESS_READ ;
2017-03-02 13:24:56 +03:00
if ( isset ( $addressBooks [ $row [ 'id' ]])) {
2017-03-02 13:34:27 +03:00
if ( $readOnly ) {
// New share can not have more permissions then the old one.
continue ;
}
if ( isset ( $addressBooks [ $row [ 'id' ]][ $readOnlyPropertyName ]) &&
$addressBooks [ $row [ 'id' ]][ $readOnlyPropertyName ] === 0 ) {
// Old share is already read-write, no more permissions can be gained
continue ;
}
2017-03-02 13:24:56 +03:00
}
2017-07-20 10:43:23 +03:00
list (, $name ) = \Sabre\Uri\split ( $row [ 'principaluri' ]);
2016-01-21 18:20:52 +03:00
$uri = $row [ 'uri' ] . '_shared_by_' . $name ;
2016-08-18 16:14:04 +03:00
$displayName = $row [ 'displayname' ] . ' (' . $this -> getUserDisplayName ( $name ) . ')' ;
2017-03-02 13:24:56 +03:00
$addressBooks [ $row [ 'id' ]] = [
2020-07-30 22:18:20 +03:00
'id' => $row [ 'id' ],
2017-03-02 13:24:56 +03:00
'uri' => $uri ,
'principaluri' => $principalUriOriginal ,
'{DAV:}displayname' => $displayName ,
'{' . Plugin :: NS_CARDDAV . '}addressbook-description' => $row [ 'description' ],
'{http://calendarserver.org/ns/}getctag' => $row [ 'synctoken' ],
2020-08-03 12:25:21 +03:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2017-03-02 13:24:56 +03:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}owner-principal' => $row [ 'principaluri' ],
2017-03-02 13:34:27 +03:00
$readOnlyPropertyName => $readOnly ,
2017-03-02 13:24:56 +03:00
];
2017-04-19 17:18:44 +03:00
$this -> addOwnerPrincipal ( $addressBooks [ $row [ 'id' ]]);
2015-11-05 18:46:37 +03:00
}
$result -> closeCursor ();
2016-01-28 19:40:59 +03:00
return array_values ( $addressBooks );
2015-10-30 18:05:25 +03:00
}
2017-02-14 18:56:56 +03:00
public function getUsersOwnAddressBooks ( $principalUri ) {
2017-02-14 11:21:33 +03:00
$principalUri = $this -> convertPrincipal ( $principalUri , true );
$query = $this -> db -> getQueryBuilder ();
$query -> select ([ 'id' , 'uri' , 'displayname' , 'principaluri' , 'description' , 'synctoken' ])
2020-07-30 22:18:20 +03:00
-> from ( 'addressbooks' )
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )));
2017-02-14 11:21:33 +03:00
$addressBooks = [];
$result = $query -> execute ();
2020-04-10 15:19:56 +03:00
while ( $row = $result -> fetch ()) {
2017-02-14 11:21:33 +03:00
$addressBooks [ $row [ 'id' ]] = [
2020-07-30 22:18:20 +03:00
'id' => $row [ 'id' ],
2017-02-14 11:21:33 +03:00
'uri' => $row [ 'uri' ],
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], false ),
'{DAV:}displayname' => $row [ 'displayname' ],
'{' . Plugin :: NS_CARDDAV . '}addressbook-description' => $row [ 'description' ],
'{http://calendarserver.org/ns/}getctag' => $row [ 'synctoken' ],
2020-08-03 12:25:21 +03:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2017-02-14 11:21:33 +03:00
];
2017-04-19 17:18:44 +03:00
$this -> addOwnerPrincipal ( $addressBooks [ $row [ 'id' ]]);
2017-02-14 11:21:33 +03:00
}
$result -> closeCursor ();
return array_values ( $addressBooks );
}
2016-08-18 16:14:04 +03:00
private function getUserDisplayName ( $uid ) {
if ( ! isset ( $this -> userDisplayNames [ $uid ])) {
$user = $this -> userManager -> get ( $uid );
if ( $user instanceof IUser ) {
$this -> userDisplayNames [ $uid ] = $user -> getDisplayName ();
} else {
$this -> userDisplayNames [ $uid ] = $uid ;
}
}
return $this -> userDisplayNames [ $uid ];
}
2015-12-22 18:11:53 +03:00
/**
* @ param int $addressBookId
*/
public function getAddressBookById ( $addressBookId ) {
$query = $this -> db -> getQueryBuilder ();
$result = $query -> select ([ 'id' , 'uri' , 'displayname' , 'principaluri' , 'description' , 'synctoken' ])
-> from ( 'addressbooks' )
-> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $addressBookId )))
-> execute ();
$row = $result -> fetch ();
$result -> closeCursor ();
if ( $row === false ) {
return null ;
}
2017-04-19 17:18:44 +03:00
$addressBook = [
2020-07-30 22:18:20 +03:00
'id' => $row [ 'id' ],
2015-12-22 18:11:53 +03:00
'uri' => $row [ 'uri' ],
'principaluri' => $row [ 'principaluri' ],
'{DAV:}displayname' => $row [ 'displayname' ],
'{' . Plugin :: NS_CARDDAV . '}addressbook-description' => $row [ 'description' ],
'{http://calendarserver.org/ns/}getctag' => $row [ 'synctoken' ],
2020-08-03 12:25:21 +03:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2015-12-22 18:11:53 +03:00
];
2017-04-19 17:18:44 +03:00
$this -> addOwnerPrincipal ( $addressBook );
return $addressBook ;
2015-12-22 18:11:53 +03:00
}
/**
* @ param $addressBookUri
* @ return array | null
*/
public function getAddressBooksByUri ( $principal , $addressBookUri ) {
2015-11-05 18:46:37 +03:00
$query = $this -> db -> getQueryBuilder ();
$result = $query -> select ([ 'id' , 'uri' , 'displayname' , 'principaluri' , 'description' , 'synctoken' ])
-> from ( 'addressbooks' )
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $addressBookUri )))
2015-12-22 18:11:53 +03:00
-> andWhere ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principal )))
2015-11-05 18:46:37 +03:00
-> setMaxResults ( 1 )
-> execute ();
$row = $result -> fetch ();
2015-12-02 11:20:58 +03:00
$result -> closeCursor ();
2015-11-25 01:53:27 +03:00
if ( $row === false ) {
2015-11-05 18:46:37 +03:00
return null ;
}
2017-04-19 17:18:44 +03:00
$addressBook = [
2020-07-30 22:18:20 +03:00
'id' => $row [ 'id' ],
2017-04-19 17:18:44 +03:00
'uri' => $row [ 'uri' ],
'principaluri' => $row [ 'principaluri' ],
'{DAV:}displayname' => $row [ 'displayname' ],
'{' . Plugin :: NS_CARDDAV . '}addressbook-description' => $row [ 'description' ],
'{http://calendarserver.org/ns/}getctag' => $row [ 'synctoken' ],
2020-08-03 12:25:21 +03:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2017-04-19 17:18:44 +03:00
];
$this -> addOwnerPrincipal ( $addressBook );
return $addressBook ;
2015-11-05 18:46:37 +03:00
}
2015-10-30 18:05:25 +03:00
/**
* Updates properties for an address book .
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object .
* To do the actual updates , you must tell this object which properties
* you ' re going to process with the handle () method .
*
* Calling the handle method is like telling the PropPatch object " I
* promise I can handle updating this property " .
*
2015-11-18 17:04:53 +03:00
* Read the PropPatch documentation for more info and examples .
2015-10-30 18:05:25 +03:00
*
* @ param string $addressBookId
* @ param \Sabre\DAV\PropPatch $propPatch
* @ return void
*/
2020-04-10 17:51:06 +03:00
public function updateAddressBook ( $addressBookId , \Sabre\DAV\PropPatch $propPatch ) {
2015-10-30 18:05:25 +03:00
$supportedProperties = [
'{DAV:}displayname' ,
'{' . Plugin :: NS_CARDDAV . '}addressbook-description' ,
];
2017-07-20 23:48:13 +03:00
/**
* @ suppress SqlInjectionChecker
*/
2020-04-09 14:53:40 +03:00
$propPatch -> handle ( $supportedProperties , function ( $mutations ) use ( $addressBookId ) {
2015-10-30 18:05:25 +03:00
$updates = [];
2020-07-30 22:18:20 +03:00
foreach ( $mutations as $property => $newValue ) {
2020-04-10 15:19:56 +03:00
switch ( $property ) {
2020-04-09 17:17:53 +03:00
case '{DAV:}displayname' :
2015-10-30 18:05:25 +03:00
$updates [ 'displayname' ] = $newValue ;
break ;
2020-04-09 17:17:53 +03:00
case '{' . Plugin :: NS_CARDDAV . '}addressbook-description' :
2015-10-30 18:05:25 +03:00
$updates [ 'description' ] = $newValue ;
break ;
}
}
$query = $this -> db -> getQueryBuilder ();
$query -> update ( 'addressbooks' );
2020-07-30 22:18:20 +03:00
foreach ( $updates as $key => $value ) {
2015-10-30 18:05:25 +03:00
$query -> set ( $key , $query -> createNamedParameter ( $value ));
}
$query -> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $addressBookId )))
2020-07-30 22:18:20 +03:00
-> execute ();
2015-10-30 18:05:25 +03:00
$this -> addChange ( $addressBookId , " " , 2 );
2020-08-19 16:30:13 +03:00
$addressBookRow = $this -> getAddressBookById (( int ) $addressBookId );
2020-07-28 10:35:51 +03:00
$shares = $this -> getShares ( $addressBookId );
$this -> dispatcher -> dispatchTyped ( new AddressBookUpdatedEvent (( int ) $addressBookId , $addressBookRow , $shares , $mutations ));
2015-10-30 18:05:25 +03:00
return true ;
});
}
/**
* Creates a new address book
*
* @ param string $principalUri
* @ param string $url Just the 'basename' of the url .
* @ param array $properties
2015-12-22 18:11:53 +03:00
* @ return int
2015-11-10 13:37:07 +03:00
* @ throws BadRequest
2015-10-30 18:05:25 +03:00
*/
2020-04-10 17:51:06 +03:00
public function createAddressBook ( $principalUri , $url , array $properties ) {
2015-10-30 18:05:25 +03:00
$values = [
'displayname' => null ,
'description' => null ,
'principaluri' => $principalUri ,
'uri' => $url ,
'synctoken' => 1
];
2020-07-30 22:18:20 +03:00
foreach ( $properties as $property => $newValue ) {
2020-04-10 15:19:56 +03:00
switch ( $property ) {
2020-04-09 17:17:53 +03:00
case '{DAV:}displayname' :
2015-10-30 18:05:25 +03:00
$values [ 'displayname' ] = $newValue ;
break ;
2020-04-09 17:17:53 +03:00
case '{' . Plugin :: NS_CARDDAV . '}addressbook-description' :
2015-10-30 18:05:25 +03:00
$values [ 'description' ] = $newValue ;
break ;
2020-04-09 17:17:53 +03:00
default :
2015-10-30 18:05:25 +03:00
throw new BadRequest ( 'Unknown property: ' . $property );
}
}
2015-11-10 13:37:07 +03:00
// Fallback to make sure the displayname is set. Some clients may refuse
// to work with addressbooks not having a displayname.
2020-04-10 15:19:56 +03:00
if ( is_null ( $values [ 'displayname' ])) {
2015-11-10 13:37:07 +03:00
$values [ 'displayname' ] = $url ;
}
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( 'addressbooks' )
-> values ([
'uri' => $query -> createParameter ( 'uri' ),
'displayname' => $query -> createParameter ( 'displayname' ),
'description' => $query -> createParameter ( 'description' ),
'principaluri' => $query -> createParameter ( 'principaluri' ),
'synctoken' => $query -> createParameter ( 'synctoken' ),
])
-> setParameters ( $values )
-> execute ();
2015-12-22 18:11:53 +03:00
2020-07-28 10:35:51 +03:00
$addressBookId = $query -> getLastInsertId ();
$addressBookRow = $this -> getAddressBookById ( $addressBookId );
$this -> dispatcher -> dispatchTyped ( new AddressBookCreatedEvent (( int ) $addressBookId , $addressBookRow ));
return $addressBookId ;
2015-10-30 18:05:25 +03:00
}
/**
* Deletes an entire addressbook and all its contents
*
* @ param mixed $addressBookId
* @ return void
*/
2020-04-10 17:51:06 +03:00
public function deleteAddressBook ( $addressBookId ) {
2020-07-28 10:35:51 +03:00
$addressBookData = $this -> getAddressBookById ( $addressBookId );
$shares = $this -> getShares ( $addressBookId );
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
2020-03-12 13:05:05 +03:00
$query -> delete ( $this -> dbCardsTable )
2015-10-30 18:05:25 +03:00
-> where ( $query -> expr () -> eq ( 'addressbookid' , $query -> createParameter ( 'addressbookid' )))
-> setParameter ( 'addressbookid' , $addressBookId )
-> execute ();
$query -> delete ( 'addressbookchanges' )
-> where ( $query -> expr () -> eq ( 'addressbookid' , $query -> createParameter ( 'addressbookid' )))
-> setParameter ( 'addressbookid' , $addressBookId )
-> execute ();
$query -> delete ( 'addressbooks' )
-> where ( $query -> expr () -> eq ( 'id' , $query -> createParameter ( 'id' )))
-> setParameter ( 'id' , $addressBookId )
-> execute ();
2015-11-05 18:46:37 +03:00
2016-02-03 22:18:56 +03:00
$this -> sharingBackend -> deleteAllShares ( $addressBookId );
2015-11-25 17:24:50 +03:00
$query -> delete ( $this -> dbCardsPropertiesTable )
-> where ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )))
-> execute ();
2020-07-28 10:35:51 +03:00
if ( $addressBookData ) {
$this -> dispatcher -> dispatchTyped ( new AddressBookDeletedEvent (( int ) $addressBookId , $addressBookData , $shares ));
}
2015-10-30 18:05:25 +03:00
}
/**
* Returns all cards for a specific addressbook id .
*
* This method should return the following properties for each card :
* * carddata - raw vcard data
* * uri - Some unique url
* * lastmodified - A unix timestamp
*
* It ' s recommended to also return the following properties :
* * etag - A unique etag . This must change every time the card changes .
* * size - The size of the card in bytes .
*
* If these last two properties are provided , less time will be spent
* calculating them . If they are specified , you can also ommit carddata .
* This may speed up certain requests , especially with large cards .
*
* @ param mixed $addressBookId
* @ return array
*/
2020-04-10 17:51:06 +03:00
public function getCards ( $addressBookId ) {
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
2018-10-27 12:54:57 +03:00
$query -> select ([ 'id' , 'uri' , 'lastmodified' , 'etag' , 'size' , 'carddata' , 'uid' ])
2020-03-12 13:05:05 +03:00
-> from ( $this -> dbCardsTable )
2015-10-30 18:05:25 +03:00
-> where ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )));
$cards = [];
$result = $query -> execute ();
2020-04-10 15:19:56 +03:00
while ( $row = $result -> fetch ()) {
2015-10-30 18:05:25 +03:00
$row [ 'etag' ] = '"' . $row [ 'etag' ] . '"' ;
2020-04-15 16:47:27 +03:00
$modified = false ;
$row [ 'carddata' ] = $this -> readBlob ( $row [ 'carddata' ], $modified );
if ( $modified ) {
$row [ 'size' ] = strlen ( $row [ 'carddata' ]);
}
2015-10-30 18:05:25 +03:00
$cards [] = $row ;
}
$result -> closeCursor ();
return $cards ;
}
/**
2016-01-29 20:25:27 +03:00
* Returns a specific card .
2015-10-30 18:05:25 +03:00
*
* The same set of properties must be returned as with getCards . The only
* exception is that 'carddata' is absolutely required .
*
* If the card does not exist , you must return false .
*
* @ param mixed $addressBookId
* @ param string $cardUri
* @ return array
*/
2020-04-10 17:51:06 +03:00
public function getCard ( $addressBookId , $cardUri ) {
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
2018-10-27 12:54:57 +03:00
$query -> select ([ 'id' , 'uri' , 'lastmodified' , 'etag' , 'size' , 'carddata' , 'uid' ])
2020-03-12 13:05:05 +03:00
-> from ( $this -> dbCardsTable )
2015-10-30 18:05:25 +03:00
-> where ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )))
-> andWhere ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $cardUri )))
-> setMaxResults ( 1 );
$result = $query -> execute ();
$row = $result -> fetch ();
if ( ! $row ) {
return false ;
}
$row [ 'etag' ] = '"' . $row [ 'etag' ] . '"' ;
2020-04-15 16:47:27 +03:00
$modified = false ;
$row [ 'carddata' ] = $this -> readBlob ( $row [ 'carddata' ], $modified );
if ( $modified ) {
$row [ 'size' ] = strlen ( $row [ 'carddata' ]);
}
2015-10-30 18:05:25 +03:00
return $row ;
}
/**
* Returns a list of cards .
*
* This method should work identical to getCard , but instead return all the
* cards in the list as an array .
*
* If the backend supports this , it may allow for some speed - ups .
*
* @ param mixed $addressBookId
2015-11-20 18:42:34 +03:00
* @ param string [] $uris
2015-10-30 18:05:25 +03:00
* @ return array
*/
2020-04-10 17:51:06 +03:00
public function getMultipleCards ( $addressBookId , array $uris ) {
2016-09-14 17:29:58 +03:00
if ( empty ( $uris )) {
return [];
}
$chunks = array_chunk ( $uris , 100 );
$cards = [];
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
2018-10-27 12:54:57 +03:00
$query -> select ([ 'id' , 'uri' , 'lastmodified' , 'etag' , 'size' , 'carddata' , 'uid' ])
2020-03-12 13:05:05 +03:00
-> from ( $this -> dbCardsTable )
2015-10-30 18:05:25 +03:00
-> where ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )))
2016-09-14 17:29:58 +03:00
-> andWhere ( $query -> expr () -> in ( 'uri' , $query -> createParameter ( 'uri' )));
2015-10-30 18:05:25 +03:00
2016-09-14 17:29:58 +03:00
foreach ( $chunks as $uris ) {
$query -> setParameter ( 'uri' , $uris , IQueryBuilder :: PARAM_STR_ARRAY );
$result = $query -> execute ();
2015-10-30 18:05:25 +03:00
2016-09-15 10:47:39 +03:00
while ( $row = $result -> fetch ()) {
2016-09-14 17:29:58 +03:00
$row [ 'etag' ] = '"' . $row [ 'etag' ] . '"' ;
2020-04-15 16:47:27 +03:00
$modified = false ;
$row [ 'carddata' ] = $this -> readBlob ( $row [ 'carddata' ], $modified );
if ( $modified ) {
$row [ 'size' ] = strlen ( $row [ 'carddata' ]);
}
2016-09-14 17:29:58 +03:00
$cards [] = $row ;
}
2016-09-15 10:47:39 +03:00
$result -> closeCursor ();
2015-10-30 18:05:25 +03:00
}
return $cards ;
}
/**
* Creates a new card .
*
* The addressbook id will be passed as the first argument . This is the
* same id as it is returned from the getAddressBooksForUser method .
*
* The cardUri is a base uri , and doesn ' t include the full path . The
* cardData argument is the vcard body , and is passed as a string .
*
* It is possible to return an ETag from this method . This ETag is for the
* newly created resource , and must be enclosed with double quotes ( that
* is , the string itself must contain the double quotes ) .
*
* You should only return the ETag if you store the carddata as - is . If a
* subsequent GET request on the same card does not have the same body ,
* byte - by - byte and you did return an ETag here , clients tend to get
* confused .
*
* If you don ' t return an ETag , you can just return null .
*
* @ param mixed $addressBookId
* @ param string $cardUri
* @ param string $cardData
2015-11-20 18:42:34 +03:00
* @ return string
2015-10-30 18:05:25 +03:00
*/
2020-04-10 17:51:06 +03:00
public function createCard ( $addressBookId , $cardUri , $cardData ) {
2015-10-30 18:05:25 +03:00
$etag = md5 ( $cardData );
2018-10-27 12:54:57 +03:00
$uid = $this -> getUID ( $cardData );
2015-10-30 18:05:25 +03:00
2018-11-03 21:03:32 +03:00
$q = $this -> db -> getQueryBuilder ();
$q -> select ( 'uid' )
2020-03-12 13:05:05 +03:00
-> from ( $this -> dbCardsTable )
2018-11-03 21:03:32 +03:00
-> where ( $q -> expr () -> eq ( 'addressbookid' , $q -> createNamedParameter ( $addressBookId )))
-> andWhere ( $q -> expr () -> eq ( 'uid' , $q -> createNamedParameter ( $uid )))
-> setMaxResults ( 1 );
$result = $q -> execute ();
2020-07-30 22:18:20 +03:00
$count = ( bool ) $result -> fetchColumn ();
2018-11-03 21:03:32 +03:00
$result -> closeCursor ();
if ( $count ) {
throw new \Sabre\DAV\Exception\BadRequest ( 'VCard object with uid already exists in this addressbook collection.' );
}
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( 'cards' )
-> values ([
2016-02-29 11:44:40 +03:00
'carddata' => $query -> createNamedParameter ( $cardData , IQueryBuilder :: PARAM_LOB ),
2015-10-30 18:05:25 +03:00
'uri' => $query -> createNamedParameter ( $cardUri ),
'lastmodified' => $query -> createNamedParameter ( time ()),
'addressbookid' => $query -> createNamedParameter ( $addressBookId ),
'size' => $query -> createNamedParameter ( strlen ( $cardData )),
'etag' => $query -> createNamedParameter ( $etag ),
2018-10-27 12:54:57 +03:00
'uid' => $query -> createNamedParameter ( $uid ),
2015-10-30 18:05:25 +03:00
])
-> execute ();
2020-07-07 15:54:26 +03:00
$etagCacheKey = " $addressBookId # $cardUri " ;
$this -> etagCache [ $etagCacheKey ] = $etag ;
2015-10-30 18:05:25 +03:00
$this -> addChange ( $addressBookId , $cardUri , 1 );
2015-11-25 17:24:50 +03:00
$this -> updateProperties ( $addressBookId , $cardUri , $cardData );
2015-10-30 18:05:25 +03:00
2020-07-28 10:35:51 +03:00
$addressBookData = $this -> getAddressBookById ( $addressBookId );
$shares = $this -> getShares ( $addressBookId );
$objectRow = $this -> getCard ( $addressBookId , $cardUri );
$this -> dispatcher -> dispatchTyped ( new CardCreatedEvent (( int ) $addressBookId , $addressBookData , $shares , $objectRow ));
$this -> legacyDispatcher -> dispatch ( '\OCA\DAV\CardDAV\CardDavBackend::createCard' ,
2017-05-02 15:32:26 +03:00
new GenericEvent ( null , [
'addressBookId' => $addressBookId ,
'cardUri' => $cardUri ,
'cardData' => $cardData ]));
2016-02-03 17:43:45 +03:00
2015-10-30 18:05:25 +03:00
return '"' . $etag . '"' ;
}
/**
* Updates a card .
*
* The addressbook id will be passed as the first argument . This is the
* same id as it is returned from the getAddressBooksForUser method .
*
* The cardUri is a base uri , and doesn ' t include the full path . The
* cardData argument is the vcard body , and is passed as a string .
*
* It is possible to return an ETag from this method . This ETag should
* match that of the updated resource , and must be enclosed with double
* quotes ( that is : the string itself must contain the actual quotes ) .
*
* You should only return the ETag if you store the carddata as - is . If a
* subsequent GET request on the same card does not have the same body ,
* byte - by - byte and you did return an ETag here , clients tend to get
* confused .
*
* If you don ' t return an ETag , you can just return null .
*
* @ param mixed $addressBookId
* @ param string $cardUri
* @ param string $cardData
2015-11-20 18:42:34 +03:00
* @ return string
2015-10-30 18:05:25 +03:00
*/
2020-04-10 17:51:06 +03:00
public function updateCard ( $addressBookId , $cardUri , $cardData ) {
2018-10-27 12:54:57 +03:00
$uid = $this -> getUID ( $cardData );
2015-10-30 18:05:25 +03:00
$etag = md5 ( $cardData );
$query = $this -> db -> getQueryBuilder ();
2020-07-07 15:54:26 +03:00
// check for recently stored etag and stop if it is the same
$etagCacheKey = " $addressBookId # $cardUri " ;
if ( isset ( $this -> etagCache [ $etagCacheKey ]) && $this -> etagCache [ $etagCacheKey ] === $etag ) {
return '"' . $etag . '"' ;
}
2020-03-12 13:05:05 +03:00
$query -> update ( $this -> dbCardsTable )
2016-02-29 11:44:40 +03:00
-> set ( 'carddata' , $query -> createNamedParameter ( $cardData , IQueryBuilder :: PARAM_LOB ))
2015-10-30 18:05:25 +03:00
-> set ( 'lastmodified' , $query -> createNamedParameter ( time ()))
-> set ( 'size' , $query -> createNamedParameter ( strlen ( $cardData )))
-> set ( 'etag' , $query -> createNamedParameter ( $etag ))
2018-10-27 12:54:57 +03:00
-> set ( 'uid' , $query -> createNamedParameter ( $uid ))
2015-10-30 18:05:25 +03:00
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $cardUri )))
-> andWhere ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )))
-> execute ();
2020-07-07 15:54:26 +03:00
$this -> etagCache [ $etagCacheKey ] = $etag ;
2015-10-30 18:05:25 +03:00
$this -> addChange ( $addressBookId , $cardUri , 2 );
2015-11-25 17:24:50 +03:00
$this -> updateProperties ( $addressBookId , $cardUri , $cardData );
2015-10-30 18:05:25 +03:00
2020-07-28 10:35:51 +03:00
$addressBookData = $this -> getAddressBookById ( $addressBookId );
$shares = $this -> getShares ( $addressBookId );
$objectRow = $this -> getCard ( $addressBookId , $cardUri );
$this -> dispatcher -> dispatchTyped ( new CardUpdatedEvent (( int ) $addressBookId , $addressBookData , $shares , $objectRow ));
$this -> legacyDispatcher -> dispatch ( '\OCA\DAV\CardDAV\CardDavBackend::updateCard' ,
2017-05-02 15:32:26 +03:00
new GenericEvent ( null , [
'addressBookId' => $addressBookId ,
'cardUri' => $cardUri ,
'cardData' => $cardData ]));
2016-02-03 17:43:45 +03:00
2015-10-30 18:05:25 +03:00
return '"' . $etag . '"' ;
}
/**
* Deletes a card
*
* @ param mixed $addressBookId
* @ param string $cardUri
* @ return bool
*/
2020-04-10 17:51:06 +03:00
public function deleteCard ( $addressBookId , $cardUri ) {
2020-07-28 10:35:51 +03:00
$addressBookData = $this -> getAddressBookById ( $addressBookId );
$shares = $this -> getShares ( $addressBookId );
$objectRow = $this -> getCard ( $addressBookId , $cardUri );
2016-01-21 14:19:28 +03:00
try {
2016-02-15 16:13:04 +03:00
$cardId = $this -> getCardId ( $addressBookId , $cardUri );
2016-01-21 14:19:28 +03:00
} catch ( \InvalidArgumentException $e ) {
$cardId = null ;
}
2015-10-30 18:05:25 +03:00
$query = $this -> db -> getQueryBuilder ();
2020-03-12 13:05:05 +03:00
$ret = $query -> delete ( $this -> dbCardsTable )
2015-10-30 18:05:25 +03:00
-> where ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )))
-> andWhere ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $cardUri )))
-> execute ();
$this -> addChange ( $addressBookId , $cardUri , 3 );
2015-11-25 17:24:50 +03:00
if ( $ret === 1 ) {
2016-01-21 14:19:28 +03:00
if ( $cardId !== null ) {
2020-07-28 10:35:51 +03:00
$this -> dispatcher -> dispatchTyped ( new CardDeletedEvent (( int ) $addressBookId , $addressBookData , $shares , $objectRow ));
$this -> legacyDispatcher -> dispatch ( '\OCA\DAV\CardDAV\CardDavBackend::deleteCard' ,
new GenericEvent ( null , [
'addressBookId' => $addressBookId ,
'cardUri' => $cardUri ]));
2016-01-21 14:19:28 +03:00
$this -> purgeProperties ( $addressBookId , $cardId );
}
2015-11-25 17:24:50 +03:00
return true ;
}
return false ;
2015-10-30 18:05:25 +03:00
}
/**
* The getChanges method returns all the changes that have happened , since
* the specified syncToken in the specified address book .
*
* This function should return an array , such as the following :
*
* [
* 'syncToken' => 'The current synctoken' ,
* 'added' => [
* 'new.txt' ,
* ],
* 'modified' => [
* 'modified.txt' ,
* ],
* 'deleted' => [
* 'foo.php.bak' ,
* 'old.txt'
* ]
* ];
*
* The returned syncToken property should reflect the * current * syncToken
* of the calendar , as reported in the { http :// sabredav . org / ns } sync - token
* property . This is needed here too , to ensure the operation is atomic .
*
* If the $syncToken argument is specified as null , this is an initial
* sync , and all members should be reported .
*
* The modified property is an array of nodenames that have changed since
* the last token .
*
* The deleted property is an array with nodenames , that have been deleted
* from collection .
*
* The $syncLevel argument is basically the 'depth' of the report . If it ' s
* 1 , you only have to report changes that happened only directly in
* immediate descendants . If it ' s 2 , it should also include changes from
* the nodes below the child collections . ( grandchildren )
*
* The $limit argument allows a client to specify how many results should
* be returned at most . If the limit is not specified , it should be treated
* as infinite .
*
* If the limit ( infinite or not ) is higher than you ' re willing to return ,
* you should throw a Sabre\DAV\Exception\TooMuchMatches () exception .
*
* If the syncToken is expired ( due to data cleanup ) or unknown , you must
* return null .
*
* The limit is 'suggestive' . You are free to ignore it .
*
* @ param string $addressBookId
* @ param string $syncToken
* @ param int $syncLevel
* @ param int $limit
* @ return array
*/
2020-04-10 17:51:06 +03:00
public function getChangesForAddressBook ( $addressBookId , $syncToken , $syncLevel , $limit = null ) {
2015-10-30 18:05:25 +03:00
// Current synctoken
$stmt = $this -> db -> prepare ( 'SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?' );
2020-07-30 22:18:20 +03:00
$stmt -> execute ([ $addressBookId ]);
2015-10-30 18:05:25 +03:00
$currentToken = $stmt -> fetchColumn ( 0 );
2020-04-10 15:19:56 +03:00
if ( is_null ( $currentToken )) {
return null ;
}
2015-10-30 18:05:25 +03:00
$result = [
'syncToken' => $currentToken ,
2020-07-30 22:18:20 +03:00
'added' => [],
'modified' => [],
'deleted' => [],
2015-10-30 18:05:25 +03:00
];
if ( $syncToken ) {
$query = " SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken` " ;
2020-07-30 22:18:20 +03:00
if ( $limit > 0 ) {
2018-08-24 18:23:14 +03:00
$query .= " LIMIT " . ( int ) $limit ;
2015-10-30 18:05:25 +03:00
}
// Fetching all changes
$stmt = $this -> db -> prepare ( $query );
$stmt -> execute ([ $syncToken , $currentToken , $addressBookId ]);
$changes = [];
// This loop ensures that any duplicates are overwritten, only the
// last change on a node is relevant.
2020-04-10 15:19:56 +03:00
while ( $row = $stmt -> fetch ( \PDO :: FETCH_ASSOC )) {
2015-10-30 18:05:25 +03:00
$changes [ $row [ 'uri' ]] = $row [ 'operation' ];
}
2020-04-10 15:19:56 +03:00
foreach ( $changes as $uri => $operation ) {
switch ( $operation ) {
2015-10-30 18:05:25 +03:00
case 1 :
$result [ 'added' ][] = $uri ;
break ;
case 2 :
$result [ 'modified' ][] = $uri ;
break ;
case 3 :
$result [ 'deleted' ][] = $uri ;
break ;
}
}
} else {
// No synctoken supplied, this is the initial sync.
$query = " SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ? " ;
$stmt = $this -> db -> prepare ( $query );
$stmt -> execute ([ $addressBookId ]);
$result [ 'added' ] = $stmt -> fetchAll ( \PDO :: FETCH_COLUMN );
}
return $result ;
}
/**
* Adds a change record to the addressbookchanges table .
*
* @ param mixed $addressBookId
* @ param string $objectUri
* @ param int $operation 1 = add , 2 = modify , 3 = delete
* @ return void
*/
protected function addChange ( $addressBookId , $objectUri , $operation ) {
$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?' ;
$stmt = $this -> db -> prepare ( $sql );
$stmt -> execute ([
$objectUri ,
$addressBookId ,
$operation ,
$addressBookId
]);
$stmt = $this -> db -> prepare ( 'UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?' );
$stmt -> execute ([
$addressBookId
]);
}
2020-04-15 16:47:27 +03:00
/**
* @ param resource | string $cardData
* @ param bool $modified
* @ return string
*/
2020-07-30 22:18:20 +03:00
private function readBlob ( $cardData , & $modified = false ) {
2015-10-30 18:05:25 +03:00
if ( is_resource ( $cardData )) {
2020-03-12 13:26:31 +03:00
$cardData = stream_get_contents ( $cardData );
2015-10-30 18:05:25 +03:00
}
2020-03-12 13:26:31 +03:00
$cardDataArray = explode ( " \r \n " , $cardData );
$cardDataFiltered = [];
$removingPhoto = false ;
foreach ( $cardDataArray as $line ) {
if ( strpos ( $line , 'PHOTO:data:' ) === 0
&& strpos ( $line , 'PHOTO:data:image/' ) !== 0 ) {
// Filter out PHOTO data of non-images
$removingPhoto = true ;
2020-04-15 16:47:27 +03:00
$modified = true ;
2020-03-12 13:26:31 +03:00
continue ;
}
if ( $removingPhoto ) {
if ( strpos ( $line , ' ' ) === 0 ) {
continue ;
}
// No leading space means this is a new property
$removingPhoto = false ;
}
$cardDataFiltered [] = $line ;
}
return implode ( " \r \n " , $cardDataFiltered );
2015-10-30 18:05:25 +03:00
}
2015-11-18 17:04:53 +03:00
/**
2016-01-26 00:49:26 +03:00
* @ param IShareable $shareable
2015-11-18 17:04:53 +03:00
* @ param string [] $add
* @ param string [] $remove
*/
2016-01-26 00:49:26 +03:00
public function updateShares ( IShareable $shareable , $add , $remove ) {
2020-07-28 10:35:51 +03:00
$addressBookId = $shareable -> getResourceId ();
$addressBookData = $this -> getAddressBookById ( $addressBookId );
$oldShares = $this -> getShares ( $addressBookId );
2016-01-26 00:49:26 +03:00
$this -> sharingBackend -> updateShares ( $shareable , $add , $remove );
2020-07-28 10:35:51 +03:00
$this -> dispatcher -> dispatchTyped ( new AddressBookShareUpdatedEvent ( $addressBookId , $addressBookData , $oldShares , $add , $remove ));
2015-11-05 18:46:37 +03:00
}
2015-11-25 17:24:50 +03:00
/**
2020-07-24 15:57:52 +03:00
* Search contacts in a specific address - book
2015-11-25 17:24:50 +03:00
*
* @ param int $addressBookId
* @ param string $pattern which should match within the $searchProperties
* @ param array $searchProperties defines the properties within the query pattern should match
2018-01-18 12:38:24 +03:00
* @ param array $options = array () to define the search behavior
2020-07-30 22:18:20 +03:00
* - 'escape_like_param' - If set to false wildcards _ and % are not escaped , otherwise they are
* - 'limit' - Set a numeric limit for the search results
* - 'offset' - Set the offset for the limited search results
2015-11-25 17:24:50 +03:00
* @ return array an array of contacts which are arrays of key - value - pairs
*/
2020-07-24 15:57:52 +03:00
public function search ( $addressBookId , $pattern , $searchProperties , $options = []) : array {
return $this -> searchByAddressBookIds ([ $addressBookId ], $pattern , $searchProperties , $options );
}
/**
* Search contacts in all address - books accessible by a user
*
* @ param string $principalUri
* @ param string $pattern
* @ param array $searchProperties
* @ param array $options
* @ return array
*/
public function searchPrincipalUri ( string $principalUri ,
string $pattern ,
array $searchProperties ,
array $options = []) : array {
$addressBookIds = array_map ( static function ( $row ) : int {
return ( int ) $row [ 'id' ];
}, $this -> getAddressBooksForUser ( $principalUri ));
return $this -> searchByAddressBookIds ( $addressBookIds , $pattern , $searchProperties , $options );
}
/**
* @ param array $addressBookIds
* @ param string $pattern
* @ param array $searchProperties
* @ param array $options
* @ return array
*/
private function searchByAddressBookIds ( array $addressBookIds ,
string $pattern ,
array $searchProperties ,
array $options = []) : array {
2020-06-04 11:23:23 +03:00
$escapePattern = ! \array_key_exists ( 'escape_like_param' , $options ) || $options [ 'escape_like_param' ] !== false ;
2017-03-10 11:40:57 +03:00
2020-06-04 11:23:23 +03:00
$query2 = $this -> db -> getQueryBuilder ();
2020-07-24 15:57:52 +03:00
$addressBookOr = $query2 -> expr () -> orX ();
foreach ( $addressBookIds as $addressBookId ) {
$addressBookOr -> add ( $query2 -> expr () -> eq ( 'cp.addressbookid' , $query2 -> createNamedParameter ( $addressBookId )));
}
if ( $addressBookOr -> count () === 0 ) {
return [];
}
$propertyOr = $query2 -> expr () -> orX ();
2015-11-25 17:24:50 +03:00
foreach ( $searchProperties as $property ) {
2020-06-04 11:23:23 +03:00
if ( $escapePattern ) {
if ( $property === 'EMAIL' && strpos ( $pattern , ' ' ) !== false ) {
// There can be no spaces in emails
continue ;
}
2020-07-30 22:10:54 +03:00
if ( $property === 'CLOUD' && preg_match ( '/[^a-zA-Z0-9 :_.@\/\-\']/' , $pattern ) === 1 ) {
// There can be no chars in cloud ids which are not valid for user ids plus :/
// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
2020-06-04 11:23:23 +03:00
continue ;
}
}
2020-07-24 15:57:52 +03:00
$propertyOr -> add ( $query2 -> expr () -> eq ( 'cp.name' , $query2 -> createNamedParameter ( $property )));
2015-11-25 17:24:50 +03:00
}
2020-06-04 11:23:23 +03:00
2020-07-24 15:57:52 +03:00
if ( $propertyOr -> count () === 0 ) {
2020-06-04 11:23:23 +03:00
return [];
}
$query2 -> selectDistinct ( 'cp.cardid' )
-> from ( $this -> dbCardsPropertiesTable , 'cp' )
2020-07-24 15:57:52 +03:00
-> andWhere ( $addressBookOr )
-> andWhere ( $propertyOr );
2017-09-24 16:24:08 +03:00
// No need for like when the pattern is empty
if ( '' !== $pattern ) {
2020-06-04 11:23:23 +03:00
if ( ! $escapePattern ) {
2020-06-04 11:05:28 +03:00
$query2 -> andWhere ( $query2 -> expr () -> ilike ( 'cp.value' , $query2 -> createNamedParameter ( $pattern )));
2018-01-18 12:38:24 +03:00
} else {
2020-06-04 11:05:28 +03:00
$query2 -> andWhere ( $query2 -> expr () -> ilike ( 'cp.value' , $query2 -> createNamedParameter ( '%' . $this -> db -> escapeLikeParameter ( $pattern ) . '%' )));
2018-01-18 12:38:24 +03:00
}
2017-09-24 16:24:08 +03:00
}
2020-06-04 11:05:28 +03:00
if ( isset ( $options [ 'limit' ])) {
$query2 -> setMaxResults ( $options [ 'limit' ]);
}
if ( isset ( $options [ 'offset' ])) {
$query2 -> setFirstResult ( $options [ 'offset' ]);
}
$result = $query2 -> execute ();
$matches = $result -> fetchAll ();
$result -> closeCursor ();
$matches = array_map ( function ( $match ) {
2020-07-30 22:18:20 +03:00
return ( int ) $match [ 'cardid' ];
2020-06-04 11:05:28 +03:00
}, $matches );
$query = $this -> db -> getQueryBuilder ();
2020-07-24 15:57:52 +03:00
$query -> select ( 'c.addressbookid' , 'c.carddata' , 'c.uri' )
2020-06-04 11:05:28 +03:00
-> from ( $this -> dbCardsTable , 'c' )
-> where ( $query -> expr () -> in ( 'c.id' , $query -> createNamedParameter ( $matches , IQueryBuilder :: PARAM_INT_ARRAY )));
2015-12-14 15:51:12 +03:00
$result = $query -> execute ();
2015-11-25 17:24:50 +03:00
$cards = $result -> fetchAll ();
2015-12-14 15:51:12 +03:00
2015-11-25 17:24:50 +03:00
$result -> closeCursor ();
2020-04-09 14:53:40 +03:00
return array_map ( function ( $array ) {
2020-07-24 15:57:52 +03:00
$array [ 'addressbookid' ] = ( int ) $array [ 'addressbookid' ];
2020-04-15 16:47:27 +03:00
$modified = false ;
$array [ 'carddata' ] = $this -> readBlob ( $array [ 'carddata' ], $modified );
if ( $modified ) {
$array [ 'size' ] = strlen ( $array [ 'carddata' ]);
}
2016-06-21 16:25:44 +03:00
return $array ;
}, $cards );
2015-11-25 17:24:50 +03:00
}
2016-02-10 19:06:13 +03:00
/**
* @ param int $bookId
* @ param string $name
* @ return array
*/
public function collectCardProperties ( $bookId , $name ) {
$query = $this -> db -> getQueryBuilder ();
$result = $query -> selectDistinct ( 'value' )
-> from ( $this -> dbCardsPropertiesTable )
-> where ( $query -> expr () -> eq ( 'name' , $query -> createNamedParameter ( $name )))
-> andWhere ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $bookId )))
-> execute ();
$all = $result -> fetchAll ( PDO :: FETCH_COLUMN );
$result -> closeCursor ();
return $all ;
}
2015-11-25 17:24:50 +03:00
/**
* get URI from a given contact
*
* @ param int $id
* @ return string
*/
public function getCardUri ( $id ) {
$query = $this -> db -> getQueryBuilder ();
$query -> select ( 'uri' ) -> from ( $this -> dbCardsTable )
2020-07-30 22:18:20 +03:00
-> where ( $query -> expr () -> eq ( 'id' , $query -> createParameter ( 'id' )))
-> setParameter ( 'id' , $id );
2015-11-25 17:24:50 +03:00
$result = $query -> execute ();
$uri = $result -> fetch ();
$result -> closeCursor ();
if ( ! isset ( $uri [ 'uri' ])) {
throw new \InvalidArgumentException ( 'Card does not exists: ' . $id );
}
return $uri [ 'uri' ];
}
/**
* return contact with the given URI
*
2016-02-15 16:13:04 +03:00
* @ param int $addressBookId
2015-11-25 17:24:50 +03:00
* @ param string $uri
* @ returns array
*/
2016-02-15 16:13:04 +03:00
public function getContact ( $addressBookId , $uri ) {
2015-11-25 17:24:50 +03:00
$result = [];
$query = $this -> db -> getQueryBuilder ();
$query -> select ( '*' ) -> from ( $this -> dbCardsTable )
2020-07-30 22:18:20 +03:00
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $uri )))
-> andWhere ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )));
2015-11-25 17:24:50 +03:00
$queryResult = $query -> execute ();
$contact = $queryResult -> fetch ();
$queryResult -> closeCursor ();
if ( is_array ( $contact )) {
2020-04-15 16:47:27 +03:00
$modified = false ;
2020-03-12 13:25:47 +03:00
$contact [ 'etag' ] = '"' . $contact [ 'etag' ] . '"' ;
2020-04-15 16:47:27 +03:00
$contact [ 'carddata' ] = $this -> readBlob ( $contact [ 'carddata' ], $modified );
if ( $modified ) {
$contact [ 'size' ] = strlen ( $contact [ 'carddata' ]);
}
2015-11-25 17:24:50 +03:00
$result = $contact ;
}
return $result ;
}
2015-11-05 18:46:37 +03:00
/**
2015-11-18 17:04:53 +03:00
* Returns the list of people whom this address book is shared with .
2015-11-05 18:46:37 +03:00
*
* Every element in this array should have the following properties :
* * href - Often a mailto : address
* * commonName - Optional , for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin :: STATUS_ constants .
* * readOnly - boolean
* * summary - Optional , a description for the share
*
* @ return array
*/
2016-01-12 15:23:50 +03:00
public function getShares ( $addressBookId ) {
2016-01-26 00:49:26 +03:00
return $this -> sharingBackend -> getShares ( $addressBookId );
2015-11-05 18:46:37 +03:00
}
2015-11-25 17:24:50 +03:00
/**
* update properties table
*
* @ param int $addressBookId
* @ param string $cardUri
* @ param string $vCardSerialized
*/
protected function updateProperties ( $addressBookId , $cardUri , $vCardSerialized ) {
2016-02-15 16:13:04 +03:00
$cardId = $this -> getCardId ( $addressBookId , $cardUri );
2015-11-25 17:24:50 +03:00
$vCard = $this -> readCard ( $vCardSerialized );
$this -> purgeProperties ( $addressBookId , $cardId );
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( $this -> dbCardsPropertiesTable )
-> values (
[
'addressbookid' => $query -> createNamedParameter ( $addressBookId ),
'cardid' => $query -> createNamedParameter ( $cardId ),
'name' => $query -> createParameter ( 'name' ),
'value' => $query -> createParameter ( 'value' ),
'preferred' => $query -> createParameter ( 'preferred' )
]
);
2016-10-13 13:15:10 +03:00
foreach ( $vCard -> children () as $property ) {
2020-04-10 15:19:56 +03:00
if ( ! in_array ( $property -> name , self :: $indexProperties )) {
2015-11-25 17:24:50 +03:00
continue ;
}
$preferred = 0 ;
2020-04-10 15:19:56 +03:00
foreach ( $property -> parameters as $parameter ) {
2017-05-10 15:03:14 +03:00
if ( $parameter -> name === 'TYPE' && strtoupper ( $parameter -> getValue ()) === 'PREF' ) {
2015-11-25 17:24:50 +03:00
$preferred = 1 ;
break ;
}
}
$query -> setParameter ( 'name' , $property -> name );
2020-02-25 11:44:58 +03:00
$query -> setParameter ( 'value' , mb_substr ( $property -> getValue (), 0 , 254 ));
2015-11-25 17:24:50 +03:00
$query -> setParameter ( 'preferred' , $preferred );
$query -> execute ();
}
}
/**
* read vCard data into a vCard object
*
* @ param string $cardData
* @ return VCard
*/
protected function readCard ( $cardData ) {
2020-07-30 22:18:20 +03:00
return Reader :: read ( $cardData );
2015-11-25 17:24:50 +03:00
}
/**
* delete all properties from a given card
*
* @ param int $addressBookId
* @ param int $cardId
*/
protected function purgeProperties ( $addressBookId , $cardId ) {
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( $this -> dbCardsPropertiesTable )
-> where ( $query -> expr () -> eq ( 'cardid' , $query -> createNamedParameter ( $cardId )))
-> andWhere ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )));
$query -> execute ();
}
/**
* get ID from a given contact
*
2016-02-15 16:13:04 +03:00
* @ param int $addressBookId
2015-11-25 17:24:50 +03:00
* @ param string $uri
* @ return int
*/
2016-02-15 16:13:04 +03:00
protected function getCardId ( $addressBookId , $uri ) {
2015-11-25 17:24:50 +03:00
$query = $this -> db -> getQueryBuilder ();
$query -> select ( 'id' ) -> from ( $this -> dbCardsTable )
2016-02-15 16:13:04 +03:00
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $uri )))
-> andWhere ( $query -> expr () -> eq ( 'addressbookid' , $query -> createNamedParameter ( $addressBookId )));
2015-11-25 17:24:50 +03:00
$result = $query -> execute ();
$cardIds = $result -> fetch ();
$result -> closeCursor ();
if ( ! isset ( $cardIds [ 'id' ])) {
throw new \InvalidArgumentException ( 'Card does not exists: ' . $uri );
}
return ( int ) $cardIds [ 'id' ];
}
2016-01-12 15:23:50 +03:00
/**
2016-01-13 13:55:50 +03:00
* For shared address books the sharee is set in the ACL of the address book
2020-07-30 22:18:20 +03:00
*
2016-01-12 15:23:50 +03:00
* @ param $addressBookId
* @ param $acl
* @ return array
*/
public function applyShareAcl ( $addressBookId , $acl ) {
2016-01-26 14:06:02 +03:00
return $this -> sharingBackend -> applyShareAcl ( $addressBookId , $acl );
2016-01-12 15:23:50 +03:00
}
2016-01-20 23:08:23 +03:00
private function convertPrincipal ( $principalUri , $toV2 ) {
if ( $this -> principalBackend -> getPrincipalPrefix () === 'principals' ) {
2017-07-20 10:43:23 +03:00
list (, $name ) = \Sabre\Uri\split ( $principalUri );
2016-01-20 23:08:23 +03:00
if ( $toV2 === true ) {
return " principals/users/ $name " ;
}
return " principals/ $name " ;
}
return $principalUri ;
}
2017-04-19 17:18:44 +03:00
private function addOwnerPrincipal ( & $addressbookInfo ) {
$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}owner-principal' ;
$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_NEXTCLOUD . '}owner-displayname' ;
if ( isset ( $addressbookInfo [ $ownerPrincipalKey ])) {
$uri = $addressbookInfo [ $ownerPrincipalKey ];
} else {
$uri = $addressbookInfo [ 'principaluri' ];
}
$principalInformation = $this -> principalBackend -> getPrincipalByPath ( $uri );
if ( isset ( $principalInformation [ '{DAV:}displayname' ])) {
$addressbookInfo [ $displaynameKey ] = $principalInformation [ '{DAV:}displayname' ];
}
}
2018-10-27 12:54:57 +03:00
2018-10-30 15:03:49 +03:00
/**
* Extract UID from vcard
*
* @ param string $cardData the vcard raw data
2018-11-01 16:44:35 +03:00
* @ return string the uid
* @ throws BadRequest if no UID is available
2018-10-30 15:03:49 +03:00
*/
2018-10-27 12:54:57 +03:00
private function getUID ( $cardData ) {
2018-11-02 16:20:53 +03:00
if ( $cardData != '' ) {
$vCard = Reader :: read ( $cardData );
if ( $vCard -> UID ) {
$uid = $vCard -> UID -> getValue ();
return $uid ;
}
// should already be handled, but just in case
throw new BadRequest ( 'vCards on CardDAV servers MUST have a UID property' );
2018-10-30 15:03:49 +03:00
}
// should already be handled, but just in case
2018-11-02 16:20:53 +03:00
throw new BadRequest ( 'vCard can not be empty' );
2018-10-27 12:54:57 +03:00
}
2015-10-30 18:05:25 +03:00
}