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