CardDavBackend.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  1. <?php
  2. /**
  3. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  4. * @author Björn Schießle <bjoern@schiessle.org>
  5. * @author Joas Schilling <nickvergessen@owncloud.com>
  6. * @author Stefan Weil <sw@weilnetz.de>
  7. * @author Thomas Müller <thomas.mueller@tmit.eu>
  8. *
  9. * @copyright Copyright (c) 2016, ownCloud, Inc.
  10. * @license AGPL-3.0
  11. *
  12. * This code is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License, version 3,
  14. * as published by the Free Software Foundation.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License, version 3,
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>
  23. *
  24. */
  25. namespace OCA\DAV\CardDAV;
  26. use OCA\DAV\Connector\Sabre\Principal;
  27. use OCP\DB\QueryBuilder\IQueryBuilder;
  28. use OCA\DAV\DAV\Sharing\Backend;
  29. use OCA\DAV\DAV\Sharing\IShareable;
  30. use OCP\IDBConnection;
  31. use PDO;
  32. use Sabre\CardDAV\Backend\BackendInterface;
  33. use Sabre\CardDAV\Backend\SyncSupport;
  34. use Sabre\CardDAV\Plugin;
  35. use Sabre\DAV\Exception\BadRequest;
  36. use Sabre\HTTP\URLUtil;
  37. use Sabre\VObject\Component\VCard;
  38. use Sabre\VObject\Reader;
  39. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  40. use Symfony\Component\EventDispatcher\GenericEvent;
  41. class CardDavBackend implements BackendInterface, SyncSupport {
  42. /** @var Principal */
  43. private $principalBackend;
  44. /** @var string */
  45. private $dbCardsTable = 'cards';
  46. /** @var string */
  47. private $dbCardsPropertiesTable = 'cards_properties';
  48. /** @var IDBConnection */
  49. private $db;
  50. /** @var Backend */
  51. private $sharingBackend;
  52. /** @var array properties to index */
  53. public static $indexProperties = array(
  54. 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
  55. 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD');
  56. /** @var EventDispatcherInterface */
  57. private $dispatcher;
  58. /**
  59. * CardDavBackend constructor.
  60. *
  61. * @param IDBConnection $db
  62. * @param Principal $principalBackend
  63. * @param EventDispatcherInterface $dispatcher
  64. */
  65. public function __construct(IDBConnection $db,
  66. Principal $principalBackend,
  67. EventDispatcherInterface $dispatcher = null) {
  68. $this->db = $db;
  69. $this->principalBackend = $principalBackend;
  70. $this->dispatcher = $dispatcher;
  71. $this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook');
  72. }
  73. /**
  74. * Returns the list of address books for a specific user.
  75. *
  76. * Every addressbook should have the following properties:
  77. * id - an arbitrary unique id
  78. * uri - the 'basename' part of the url
  79. * principaluri - Same as the passed parameter
  80. *
  81. * Any additional clark-notation property may be passed besides this. Some
  82. * common ones are :
  83. * {DAV:}displayname
  84. * {urn:ietf:params:xml:ns:carddav}addressbook-description
  85. * {http://calendarserver.org/ns/}getctag
  86. *
  87. * @param string $principalUri
  88. * @return array
  89. */
  90. function getAddressBooksForUser($principalUri) {
  91. $principalUriOriginal = $principalUri;
  92. $principalUri = $this->convertPrincipal($principalUri, true);
  93. $query = $this->db->getQueryBuilder();
  94. $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
  95. ->from('addressbooks')
  96. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
  97. $addressBooks = [];
  98. $result = $query->execute();
  99. while($row = $result->fetch()) {
  100. $addressBooks[$row['id']] = [
  101. 'id' => $row['id'],
  102. 'uri' => $row['uri'],
  103. 'principaluri' => $this->convertPrincipal($row['principaluri'], false),
  104. '{DAV:}displayname' => $row['displayname'],
  105. '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
  106. '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
  107. '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
  108. ];
  109. }
  110. $result->closeCursor();
  111. // query for shared calendars
  112. $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
  113. $principals[]= $principalUri;
  114. $query = $this->db->getQueryBuilder();
  115. $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
  116. ->from('dav_shares', 's')
  117. ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
  118. ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
  119. ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
  120. ->setParameter('type', 'addressbook')
  121. ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
  122. ->execute();
  123. while($row = $result->fetch()) {
  124. list(, $name) = URLUtil::splitPath($row['principaluri']);
  125. $uri = $row['uri'] . '_shared_by_' . $name;
  126. $displayName = $row['displayname'] . "($name)";
  127. if (!isset($addressBooks[$row['id']])) {
  128. $addressBooks[$row['id']] = [
  129. 'id' => $row['id'],
  130. 'uri' => $uri,
  131. 'principaluri' => $principalUri,
  132. '{DAV:}displayname' => $displayName,
  133. '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
  134. '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
  135. '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
  136. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
  137. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
  138. ];
  139. }
  140. }
  141. $result->closeCursor();
  142. return array_values($addressBooks);
  143. }
  144. /**
  145. * @param int $addressBookId
  146. */
  147. public function getAddressBookById($addressBookId) {
  148. $query = $this->db->getQueryBuilder();
  149. $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
  150. ->from('addressbooks')
  151. ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
  152. ->execute();
  153. $row = $result->fetch();
  154. $result->closeCursor();
  155. if ($row === false) {
  156. return null;
  157. }
  158. return [
  159. 'id' => $row['id'],
  160. 'uri' => $row['uri'],
  161. 'principaluri' => $row['principaluri'],
  162. '{DAV:}displayname' => $row['displayname'],
  163. '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
  164. '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
  165. '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
  166. ];
  167. }
  168. /**
  169. * @param $addressBookUri
  170. * @return array|null
  171. */
  172. public function getAddressBooksByUri($principal, $addressBookUri) {
  173. $query = $this->db->getQueryBuilder();
  174. $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
  175. ->from('addressbooks')
  176. ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
  177. ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
  178. ->setMaxResults(1)
  179. ->execute();
  180. $row = $result->fetch();
  181. $result->closeCursor();
  182. if ($row === false) {
  183. return null;
  184. }
  185. return [
  186. 'id' => $row['id'],
  187. 'uri' => $row['uri'],
  188. 'principaluri' => $row['principaluri'],
  189. '{DAV:}displayname' => $row['displayname'],
  190. '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
  191. '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
  192. '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
  193. ];
  194. }
  195. /**
  196. * Updates properties for an address book.
  197. *
  198. * The list of mutations is stored in a Sabre\DAV\PropPatch object.
  199. * To do the actual updates, you must tell this object which properties
  200. * you're going to process with the handle() method.
  201. *
  202. * Calling the handle method is like telling the PropPatch object "I
  203. * promise I can handle updating this property".
  204. *
  205. * Read the PropPatch documentation for more info and examples.
  206. *
  207. * @param string $addressBookId
  208. * @param \Sabre\DAV\PropPatch $propPatch
  209. * @return void
  210. */
  211. function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
  212. $supportedProperties = [
  213. '{DAV:}displayname',
  214. '{' . Plugin::NS_CARDDAV . '}addressbook-description',
  215. ];
  216. $propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
  217. $updates = [];
  218. foreach($mutations as $property=>$newValue) {
  219. switch($property) {
  220. case '{DAV:}displayname' :
  221. $updates['displayname'] = $newValue;
  222. break;
  223. case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
  224. $updates['description'] = $newValue;
  225. break;
  226. }
  227. }
  228. $query = $this->db->getQueryBuilder();
  229. $query->update('addressbooks');
  230. foreach($updates as $key=>$value) {
  231. $query->set($key, $query->createNamedParameter($value));
  232. }
  233. $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
  234. ->execute();
  235. $this->addChange($addressBookId, "", 2);
  236. return true;
  237. });
  238. }
  239. /**
  240. * Creates a new address book
  241. *
  242. * @param string $principalUri
  243. * @param string $url Just the 'basename' of the url.
  244. * @param array $properties
  245. * @return int
  246. * @throws BadRequest
  247. */
  248. function createAddressBook($principalUri, $url, array $properties) {
  249. $values = [
  250. 'displayname' => null,
  251. 'description' => null,
  252. 'principaluri' => $principalUri,
  253. 'uri' => $url,
  254. 'synctoken' => 1
  255. ];
  256. foreach($properties as $property=>$newValue) {
  257. switch($property) {
  258. case '{DAV:}displayname' :
  259. $values['displayname'] = $newValue;
  260. break;
  261. case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
  262. $values['description'] = $newValue;
  263. break;
  264. default :
  265. throw new BadRequest('Unknown property: ' . $property);
  266. }
  267. }
  268. // Fallback to make sure the displayname is set. Some clients may refuse
  269. // to work with addressbooks not having a displayname.
  270. if(is_null($values['displayname'])) {
  271. $values['displayname'] = $url;
  272. }
  273. $query = $this->db->getQueryBuilder();
  274. $query->insert('addressbooks')
  275. ->values([
  276. 'uri' => $query->createParameter('uri'),
  277. 'displayname' => $query->createParameter('displayname'),
  278. 'description' => $query->createParameter('description'),
  279. 'principaluri' => $query->createParameter('principaluri'),
  280. 'synctoken' => $query->createParameter('synctoken'),
  281. ])
  282. ->setParameters($values)
  283. ->execute();
  284. return $query->getLastInsertId();
  285. }
  286. /**
  287. * Deletes an entire addressbook and all its contents
  288. *
  289. * @param mixed $addressBookId
  290. * @return void
  291. */
  292. function deleteAddressBook($addressBookId) {
  293. $query = $this->db->getQueryBuilder();
  294. $query->delete('cards')
  295. ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
  296. ->setParameter('addressbookid', $addressBookId)
  297. ->execute();
  298. $query->delete('addressbookchanges')
  299. ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
  300. ->setParameter('addressbookid', $addressBookId)
  301. ->execute();
  302. $query->delete('addressbooks')
  303. ->where($query->expr()->eq('id', $query->createParameter('id')))
  304. ->setParameter('id', $addressBookId)
  305. ->execute();
  306. $this->sharingBackend->deleteAllShares($addressBookId);
  307. $query->delete($this->dbCardsPropertiesTable)
  308. ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
  309. ->execute();
  310. }
  311. /**
  312. * Returns all cards for a specific addressbook id.
  313. *
  314. * This method should return the following properties for each card:
  315. * * carddata - raw vcard data
  316. * * uri - Some unique url
  317. * * lastmodified - A unix timestamp
  318. *
  319. * It's recommended to also return the following properties:
  320. * * etag - A unique etag. This must change every time the card changes.
  321. * * size - The size of the card in bytes.
  322. *
  323. * If these last two properties are provided, less time will be spent
  324. * calculating them. If they are specified, you can also ommit carddata.
  325. * This may speed up certain requests, especially with large cards.
  326. *
  327. * @param mixed $addressBookId
  328. * @return array
  329. */
  330. function getCards($addressBookId) {
  331. $query = $this->db->getQueryBuilder();
  332. $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
  333. ->from('cards')
  334. ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
  335. $cards = [];
  336. $result = $query->execute();
  337. while($row = $result->fetch()) {
  338. $row['etag'] = '"' . $row['etag'] . '"';
  339. $row['carddata'] = $this->readBlob($row['carddata']);
  340. $cards[] = $row;
  341. }
  342. $result->closeCursor();
  343. return $cards;
  344. }
  345. /**
  346. * Returns a specific card.
  347. *
  348. * The same set of properties must be returned as with getCards. The only
  349. * exception is that 'carddata' is absolutely required.
  350. *
  351. * If the card does not exist, you must return false.
  352. *
  353. * @param mixed $addressBookId
  354. * @param string $cardUri
  355. * @return array
  356. */
  357. function getCard($addressBookId, $cardUri) {
  358. $query = $this->db->getQueryBuilder();
  359. $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
  360. ->from('cards')
  361. ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
  362. ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
  363. ->setMaxResults(1);
  364. $result = $query->execute();
  365. $row = $result->fetch();
  366. if (!$row) {
  367. return false;
  368. }
  369. $row['etag'] = '"' . $row['etag'] . '"';
  370. $row['carddata'] = $this->readBlob($row['carddata']);
  371. return $row;
  372. }
  373. /**
  374. * Returns a list of cards.
  375. *
  376. * This method should work identical to getCard, but instead return all the
  377. * cards in the list as an array.
  378. *
  379. * If the backend supports this, it may allow for some speed-ups.
  380. *
  381. * @param mixed $addressBookId
  382. * @param string[] $uris
  383. * @return array
  384. */
  385. function getMultipleCards($addressBookId, array $uris) {
  386. $query = $this->db->getQueryBuilder();
  387. $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
  388. ->from('cards')
  389. ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
  390. ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
  391. ->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
  392. $cards = [];
  393. $result = $query->execute();
  394. while($row = $result->fetch()) {
  395. $row['etag'] = '"' . $row['etag'] . '"';
  396. $row['carddata'] = $this->readBlob($row['carddata']);
  397. $cards[] = $row;
  398. }
  399. $result->closeCursor();
  400. return $cards;
  401. }
  402. /**
  403. * Creates a new card.
  404. *
  405. * The addressbook id will be passed as the first argument. This is the
  406. * same id as it is returned from the getAddressBooksForUser method.
  407. *
  408. * The cardUri is a base uri, and doesn't include the full path. The
  409. * cardData argument is the vcard body, and is passed as a string.
  410. *
  411. * It is possible to return an ETag from this method. This ETag is for the
  412. * newly created resource, and must be enclosed with double quotes (that
  413. * is, the string itself must contain the double quotes).
  414. *
  415. * You should only return the ETag if you store the carddata as-is. If a
  416. * subsequent GET request on the same card does not have the same body,
  417. * byte-by-byte and you did return an ETag here, clients tend to get
  418. * confused.
  419. *
  420. * If you don't return an ETag, you can just return null.
  421. *
  422. * @param mixed $addressBookId
  423. * @param string $cardUri
  424. * @param string $cardData
  425. * @return string
  426. */
  427. function createCard($addressBookId, $cardUri, $cardData) {
  428. $etag = md5($cardData);
  429. $query = $this->db->getQueryBuilder();
  430. $query->insert('cards')
  431. ->values([
  432. 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
  433. 'uri' => $query->createNamedParameter($cardUri),
  434. 'lastmodified' => $query->createNamedParameter(time()),
  435. 'addressbookid' => $query->createNamedParameter($addressBookId),
  436. 'size' => $query->createNamedParameter(strlen($cardData)),
  437. 'etag' => $query->createNamedParameter($etag),
  438. ])
  439. ->execute();
  440. $this->addChange($addressBookId, $cardUri, 1);
  441. $this->updateProperties($addressBookId, $cardUri, $cardData);
  442. if (!is_null($this->dispatcher)) {
  443. $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
  444. new GenericEvent(null, [
  445. 'addressBookId' => $addressBookId,
  446. 'cardUri' => $cardUri,
  447. 'cardData' => $cardData]));
  448. }
  449. return '"' . $etag . '"';
  450. }
  451. /**
  452. * Updates a card.
  453. *
  454. * The addressbook id will be passed as the first argument. This is the
  455. * same id as it is returned from the getAddressBooksForUser method.
  456. *
  457. * The cardUri is a base uri, and doesn't include the full path. The
  458. * cardData argument is the vcard body, and is passed as a string.
  459. *
  460. * It is possible to return an ETag from this method. This ETag should
  461. * match that of the updated resource, and must be enclosed with double
  462. * quotes (that is: the string itself must contain the actual quotes).
  463. *
  464. * You should only return the ETag if you store the carddata as-is. If a
  465. * subsequent GET request on the same card does not have the same body,
  466. * byte-by-byte and you did return an ETag here, clients tend to get
  467. * confused.
  468. *
  469. * If you don't return an ETag, you can just return null.
  470. *
  471. * @param mixed $addressBookId
  472. * @param string $cardUri
  473. * @param string $cardData
  474. * @return string
  475. */
  476. function updateCard($addressBookId, $cardUri, $cardData) {
  477. $etag = md5($cardData);
  478. $query = $this->db->getQueryBuilder();
  479. $query->update('cards')
  480. ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
  481. ->set('lastmodified', $query->createNamedParameter(time()))
  482. ->set('size', $query->createNamedParameter(strlen($cardData)))
  483. ->set('etag', $query->createNamedParameter($etag))
  484. ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
  485. ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
  486. ->execute();
  487. $this->addChange($addressBookId, $cardUri, 2);
  488. $this->updateProperties($addressBookId, $cardUri, $cardData);
  489. if (!is_null($this->dispatcher)) {
  490. $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
  491. new GenericEvent(null, [
  492. 'addressBookId' => $addressBookId,
  493. 'cardUri' => $cardUri,
  494. 'cardData' => $cardData]));
  495. }
  496. return '"' . $etag . '"';
  497. }
  498. /**
  499. * Deletes a card
  500. *
  501. * @param mixed $addressBookId
  502. * @param string $cardUri
  503. * @return bool
  504. */
  505. function deleteCard($addressBookId, $cardUri) {
  506. try {
  507. $cardId = $this->getCardId($addressBookId, $cardUri);
  508. } catch (\InvalidArgumentException $e) {
  509. $cardId = null;
  510. }
  511. $query = $this->db->getQueryBuilder();
  512. $ret = $query->delete('cards')
  513. ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
  514. ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
  515. ->execute();
  516. $this->addChange($addressBookId, $cardUri, 3);
  517. if (!is_null($this->dispatcher)) {
  518. $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
  519. new GenericEvent(null, [
  520. 'addressBookId' => $addressBookId,
  521. 'cardUri' => $cardUri]));
  522. }
  523. if ($ret === 1) {
  524. if ($cardId !== null) {
  525. $this->purgeProperties($addressBookId, $cardId);
  526. }
  527. return true;
  528. }
  529. return false;
  530. }
  531. /**
  532. * The getChanges method returns all the changes that have happened, since
  533. * the specified syncToken in the specified address book.
  534. *
  535. * This function should return an array, such as the following:
  536. *
  537. * [
  538. * 'syncToken' => 'The current synctoken',
  539. * 'added' => [
  540. * 'new.txt',
  541. * ],
  542. * 'modified' => [
  543. * 'modified.txt',
  544. * ],
  545. * 'deleted' => [
  546. * 'foo.php.bak',
  547. * 'old.txt'
  548. * ]
  549. * ];
  550. *
  551. * The returned syncToken property should reflect the *current* syncToken
  552. * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
  553. * property. This is needed here too, to ensure the operation is atomic.
  554. *
  555. * If the $syncToken argument is specified as null, this is an initial
  556. * sync, and all members should be reported.
  557. *
  558. * The modified property is an array of nodenames that have changed since
  559. * the last token.
  560. *
  561. * The deleted property is an array with nodenames, that have been deleted
  562. * from collection.
  563. *
  564. * The $syncLevel argument is basically the 'depth' of the report. If it's
  565. * 1, you only have to report changes that happened only directly in
  566. * immediate descendants. If it's 2, it should also include changes from
  567. * the nodes below the child collections. (grandchildren)
  568. *
  569. * The $limit argument allows a client to specify how many results should
  570. * be returned at most. If the limit is not specified, it should be treated
  571. * as infinite.
  572. *
  573. * If the limit (infinite or not) is higher than you're willing to return,
  574. * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
  575. *
  576. * If the syncToken is expired (due to data cleanup) or unknown, you must
  577. * return null.
  578. *
  579. * The limit is 'suggestive'. You are free to ignore it.
  580. *
  581. * @param string $addressBookId
  582. * @param string $syncToken
  583. * @param int $syncLevel
  584. * @param int $limit
  585. * @return array
  586. */
  587. function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
  588. // Current synctoken
  589. $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
  590. $stmt->execute([ $addressBookId ]);
  591. $currentToken = $stmt->fetchColumn(0);
  592. if (is_null($currentToken)) return null;
  593. $result = [
  594. 'syncToken' => $currentToken,
  595. 'added' => [],
  596. 'modified' => [],
  597. 'deleted' => [],
  598. ];
  599. if ($syncToken) {
  600. $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
  601. if ($limit>0) {
  602. $query .= " `LIMIT` " . (int)$limit;
  603. }
  604. // Fetching all changes
  605. $stmt = $this->db->prepare($query);
  606. $stmt->execute([$syncToken, $currentToken, $addressBookId]);
  607. $changes = [];
  608. // This loop ensures that any duplicates are overwritten, only the
  609. // last change on a node is relevant.
  610. while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  611. $changes[$row['uri']] = $row['operation'];
  612. }
  613. foreach($changes as $uri => $operation) {
  614. switch($operation) {
  615. case 1:
  616. $result['added'][] = $uri;
  617. break;
  618. case 2:
  619. $result['modified'][] = $uri;
  620. break;
  621. case 3:
  622. $result['deleted'][] = $uri;
  623. break;
  624. }
  625. }
  626. } else {
  627. // No synctoken supplied, this is the initial sync.
  628. $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
  629. $stmt = $this->db->prepare($query);
  630. $stmt->execute([$addressBookId]);
  631. $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
  632. }
  633. return $result;
  634. }
  635. /**
  636. * Adds a change record to the addressbookchanges table.
  637. *
  638. * @param mixed $addressBookId
  639. * @param string $objectUri
  640. * @param int $operation 1 = add, 2 = modify, 3 = delete
  641. * @return void
  642. */
  643. protected function addChange($addressBookId, $objectUri, $operation) {
  644. $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
  645. $stmt = $this->db->prepare($sql);
  646. $stmt->execute([
  647. $objectUri,
  648. $addressBookId,
  649. $operation,
  650. $addressBookId
  651. ]);
  652. $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
  653. $stmt->execute([
  654. $addressBookId
  655. ]);
  656. }
  657. private function readBlob($cardData) {
  658. if (is_resource($cardData)) {
  659. return stream_get_contents($cardData);
  660. }
  661. return $cardData;
  662. }
  663. /**
  664. * @param IShareable $shareable
  665. * @param string[] $add
  666. * @param string[] $remove
  667. */
  668. public function updateShares(IShareable $shareable, $add, $remove) {
  669. $this->sharingBackend->updateShares($shareable, $add, $remove);
  670. }
  671. /**
  672. * search contact
  673. *
  674. * @param int $addressBookId
  675. * @param string $pattern which should match within the $searchProperties
  676. * @param array $searchProperties defines the properties within the query pattern should match
  677. * @return array an array of contacts which are arrays of key-value-pairs
  678. */
  679. public function search($addressBookId, $pattern, $searchProperties) {
  680. $query = $this->db->getQueryBuilder();
  681. $query2 = $this->db->getQueryBuilder();
  682. $query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
  683. foreach ($searchProperties as $property) {
  684. $query2->orWhere(
  685. $query2->expr()->andX(
  686. $query2->expr()->eq('cp.name', $query->createNamedParameter($property)),
  687. $query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))
  688. )
  689. );
  690. }
  691. $query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
  692. $query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
  693. ->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
  694. $result = $query->execute();
  695. $cards = $result->fetchAll();
  696. $result->closeCursor();
  697. return array_map(function($array) {
  698. $array['carddata'] = $this->readBlob($array['carddata']);
  699. return $array;
  700. }, $cards);
  701. }
  702. /**
  703. * @param int $bookId
  704. * @param string $name
  705. * @return array
  706. */
  707. public function collectCardProperties($bookId, $name) {
  708. $query = $this->db->getQueryBuilder();
  709. $result = $query->selectDistinct('value')
  710. ->from($this->dbCardsPropertiesTable)
  711. ->where($query->expr()->eq('name', $query->createNamedParameter($name)))
  712. ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
  713. ->execute();
  714. $all = $result->fetchAll(PDO::FETCH_COLUMN);
  715. $result->closeCursor();
  716. return $all;
  717. }
  718. /**
  719. * get URI from a given contact
  720. *
  721. * @param int $id
  722. * @return string
  723. */
  724. public function getCardUri($id) {
  725. $query = $this->db->getQueryBuilder();
  726. $query->select('uri')->from($this->dbCardsTable)
  727. ->where($query->expr()->eq('id', $query->createParameter('id')))
  728. ->setParameter('id', $id);
  729. $result = $query->execute();
  730. $uri = $result->fetch();
  731. $result->closeCursor();
  732. if (!isset($uri['uri'])) {
  733. throw new \InvalidArgumentException('Card does not exists: ' . $id);
  734. }
  735. return $uri['uri'];
  736. }
  737. /**
  738. * return contact with the given URI
  739. *
  740. * @param int $addressBookId
  741. * @param string $uri
  742. * @returns array
  743. */
  744. public function getContact($addressBookId, $uri) {
  745. $result = [];
  746. $query = $this->db->getQueryBuilder();
  747. $query->select('*')->from($this->dbCardsTable)
  748. ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
  749. ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
  750. $queryResult = $query->execute();
  751. $contact = $queryResult->fetch();
  752. $queryResult->closeCursor();
  753. if (is_array($contact)) {
  754. $result = $contact;
  755. }
  756. return $result;
  757. }
  758. /**
  759. * Returns the list of people whom this address book is shared with.
  760. *
  761. * Every element in this array should have the following properties:
  762. * * href - Often a mailto: address
  763. * * commonName - Optional, for example a first + last name
  764. * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
  765. * * readOnly - boolean
  766. * * summary - Optional, a description for the share
  767. *
  768. * @return array
  769. */
  770. public function getShares($addressBookId) {
  771. return $this->sharingBackend->getShares($addressBookId);
  772. }
  773. /**
  774. * update properties table
  775. *
  776. * @param int $addressBookId
  777. * @param string $cardUri
  778. * @param string $vCardSerialized
  779. */
  780. protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
  781. $cardId = $this->getCardId($addressBookId, $cardUri);
  782. $vCard = $this->readCard($vCardSerialized);
  783. $this->purgeProperties($addressBookId, $cardId);
  784. $query = $this->db->getQueryBuilder();
  785. $query->insert($this->dbCardsPropertiesTable)
  786. ->values(
  787. [
  788. 'addressbookid' => $query->createNamedParameter($addressBookId),
  789. 'cardid' => $query->createNamedParameter($cardId),
  790. 'name' => $query->createParameter('name'),
  791. 'value' => $query->createParameter('value'),
  792. 'preferred' => $query->createParameter('preferred')
  793. ]
  794. );
  795. foreach ($vCard->children as $property) {
  796. if(!in_array($property->name, self::$indexProperties)) {
  797. continue;
  798. }
  799. $preferred = 0;
  800. foreach($property->parameters as $parameter) {
  801. if ($parameter->name == 'TYPE' && strtoupper($parameter->getValue()) == 'PREF') {
  802. $preferred = 1;
  803. break;
  804. }
  805. }
  806. $query->setParameter('name', $property->name);
  807. $query->setParameter('value', substr($property->getValue(), 0, 254));
  808. $query->setParameter('preferred', $preferred);
  809. $query->execute();
  810. }
  811. }
  812. /**
  813. * read vCard data into a vCard object
  814. *
  815. * @param string $cardData
  816. * @return VCard
  817. */
  818. protected function readCard($cardData) {
  819. return Reader::read($cardData);
  820. }
  821. /**
  822. * delete all properties from a given card
  823. *
  824. * @param int $addressBookId
  825. * @param int $cardId
  826. */
  827. protected function purgeProperties($addressBookId, $cardId) {
  828. $query = $this->db->getQueryBuilder();
  829. $query->delete($this->dbCardsPropertiesTable)
  830. ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
  831. ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
  832. $query->execute();
  833. }
  834. /**
  835. * get ID from a given contact
  836. *
  837. * @param int $addressBookId
  838. * @param string $uri
  839. * @return int
  840. */
  841. protected function getCardId($addressBookId, $uri) {
  842. $query = $this->db->getQueryBuilder();
  843. $query->select('id')->from($this->dbCardsTable)
  844. ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
  845. ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
  846. $result = $query->execute();
  847. $cardIds = $result->fetch();
  848. $result->closeCursor();
  849. if (!isset($cardIds['id'])) {
  850. throw new \InvalidArgumentException('Card does not exists: ' . $uri);
  851. }
  852. return (int)$cardIds['id'];
  853. }
  854. /**
  855. * For shared address books the sharee is set in the ACL of the address book
  856. * @param $addressBookId
  857. * @param $acl
  858. * @return array
  859. */
  860. public function applyShareAcl($addressBookId, $acl) {
  861. return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
  862. }
  863. private function convertPrincipal($principalUri, $toV2) {
  864. if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
  865. list(, $name) = URLUtil::splitPath($principalUri);
  866. if ($toV2 === true) {
  867. return "principals/users/$name";
  868. }
  869. return "principals/$name";
  870. }
  871. return $principalUri;
  872. }
  873. }