Passed
Push — master ( 5094e2...72fda1 )
by Roeland
15:25 queued 10s
created
apps/dav/lib/CardDAV/CardDavBackend.php 1 patch
Indentation   +1300 added lines, -1300 removed lines patch added patch discarded remove patch
@@ -61,1304 +61,1304 @@
 block discarded – undo
61 61
 use Symfony\Component\EventDispatcher\GenericEvent;
62 62
 
63 63
 class CardDavBackend implements BackendInterface, SyncSupport {
64
-	public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
65
-	public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
66
-
67
-	/** @var Principal */
68
-	private $principalBackend;
69
-
70
-	/** @var string */
71
-	private $dbCardsTable = 'cards';
72
-
73
-	/** @var string */
74
-	private $dbCardsPropertiesTable = 'cards_properties';
75
-
76
-	/** @var IDBConnection */
77
-	private $db;
78
-
79
-	/** @var Backend */
80
-	private $sharingBackend;
81
-
82
-	/** @var array properties to index */
83
-	public static $indexProperties = [
84
-		'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
85
-		'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO',
86
-		'CLOUD', 'X-SOCIALPROFILE'];
87
-
88
-	/**
89
-	 * @var string[] Map of uid => display name
90
-	 */
91
-	protected $userDisplayNames;
92
-
93
-	/** @var IUserManager */
94
-	private $userManager;
95
-
96
-	/** @var IEventDispatcher */
97
-	private $dispatcher;
98
-
99
-	/** @var EventDispatcherInterface */
100
-	private $legacyDispatcher;
101
-
102
-	private $etagCache = [];
103
-
104
-	/**
105
-	 * CardDavBackend constructor.
106
-	 *
107
-	 * @param IDBConnection $db
108
-	 * @param Principal $principalBackend
109
-	 * @param IUserManager $userManager
110
-	 * @param IGroupManager $groupManager
111
-	 * @param IEventDispatcher $dispatcher
112
-	 * @param EventDispatcherInterface $legacyDispatcher
113
-	 */
114
-	public function __construct(IDBConnection $db,
115
-								Principal $principalBackend,
116
-								IUserManager $userManager,
117
-								IGroupManager $groupManager,
118
-								IEventDispatcher $dispatcher,
119
-								EventDispatcherInterface $legacyDispatcher) {
120
-		$this->db = $db;
121
-		$this->principalBackend = $principalBackend;
122
-		$this->userManager = $userManager;
123
-		$this->dispatcher = $dispatcher;
124
-		$this->legacyDispatcher = $legacyDispatcher;
125
-		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
126
-	}
127
-
128
-	/**
129
-	 * Return the number of address books for a principal
130
-	 *
131
-	 * @param $principalUri
132
-	 * @return int
133
-	 */
134
-	public function getAddressBooksForUserCount($principalUri) {
135
-		$principalUri = $this->convertPrincipal($principalUri, true);
136
-		$query = $this->db->getQueryBuilder();
137
-		$query->select($query->func()->count('*'))
138
-			->from('addressbooks')
139
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
140
-
141
-		$result = $query->execute();
142
-		$column = (int) $result->fetchColumn();
143
-		$result->closeCursor();
144
-		return $column;
145
-	}
146
-
147
-	/**
148
-	 * Returns the list of address books for a specific user.
149
-	 *
150
-	 * Every addressbook should have the following properties:
151
-	 *   id - an arbitrary unique id
152
-	 *   uri - the 'basename' part of the url
153
-	 *   principaluri - Same as the passed parameter
154
-	 *
155
-	 * Any additional clark-notation property may be passed besides this. Some
156
-	 * common ones are :
157
-	 *   {DAV:}displayname
158
-	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
159
-	 *   {http://calendarserver.org/ns/}getctag
160
-	 *
161
-	 * @param string $principalUri
162
-	 * @return array
163
-	 */
164
-	public function getAddressBooksForUser($principalUri) {
165
-		$principalUriOriginal = $principalUri;
166
-		$principalUri = $this->convertPrincipal($principalUri, true);
167
-		$query = $this->db->getQueryBuilder();
168
-		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
169
-			->from('addressbooks')
170
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
171
-
172
-		$addressBooks = [];
173
-
174
-		$result = $query->execute();
175
-		while ($row = $result->fetch()) {
176
-			$addressBooks[$row['id']] = [
177
-				'id' => $row['id'],
178
-				'uri' => $row['uri'],
179
-				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
180
-				'{DAV:}displayname' => $row['displayname'],
181
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
182
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
183
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
184
-			];
185
-
186
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
187
-		}
188
-		$result->closeCursor();
189
-
190
-		// query for shared addressbooks
191
-		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
192
-		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
193
-
194
-		$principals[] = $principalUri;
195
-
196
-		$query = $this->db->getQueryBuilder();
197
-		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
198
-			->from('dav_shares', 's')
199
-			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
200
-			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
201
-			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
202
-			->setParameter('type', 'addressbook')
203
-			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
204
-			->execute();
205
-
206
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
207
-		while ($row = $result->fetch()) {
208
-			if ($row['principaluri'] === $principalUri) {
209
-				continue;
210
-			}
211
-
212
-			$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
213
-			if (isset($addressBooks[$row['id']])) {
214
-				if ($readOnly) {
215
-					// New share can not have more permissions then the old one.
216
-					continue;
217
-				}
218
-				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
219
-					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
220
-					// Old share is already read-write, no more permissions can be gained
221
-					continue;
222
-				}
223
-			}
224
-
225
-			list(, $name) = \Sabre\Uri\split($row['principaluri']);
226
-			$uri = $row['uri'] . '_shared_by_' . $name;
227
-			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
228
-
229
-			$addressBooks[$row['id']] = [
230
-				'id' => $row['id'],
231
-				'uri' => $uri,
232
-				'principaluri' => $principalUriOriginal,
233
-				'{DAV:}displayname' => $displayName,
234
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
235
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
236
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
237
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
238
-				$readOnlyPropertyName => $readOnly,
239
-			];
240
-
241
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
242
-		}
243
-		$result->closeCursor();
244
-
245
-		return array_values($addressBooks);
246
-	}
247
-
248
-	public function getUsersOwnAddressBooks($principalUri) {
249
-		$principalUri = $this->convertPrincipal($principalUri, true);
250
-		$query = $this->db->getQueryBuilder();
251
-		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
252
-			->from('addressbooks')
253
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
254
-
255
-		$addressBooks = [];
256
-
257
-		$result = $query->execute();
258
-		while ($row = $result->fetch()) {
259
-			$addressBooks[$row['id']] = [
260
-				'id' => $row['id'],
261
-				'uri' => $row['uri'],
262
-				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
263
-				'{DAV:}displayname' => $row['displayname'],
264
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
265
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
266
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
267
-			];
268
-
269
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
270
-		}
271
-		$result->closeCursor();
272
-
273
-		return array_values($addressBooks);
274
-	}
275
-
276
-	private function getUserDisplayName($uid) {
277
-		if (!isset($this->userDisplayNames[$uid])) {
278
-			$user = $this->userManager->get($uid);
279
-
280
-			if ($user instanceof IUser) {
281
-				$this->userDisplayNames[$uid] = $user->getDisplayName();
282
-			} else {
283
-				$this->userDisplayNames[$uid] = $uid;
284
-			}
285
-		}
286
-
287
-		return $this->userDisplayNames[$uid];
288
-	}
289
-
290
-	/**
291
-	 * @param int $addressBookId
292
-	 */
293
-	public function getAddressBookById($addressBookId) {
294
-		$query = $this->db->getQueryBuilder();
295
-		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
296
-			->from('addressbooks')
297
-			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
298
-			->execute();
299
-
300
-		$row = $result->fetch();
301
-		$result->closeCursor();
302
-		if ($row === false) {
303
-			return null;
304
-		}
305
-
306
-		$addressBook = [
307
-			'id' => $row['id'],
308
-			'uri' => $row['uri'],
309
-			'principaluri' => $row['principaluri'],
310
-			'{DAV:}displayname' => $row['displayname'],
311
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
312
-			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
313
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
314
-		];
315
-
316
-		$this->addOwnerPrincipal($addressBook);
317
-
318
-		return $addressBook;
319
-	}
320
-
321
-	/**
322
-	 * @param $addressBookUri
323
-	 * @return array|null
324
-	 */
325
-	public function getAddressBooksByUri($principal, $addressBookUri) {
326
-		$query = $this->db->getQueryBuilder();
327
-		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
328
-			->from('addressbooks')
329
-			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
330
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
331
-			->setMaxResults(1)
332
-			->execute();
333
-
334
-		$row = $result->fetch();
335
-		$result->closeCursor();
336
-		if ($row === false) {
337
-			return null;
338
-		}
339
-
340
-		$addressBook = [
341
-			'id' => $row['id'],
342
-			'uri' => $row['uri'],
343
-			'principaluri' => $row['principaluri'],
344
-			'{DAV:}displayname' => $row['displayname'],
345
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
346
-			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
347
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
348
-		];
349
-
350
-		$this->addOwnerPrincipal($addressBook);
351
-
352
-		return $addressBook;
353
-	}
354
-
355
-	/**
356
-	 * Updates properties for an address book.
357
-	 *
358
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
359
-	 * To do the actual updates, you must tell this object which properties
360
-	 * you're going to process with the handle() method.
361
-	 *
362
-	 * Calling the handle method is like telling the PropPatch object "I
363
-	 * promise I can handle updating this property".
364
-	 *
365
-	 * Read the PropPatch documentation for more info and examples.
366
-	 *
367
-	 * @param string $addressBookId
368
-	 * @param \Sabre\DAV\PropPatch $propPatch
369
-	 * @return void
370
-	 */
371
-	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
372
-		$supportedProperties = [
373
-			'{DAV:}displayname',
374
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
375
-		];
376
-
377
-		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
378
-			$updates = [];
379
-			foreach ($mutations as $property => $newValue) {
380
-				switch ($property) {
381
-					case '{DAV:}displayname':
382
-						$updates['displayname'] = $newValue;
383
-						break;
384
-					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
385
-						$updates['description'] = $newValue;
386
-						break;
387
-				}
388
-			}
389
-			$query = $this->db->getQueryBuilder();
390
-			$query->update('addressbooks');
391
-
392
-			foreach ($updates as $key => $value) {
393
-				$query->set($key, $query->createNamedParameter($value));
394
-			}
395
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
396
-				->execute();
397
-
398
-			$this->addChange($addressBookId, "", 2);
399
-
400
-			$addressBookRow = $this->getAddressBookById((int)$addressBookId);
401
-			$shares = $this->getShares($addressBookId);
402
-			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
403
-
404
-			return true;
405
-		});
406
-	}
407
-
408
-	/**
409
-	 * Creates a new address book
410
-	 *
411
-	 * @param string $principalUri
412
-	 * @param string $url Just the 'basename' of the url.
413
-	 * @param array $properties
414
-	 * @return int
415
-	 * @throws BadRequest
416
-	 */
417
-	public function createAddressBook($principalUri, $url, array $properties) {
418
-		$values = [
419
-			'displayname' => null,
420
-			'description' => null,
421
-			'principaluri' => $principalUri,
422
-			'uri' => $url,
423
-			'synctoken' => 1
424
-		];
425
-
426
-		foreach ($properties as $property => $newValue) {
427
-			switch ($property) {
428
-				case '{DAV:}displayname':
429
-					$values['displayname'] = $newValue;
430
-					break;
431
-				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
432
-					$values['description'] = $newValue;
433
-					break;
434
-				default:
435
-					throw new BadRequest('Unknown property: ' . $property);
436
-			}
437
-		}
438
-
439
-		// Fallback to make sure the displayname is set. Some clients may refuse
440
-		// to work with addressbooks not having a displayname.
441
-		if (is_null($values['displayname'])) {
442
-			$values['displayname'] = $url;
443
-		}
444
-
445
-		$query = $this->db->getQueryBuilder();
446
-		$query->insert('addressbooks')
447
-			->values([
448
-				'uri' => $query->createParameter('uri'),
449
-				'displayname' => $query->createParameter('displayname'),
450
-				'description' => $query->createParameter('description'),
451
-				'principaluri' => $query->createParameter('principaluri'),
452
-				'synctoken' => $query->createParameter('synctoken'),
453
-			])
454
-			->setParameters($values)
455
-			->execute();
456
-
457
-		$addressBookId = $query->getLastInsertId();
458
-		$addressBookRow = $this->getAddressBookById($addressBookId);
459
-		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
460
-
461
-		return $addressBookId;
462
-	}
463
-
464
-	/**
465
-	 * Deletes an entire addressbook and all its contents
466
-	 *
467
-	 * @param mixed $addressBookId
468
-	 * @return void
469
-	 */
470
-	public function deleteAddressBook($addressBookId) {
471
-		$addressBookData = $this->getAddressBookById($addressBookId);
472
-		$shares = $this->getShares($addressBookId);
473
-
474
-		$query = $this->db->getQueryBuilder();
475
-		$query->delete($this->dbCardsTable)
476
-			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
477
-			->setParameter('addressbookid', $addressBookId)
478
-			->execute();
479
-
480
-		$query->delete('addressbookchanges')
481
-			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
482
-			->setParameter('addressbookid', $addressBookId)
483
-			->execute();
484
-
485
-		$query->delete('addressbooks')
486
-			->where($query->expr()->eq('id', $query->createParameter('id')))
487
-			->setParameter('id', $addressBookId)
488
-			->execute();
489
-
490
-		$this->sharingBackend->deleteAllShares($addressBookId);
491
-
492
-		$query->delete($this->dbCardsPropertiesTable)
493
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
494
-			->execute();
495
-
496
-		if ($addressBookData) {
497
-			$this->dispatcher->dispatchTyped(new AddressBookDeletedEvent((int) $addressBookId, $addressBookData, $shares));
498
-		}
499
-	}
500
-
501
-	/**
502
-	 * Returns all cards for a specific addressbook id.
503
-	 *
504
-	 * This method should return the following properties for each card:
505
-	 *   * carddata - raw vcard data
506
-	 *   * uri - Some unique url
507
-	 *   * lastmodified - A unix timestamp
508
-	 *
509
-	 * It's recommended to also return the following properties:
510
-	 *   * etag - A unique etag. This must change every time the card changes.
511
-	 *   * size - The size of the card in bytes.
512
-	 *
513
-	 * If these last two properties are provided, less time will be spent
514
-	 * calculating them. If they are specified, you can also ommit carddata.
515
-	 * This may speed up certain requests, especially with large cards.
516
-	 *
517
-	 * @param mixed $addressBookId
518
-	 * @return array
519
-	 */
520
-	public function getCards($addressBookId) {
521
-		$query = $this->db->getQueryBuilder();
522
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
523
-			->from($this->dbCardsTable)
524
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
525
-
526
-		$cards = [];
527
-
528
-		$result = $query->execute();
529
-		while ($row = $result->fetch()) {
530
-			$row['etag'] = '"' . $row['etag'] . '"';
531
-
532
-			$modified = false;
533
-			$row['carddata'] = $this->readBlob($row['carddata'], $modified);
534
-			if ($modified) {
535
-				$row['size'] = strlen($row['carddata']);
536
-			}
537
-
538
-			$cards[] = $row;
539
-		}
540
-		$result->closeCursor();
541
-
542
-		return $cards;
543
-	}
544
-
545
-	/**
546
-	 * Returns a specific card.
547
-	 *
548
-	 * The same set of properties must be returned as with getCards. The only
549
-	 * exception is that 'carddata' is absolutely required.
550
-	 *
551
-	 * If the card does not exist, you must return false.
552
-	 *
553
-	 * @param mixed $addressBookId
554
-	 * @param string $cardUri
555
-	 * @return array
556
-	 */
557
-	public function getCard($addressBookId, $cardUri) {
558
-		$query = $this->db->getQueryBuilder();
559
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
560
-			->from($this->dbCardsTable)
561
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
562
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
563
-			->setMaxResults(1);
564
-
565
-		$result = $query->execute();
566
-		$row = $result->fetch();
567
-		if (!$row) {
568
-			return false;
569
-		}
570
-		$row['etag'] = '"' . $row['etag'] . '"';
571
-
572
-		$modified = false;
573
-		$row['carddata'] = $this->readBlob($row['carddata'], $modified);
574
-		if ($modified) {
575
-			$row['size'] = strlen($row['carddata']);
576
-		}
577
-
578
-		return $row;
579
-	}
580
-
581
-	/**
582
-	 * Returns a list of cards.
583
-	 *
584
-	 * This method should work identical to getCard, but instead return all the
585
-	 * cards in the list as an array.
586
-	 *
587
-	 * If the backend supports this, it may allow for some speed-ups.
588
-	 *
589
-	 * @param mixed $addressBookId
590
-	 * @param string[] $uris
591
-	 * @return array
592
-	 */
593
-	public function getMultipleCards($addressBookId, array $uris) {
594
-		if (empty($uris)) {
595
-			return [];
596
-		}
597
-
598
-		$chunks = array_chunk($uris, 100);
599
-		$cards = [];
600
-
601
-		$query = $this->db->getQueryBuilder();
602
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
603
-			->from($this->dbCardsTable)
604
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
605
-			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
606
-
607
-		foreach ($chunks as $uris) {
608
-			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
609
-			$result = $query->execute();
610
-
611
-			while ($row = $result->fetch()) {
612
-				$row['etag'] = '"' . $row['etag'] . '"';
613
-
614
-				$modified = false;
615
-				$row['carddata'] = $this->readBlob($row['carddata'], $modified);
616
-				if ($modified) {
617
-					$row['size'] = strlen($row['carddata']);
618
-				}
619
-
620
-				$cards[] = $row;
621
-			}
622
-			$result->closeCursor();
623
-		}
624
-		return $cards;
625
-	}
626
-
627
-	/**
628
-	 * Creates a new card.
629
-	 *
630
-	 * The addressbook id will be passed as the first argument. This is the
631
-	 * same id as it is returned from the getAddressBooksForUser method.
632
-	 *
633
-	 * The cardUri is a base uri, and doesn't include the full path. The
634
-	 * cardData argument is the vcard body, and is passed as a string.
635
-	 *
636
-	 * It is possible to return an ETag from this method. This ETag is for the
637
-	 * newly created resource, and must be enclosed with double quotes (that
638
-	 * is, the string itself must contain the double quotes).
639
-	 *
640
-	 * You should only return the ETag if you store the carddata as-is. If a
641
-	 * subsequent GET request on the same card does not have the same body,
642
-	 * byte-by-byte and you did return an ETag here, clients tend to get
643
-	 * confused.
644
-	 *
645
-	 * If you don't return an ETag, you can just return null.
646
-	 *
647
-	 * @param mixed $addressBookId
648
-	 * @param string $cardUri
649
-	 * @param string $cardData
650
-	 * @return string
651
-	 */
652
-	public function createCard($addressBookId, $cardUri, $cardData) {
653
-		$etag = md5($cardData);
654
-		$uid = $this->getUID($cardData);
655
-
656
-		$q = $this->db->getQueryBuilder();
657
-		$q->select('uid')
658
-			->from($this->dbCardsTable)
659
-			->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
660
-			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
661
-			->setMaxResults(1);
662
-		$result = $q->execute();
663
-		$count = (bool)$result->fetchColumn();
664
-		$result->closeCursor();
665
-		if ($count) {
666
-			throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
667
-		}
668
-
669
-		$query = $this->db->getQueryBuilder();
670
-		$query->insert('cards')
671
-			->values([
672
-				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
673
-				'uri' => $query->createNamedParameter($cardUri),
674
-				'lastmodified' => $query->createNamedParameter(time()),
675
-				'addressbookid' => $query->createNamedParameter($addressBookId),
676
-				'size' => $query->createNamedParameter(strlen($cardData)),
677
-				'etag' => $query->createNamedParameter($etag),
678
-				'uid' => $query->createNamedParameter($uid),
679
-			])
680
-			->execute();
681
-
682
-		$etagCacheKey = "$addressBookId#$cardUri";
683
-		$this->etagCache[$etagCacheKey] = $etag;
684
-
685
-		$this->addChange($addressBookId, $cardUri, 1);
686
-		$this->updateProperties($addressBookId, $cardUri, $cardData);
687
-
688
-		$addressBookData = $this->getAddressBookById($addressBookId);
689
-		$shares = $this->getShares($addressBookId);
690
-		$objectRow = $this->getCard($addressBookId, $cardUri);
691
-		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
692
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
693
-			new GenericEvent(null, [
694
-				'addressBookId' => $addressBookId,
695
-				'cardUri' => $cardUri,
696
-				'cardData' => $cardData]));
697
-
698
-		return '"' . $etag . '"';
699
-	}
700
-
701
-	/**
702
-	 * Updates a card.
703
-	 *
704
-	 * The addressbook id will be passed as the first argument. This is the
705
-	 * same id as it is returned from the getAddressBooksForUser method.
706
-	 *
707
-	 * The cardUri is a base uri, and doesn't include the full path. The
708
-	 * cardData argument is the vcard body, and is passed as a string.
709
-	 *
710
-	 * It is possible to return an ETag from this method. This ETag should
711
-	 * match that of the updated resource, and must be enclosed with double
712
-	 * quotes (that is: the string itself must contain the actual quotes).
713
-	 *
714
-	 * You should only return the ETag if you store the carddata as-is. If a
715
-	 * subsequent GET request on the same card does not have the same body,
716
-	 * byte-by-byte and you did return an ETag here, clients tend to get
717
-	 * confused.
718
-	 *
719
-	 * If you don't return an ETag, you can just return null.
720
-	 *
721
-	 * @param mixed $addressBookId
722
-	 * @param string $cardUri
723
-	 * @param string $cardData
724
-	 * @return string
725
-	 */
726
-	public function updateCard($addressBookId, $cardUri, $cardData) {
727
-		$uid = $this->getUID($cardData);
728
-		$etag = md5($cardData);
729
-		$query = $this->db->getQueryBuilder();
730
-
731
-		// check for recently stored etag and stop if it is the same
732
-		$etagCacheKey = "$addressBookId#$cardUri";
733
-		if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
734
-			return '"' . $etag . '"';
735
-		}
736
-
737
-		$query->update($this->dbCardsTable)
738
-			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
739
-			->set('lastmodified', $query->createNamedParameter(time()))
740
-			->set('size', $query->createNamedParameter(strlen($cardData)))
741
-			->set('etag', $query->createNamedParameter($etag))
742
-			->set('uid', $query->createNamedParameter($uid))
743
-			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
744
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
745
-			->execute();
746
-
747
-		$this->etagCache[$etagCacheKey] = $etag;
748
-
749
-		$this->addChange($addressBookId, $cardUri, 2);
750
-		$this->updateProperties($addressBookId, $cardUri, $cardData);
751
-
752
-		$addressBookData = $this->getAddressBookById($addressBookId);
753
-		$shares = $this->getShares($addressBookId);
754
-		$objectRow = $this->getCard($addressBookId, $cardUri);
755
-		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
756
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
757
-			new GenericEvent(null, [
758
-				'addressBookId' => $addressBookId,
759
-				'cardUri' => $cardUri,
760
-				'cardData' => $cardData]));
761
-
762
-		return '"' . $etag . '"';
763
-	}
764
-
765
-	/**
766
-	 * Deletes a card
767
-	 *
768
-	 * @param mixed $addressBookId
769
-	 * @param string $cardUri
770
-	 * @return bool
771
-	 */
772
-	public function deleteCard($addressBookId, $cardUri) {
773
-		$addressBookData = $this->getAddressBookById($addressBookId);
774
-		$shares = $this->getShares($addressBookId);
775
-		$objectRow = $this->getCard($addressBookId, $cardUri);
776
-
777
-		try {
778
-			$cardId = $this->getCardId($addressBookId, $cardUri);
779
-		} catch (\InvalidArgumentException $e) {
780
-			$cardId = null;
781
-		}
782
-		$query = $this->db->getQueryBuilder();
783
-		$ret = $query->delete($this->dbCardsTable)
784
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
785
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
786
-			->execute();
787
-
788
-		$this->addChange($addressBookId, $cardUri, 3);
789
-
790
-		if ($ret === 1) {
791
-			if ($cardId !== null) {
792
-				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
793
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
794
-					new GenericEvent(null, [
795
-						'addressBookId' => $addressBookId,
796
-						'cardUri' => $cardUri]));
797
-
798
-				$this->purgeProperties($addressBookId, $cardId);
799
-			}
800
-			return true;
801
-		}
802
-
803
-		return false;
804
-	}
805
-
806
-	/**
807
-	 * The getChanges method returns all the changes that have happened, since
808
-	 * the specified syncToken in the specified address book.
809
-	 *
810
-	 * This function should return an array, such as the following:
811
-	 *
812
-	 * [
813
-	 *   'syncToken' => 'The current synctoken',
814
-	 *   'added'   => [
815
-	 *      'new.txt',
816
-	 *   ],
817
-	 *   'modified'   => [
818
-	 *      'modified.txt',
819
-	 *   ],
820
-	 *   'deleted' => [
821
-	 *      'foo.php.bak',
822
-	 *      'old.txt'
823
-	 *   ]
824
-	 * ];
825
-	 *
826
-	 * The returned syncToken property should reflect the *current* syncToken
827
-	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
828
-	 * property. This is needed here too, to ensure the operation is atomic.
829
-	 *
830
-	 * If the $syncToken argument is specified as null, this is an initial
831
-	 * sync, and all members should be reported.
832
-	 *
833
-	 * The modified property is an array of nodenames that have changed since
834
-	 * the last token.
835
-	 *
836
-	 * The deleted property is an array with nodenames, that have been deleted
837
-	 * from collection.
838
-	 *
839
-	 * The $syncLevel argument is basically the 'depth' of the report. If it's
840
-	 * 1, you only have to report changes that happened only directly in
841
-	 * immediate descendants. If it's 2, it should also include changes from
842
-	 * the nodes below the child collections. (grandchildren)
843
-	 *
844
-	 * The $limit argument allows a client to specify how many results should
845
-	 * be returned at most. If the limit is not specified, it should be treated
846
-	 * as infinite.
847
-	 *
848
-	 * If the limit (infinite or not) is higher than you're willing to return,
849
-	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
850
-	 *
851
-	 * If the syncToken is expired (due to data cleanup) or unknown, you must
852
-	 * return null.
853
-	 *
854
-	 * The limit is 'suggestive'. You are free to ignore it.
855
-	 *
856
-	 * @param string $addressBookId
857
-	 * @param string $syncToken
858
-	 * @param int $syncLevel
859
-	 * @param int $limit
860
-	 * @return array
861
-	 */
862
-	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
863
-		// Current synctoken
864
-		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
865
-		$stmt->execute([$addressBookId]);
866
-		$currentToken = $stmt->fetchColumn(0);
867
-
868
-		if (is_null($currentToken)) {
869
-			return null;
870
-		}
871
-
872
-		$result = [
873
-			'syncToken' => $currentToken,
874
-			'added' => [],
875
-			'modified' => [],
876
-			'deleted' => [],
877
-		];
878
-
879
-		if ($syncToken) {
880
-			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
881
-			if ($limit > 0) {
882
-				$query .= " LIMIT " . (int)$limit;
883
-			}
884
-
885
-			// Fetching all changes
886
-			$stmt = $this->db->prepare($query);
887
-			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
888
-
889
-			$changes = [];
890
-
891
-			// This loop ensures that any duplicates are overwritten, only the
892
-			// last change on a node is relevant.
893
-			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
894
-				$changes[$row['uri']] = $row['operation'];
895
-			}
896
-
897
-			foreach ($changes as $uri => $operation) {
898
-				switch ($operation) {
899
-					case 1:
900
-						$result['added'][] = $uri;
901
-						break;
902
-					case 2:
903
-						$result['modified'][] = $uri;
904
-						break;
905
-					case 3:
906
-						$result['deleted'][] = $uri;
907
-						break;
908
-				}
909
-			}
910
-		} else {
911
-			// No synctoken supplied, this is the initial sync.
912
-			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
913
-			$stmt = $this->db->prepare($query);
914
-			$stmt->execute([$addressBookId]);
915
-
916
-			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
917
-		}
918
-		return $result;
919
-	}
920
-
921
-	/**
922
-	 * Adds a change record to the addressbookchanges table.
923
-	 *
924
-	 * @param mixed $addressBookId
925
-	 * @param string $objectUri
926
-	 * @param int $operation 1 = add, 2 = modify, 3 = delete
927
-	 * @return void
928
-	 */
929
-	protected function addChange($addressBookId, $objectUri, $operation) {
930
-		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
931
-		$stmt = $this->db->prepare($sql);
932
-		$stmt->execute([
933
-			$objectUri,
934
-			$addressBookId,
935
-			$operation,
936
-			$addressBookId
937
-		]);
938
-		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
939
-		$stmt->execute([
940
-			$addressBookId
941
-		]);
942
-	}
943
-
944
-	/**
945
-	 * @param resource|string $cardData
946
-	 * @param bool $modified
947
-	 * @return string
948
-	 */
949
-	private function readBlob($cardData, &$modified = false) {
950
-		if (is_resource($cardData)) {
951
-			$cardData = stream_get_contents($cardData);
952
-		}
953
-
954
-		$cardDataArray = explode("\r\n", $cardData);
955
-
956
-		$cardDataFiltered = [];
957
-		$removingPhoto = false;
958
-		foreach ($cardDataArray as $line) {
959
-			if (strpos($line, 'PHOTO:data:') === 0
960
-				&& strpos($line, 'PHOTO:data:image/') !== 0) {
961
-				// Filter out PHOTO data of non-images
962
-				$removingPhoto = true;
963
-				$modified = true;
964
-				continue;
965
-			}
966
-
967
-			if ($removingPhoto) {
968
-				if (strpos($line, ' ') === 0) {
969
-					continue;
970
-				}
971
-				// No leading space means this is a new property
972
-				$removingPhoto = false;
973
-			}
974
-
975
-			$cardDataFiltered[] = $line;
976
-		}
977
-
978
-		return implode("\r\n", $cardDataFiltered);
979
-	}
980
-
981
-	/**
982
-	 * @param IShareable $shareable
983
-	 * @param string[] $add
984
-	 * @param string[] $remove
985
-	 */
986
-	public function updateShares(IShareable $shareable, $add, $remove) {
987
-		$addressBookId = $shareable->getResourceId();
988
-		$addressBookData = $this->getAddressBookById($addressBookId);
989
-		$oldShares = $this->getShares($addressBookId);
990
-
991
-		$this->sharingBackend->updateShares($shareable, $add, $remove);
992
-
993
-		$this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
994
-	}
995
-
996
-	/**
997
-	 * Search contacts in a specific address-book
998
-	 *
999
-	 * @param int $addressBookId
1000
-	 * @param string $pattern which should match within the $searchProperties
1001
-	 * @param array $searchProperties defines the properties within the query pattern should match
1002
-	 * @param array $options = array() to define the search behavior
1003
-	 *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1004
-	 *    - 'limit' - Set a numeric limit for the search results
1005
-	 *    - 'offset' - Set the offset for the limited search results
1006
-	 * @return array an array of contacts which are arrays of key-value-pairs
1007
-	 */
1008
-	public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1009
-		return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1010
-	}
1011
-
1012
-	/**
1013
-	 * Search contacts in all address-books accessible by a user
1014
-	 *
1015
-	 * @param string $principalUri
1016
-	 * @param string $pattern
1017
-	 * @param array $searchProperties
1018
-	 * @param array $options
1019
-	 * @return array
1020
-	 */
1021
-	public function searchPrincipalUri(string $principalUri,
1022
-									   string $pattern,
1023
-									   array $searchProperties,
1024
-									   array $options = []): array {
1025
-		$addressBookIds = array_map(static function ($row):int {
1026
-			return (int) $row['id'];
1027
-		}, $this->getAddressBooksForUser($principalUri));
1028
-
1029
-		return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1030
-	}
1031
-
1032
-	/**
1033
-	 * @param array $addressBookIds
1034
-	 * @param string $pattern
1035
-	 * @param array $searchProperties
1036
-	 * @param array $options
1037
-	 * @return array
1038
-	 */
1039
-	private function searchByAddressBookIds(array $addressBookIds,
1040
-											string $pattern,
1041
-											array $searchProperties,
1042
-											array $options = []): array {
1043
-		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1044
-
1045
-		$query2 = $this->db->getQueryBuilder();
1046
-
1047
-		$addressBookOr = $query2->expr()->orX();
1048
-		foreach ($addressBookIds as $addressBookId) {
1049
-			$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1050
-		}
1051
-
1052
-		if ($addressBookOr->count() === 0) {
1053
-			return [];
1054
-		}
1055
-
1056
-		$propertyOr = $query2->expr()->orX();
1057
-		foreach ($searchProperties as $property) {
1058
-			if ($escapePattern) {
1059
-				if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1060
-					// There can be no spaces in emails
1061
-					continue;
1062
-				}
1063
-
1064
-				if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1065
-					// There can be no chars in cloud ids which are not valid for user ids plus :/
1066
-					// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1067
-					continue;
1068
-				}
1069
-			}
1070
-
1071
-			$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1072
-		}
1073
-
1074
-		if ($propertyOr->count() === 0) {
1075
-			return [];
1076
-		}
1077
-
1078
-		$query2->selectDistinct('cp.cardid')
1079
-			->from($this->dbCardsPropertiesTable, 'cp')
1080
-			->andWhere($addressBookOr)
1081
-			->andWhere($propertyOr);
1082
-
1083
-		// No need for like when the pattern is empty
1084
-		if ('' !== $pattern) {
1085
-			if (!$escapePattern) {
1086
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1087
-			} else {
1088
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1089
-			}
1090
-		}
1091
-
1092
-		if (isset($options['limit'])) {
1093
-			$query2->setMaxResults($options['limit']);
1094
-		}
1095
-		if (isset($options['offset'])) {
1096
-			$query2->setFirstResult($options['offset']);
1097
-		}
1098
-
1099
-		$result = $query2->execute();
1100
-		$matches = $result->fetchAll();
1101
-		$result->closeCursor();
1102
-		$matches = array_map(function ($match) {
1103
-			return (int)$match['cardid'];
1104
-		}, $matches);
1105
-
1106
-		$query = $this->db->getQueryBuilder();
1107
-		$query->select('c.addressbookid', 'c.carddata', 'c.uri')
1108
-			->from($this->dbCardsTable, 'c')
1109
-			->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1110
-
1111
-		$result = $query->execute();
1112
-		$cards = $result->fetchAll();
1113
-
1114
-		$result->closeCursor();
1115
-
1116
-		return array_map(function ($array) {
1117
-			$array['addressbookid'] = (int) $array['addressbookid'];
1118
-			$modified = false;
1119
-			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
1120
-			if ($modified) {
1121
-				$array['size'] = strlen($array['carddata']);
1122
-			}
1123
-			return $array;
1124
-		}, $cards);
1125
-	}
1126
-
1127
-	/**
1128
-	 * @param int $bookId
1129
-	 * @param string $name
1130
-	 * @return array
1131
-	 */
1132
-	public function collectCardProperties($bookId, $name) {
1133
-		$query = $this->db->getQueryBuilder();
1134
-		$result = $query->selectDistinct('value')
1135
-			->from($this->dbCardsPropertiesTable)
1136
-			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1137
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1138
-			->execute();
1139
-
1140
-		$all = $result->fetchAll(PDO::FETCH_COLUMN);
1141
-		$result->closeCursor();
1142
-
1143
-		return $all;
1144
-	}
1145
-
1146
-	/**
1147
-	 * get URI from a given contact
1148
-	 *
1149
-	 * @param int $id
1150
-	 * @return string
1151
-	 */
1152
-	public function getCardUri($id) {
1153
-		$query = $this->db->getQueryBuilder();
1154
-		$query->select('uri')->from($this->dbCardsTable)
1155
-			->where($query->expr()->eq('id', $query->createParameter('id')))
1156
-			->setParameter('id', $id);
1157
-
1158
-		$result = $query->execute();
1159
-		$uri = $result->fetch();
1160
-		$result->closeCursor();
1161
-
1162
-		if (!isset($uri['uri'])) {
1163
-			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1164
-		}
1165
-
1166
-		return $uri['uri'];
1167
-	}
1168
-
1169
-	/**
1170
-	 * return contact with the given URI
1171
-	 *
1172
-	 * @param int $addressBookId
1173
-	 * @param string $uri
1174
-	 * @returns array
1175
-	 */
1176
-	public function getContact($addressBookId, $uri) {
1177
-		$result = [];
1178
-		$query = $this->db->getQueryBuilder();
1179
-		$query->select('*')->from($this->dbCardsTable)
1180
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1181
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1182
-		$queryResult = $query->execute();
1183
-		$contact = $queryResult->fetch();
1184
-		$queryResult->closeCursor();
1185
-
1186
-		if (is_array($contact)) {
1187
-			$modified = false;
1188
-			$contact['etag'] = '"' . $contact['etag'] . '"';
1189
-			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1190
-			if ($modified) {
1191
-				$contact['size'] = strlen($contact['carddata']);
1192
-			}
1193
-
1194
-			$result = $contact;
1195
-		}
1196
-
1197
-		return $result;
1198
-	}
1199
-
1200
-	/**
1201
-	 * Returns the list of people whom this address book is shared with.
1202
-	 *
1203
-	 * Every element in this array should have the following properties:
1204
-	 *   * href - Often a mailto: address
1205
-	 *   * commonName - Optional, for example a first + last name
1206
-	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1207
-	 *   * readOnly - boolean
1208
-	 *   * summary - Optional, a description for the share
1209
-	 *
1210
-	 * @return array
1211
-	 */
1212
-	public function getShares($addressBookId) {
1213
-		return $this->sharingBackend->getShares($addressBookId);
1214
-	}
1215
-
1216
-	/**
1217
-	 * update properties table
1218
-	 *
1219
-	 * @param int $addressBookId
1220
-	 * @param string $cardUri
1221
-	 * @param string $vCardSerialized
1222
-	 */
1223
-	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1224
-		$cardId = $this->getCardId($addressBookId, $cardUri);
1225
-		$vCard = $this->readCard($vCardSerialized);
1226
-
1227
-		$this->purgeProperties($addressBookId, $cardId);
1228
-
1229
-		$query = $this->db->getQueryBuilder();
1230
-		$query->insert($this->dbCardsPropertiesTable)
1231
-			->values(
1232
-				[
1233
-					'addressbookid' => $query->createNamedParameter($addressBookId),
1234
-					'cardid' => $query->createNamedParameter($cardId),
1235
-					'name' => $query->createParameter('name'),
1236
-					'value' => $query->createParameter('value'),
1237
-					'preferred' => $query->createParameter('preferred')
1238
-				]
1239
-			);
1240
-
1241
-		foreach ($vCard->children() as $property) {
1242
-			if (!in_array($property->name, self::$indexProperties)) {
1243
-				continue;
1244
-			}
1245
-			$preferred = 0;
1246
-			foreach ($property->parameters as $parameter) {
1247
-				if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1248
-					$preferred = 1;
1249
-					break;
1250
-				}
1251
-			}
1252
-			$query->setParameter('name', $property->name);
1253
-			$query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1254
-			$query->setParameter('preferred', $preferred);
1255
-			$query->execute();
1256
-		}
1257
-	}
1258
-
1259
-	/**
1260
-	 * read vCard data into a vCard object
1261
-	 *
1262
-	 * @param string $cardData
1263
-	 * @return VCard
1264
-	 */
1265
-	protected function readCard($cardData) {
1266
-		return Reader::read($cardData);
1267
-	}
1268
-
1269
-	/**
1270
-	 * delete all properties from a given card
1271
-	 *
1272
-	 * @param int $addressBookId
1273
-	 * @param int $cardId
1274
-	 */
1275
-	protected function purgeProperties($addressBookId, $cardId) {
1276
-		$query = $this->db->getQueryBuilder();
1277
-		$query->delete($this->dbCardsPropertiesTable)
1278
-			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1279
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1280
-		$query->execute();
1281
-	}
1282
-
1283
-	/**
1284
-	 * get ID from a given contact
1285
-	 *
1286
-	 * @param int $addressBookId
1287
-	 * @param string $uri
1288
-	 * @return int
1289
-	 */
1290
-	protected function getCardId($addressBookId, $uri) {
1291
-		$query = $this->db->getQueryBuilder();
1292
-		$query->select('id')->from($this->dbCardsTable)
1293
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1294
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1295
-
1296
-		$result = $query->execute();
1297
-		$cardIds = $result->fetch();
1298
-		$result->closeCursor();
1299
-
1300
-		if (!isset($cardIds['id'])) {
1301
-			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1302
-		}
1303
-
1304
-		return (int)$cardIds['id'];
1305
-	}
1306
-
1307
-	/**
1308
-	 * For shared address books the sharee is set in the ACL of the address book
1309
-	 *
1310
-	 * @param $addressBookId
1311
-	 * @param $acl
1312
-	 * @return array
1313
-	 */
1314
-	public function applyShareAcl($addressBookId, $acl) {
1315
-		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1316
-	}
1317
-
1318
-	private function convertPrincipal($principalUri, $toV2) {
1319
-		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1320
-			list(, $name) = \Sabre\Uri\split($principalUri);
1321
-			if ($toV2 === true) {
1322
-				return "principals/users/$name";
1323
-			}
1324
-			return "principals/$name";
1325
-		}
1326
-		return $principalUri;
1327
-	}
1328
-
1329
-	private function addOwnerPrincipal(&$addressbookInfo) {
1330
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1331
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1332
-		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1333
-			$uri = $addressbookInfo[$ownerPrincipalKey];
1334
-		} else {
1335
-			$uri = $addressbookInfo['principaluri'];
1336
-		}
1337
-
1338
-		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1339
-		if (isset($principalInformation['{DAV:}displayname'])) {
1340
-			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1341
-		}
1342
-	}
1343
-
1344
-	/**
1345
-	 * Extract UID from vcard
1346
-	 *
1347
-	 * @param string $cardData the vcard raw data
1348
-	 * @return string the uid
1349
-	 * @throws BadRequest if no UID is available
1350
-	 */
1351
-	private function getUID($cardData) {
1352
-		if ($cardData != '') {
1353
-			$vCard = Reader::read($cardData);
1354
-			if ($vCard->UID) {
1355
-				$uid = $vCard->UID->getValue();
1356
-				return $uid;
1357
-			}
1358
-			// should already be handled, but just in case
1359
-			throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1360
-		}
1361
-		// should already be handled, but just in case
1362
-		throw new BadRequest('vCard can not be empty');
1363
-	}
64
+    public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
65
+    public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
66
+
67
+    /** @var Principal */
68
+    private $principalBackend;
69
+
70
+    /** @var string */
71
+    private $dbCardsTable = 'cards';
72
+
73
+    /** @var string */
74
+    private $dbCardsPropertiesTable = 'cards_properties';
75
+
76
+    /** @var IDBConnection */
77
+    private $db;
78
+
79
+    /** @var Backend */
80
+    private $sharingBackend;
81
+
82
+    /** @var array properties to index */
83
+    public static $indexProperties = [
84
+        'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
85
+        'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO',
86
+        'CLOUD', 'X-SOCIALPROFILE'];
87
+
88
+    /**
89
+     * @var string[] Map of uid => display name
90
+     */
91
+    protected $userDisplayNames;
92
+
93
+    /** @var IUserManager */
94
+    private $userManager;
95
+
96
+    /** @var IEventDispatcher */
97
+    private $dispatcher;
98
+
99
+    /** @var EventDispatcherInterface */
100
+    private $legacyDispatcher;
101
+
102
+    private $etagCache = [];
103
+
104
+    /**
105
+     * CardDavBackend constructor.
106
+     *
107
+     * @param IDBConnection $db
108
+     * @param Principal $principalBackend
109
+     * @param IUserManager $userManager
110
+     * @param IGroupManager $groupManager
111
+     * @param IEventDispatcher $dispatcher
112
+     * @param EventDispatcherInterface $legacyDispatcher
113
+     */
114
+    public function __construct(IDBConnection $db,
115
+                                Principal $principalBackend,
116
+                                IUserManager $userManager,
117
+                                IGroupManager $groupManager,
118
+                                IEventDispatcher $dispatcher,
119
+                                EventDispatcherInterface $legacyDispatcher) {
120
+        $this->db = $db;
121
+        $this->principalBackend = $principalBackend;
122
+        $this->userManager = $userManager;
123
+        $this->dispatcher = $dispatcher;
124
+        $this->legacyDispatcher = $legacyDispatcher;
125
+        $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
126
+    }
127
+
128
+    /**
129
+     * Return the number of address books for a principal
130
+     *
131
+     * @param $principalUri
132
+     * @return int
133
+     */
134
+    public function getAddressBooksForUserCount($principalUri) {
135
+        $principalUri = $this->convertPrincipal($principalUri, true);
136
+        $query = $this->db->getQueryBuilder();
137
+        $query->select($query->func()->count('*'))
138
+            ->from('addressbooks')
139
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
140
+
141
+        $result = $query->execute();
142
+        $column = (int) $result->fetchColumn();
143
+        $result->closeCursor();
144
+        return $column;
145
+    }
146
+
147
+    /**
148
+     * Returns the list of address books for a specific user.
149
+     *
150
+     * Every addressbook should have the following properties:
151
+     *   id - an arbitrary unique id
152
+     *   uri - the 'basename' part of the url
153
+     *   principaluri - Same as the passed parameter
154
+     *
155
+     * Any additional clark-notation property may be passed besides this. Some
156
+     * common ones are :
157
+     *   {DAV:}displayname
158
+     *   {urn:ietf:params:xml:ns:carddav}addressbook-description
159
+     *   {http://calendarserver.org/ns/}getctag
160
+     *
161
+     * @param string $principalUri
162
+     * @return array
163
+     */
164
+    public function getAddressBooksForUser($principalUri) {
165
+        $principalUriOriginal = $principalUri;
166
+        $principalUri = $this->convertPrincipal($principalUri, true);
167
+        $query = $this->db->getQueryBuilder();
168
+        $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
169
+            ->from('addressbooks')
170
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
171
+
172
+        $addressBooks = [];
173
+
174
+        $result = $query->execute();
175
+        while ($row = $result->fetch()) {
176
+            $addressBooks[$row['id']] = [
177
+                'id' => $row['id'],
178
+                'uri' => $row['uri'],
179
+                'principaluri' => $this->convertPrincipal($row['principaluri'], false),
180
+                '{DAV:}displayname' => $row['displayname'],
181
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
182
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
183
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
184
+            ];
185
+
186
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
187
+        }
188
+        $result->closeCursor();
189
+
190
+        // query for shared addressbooks
191
+        $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
192
+        $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
193
+
194
+        $principals[] = $principalUri;
195
+
196
+        $query = $this->db->getQueryBuilder();
197
+        $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
198
+            ->from('dav_shares', 's')
199
+            ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
200
+            ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
201
+            ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
202
+            ->setParameter('type', 'addressbook')
203
+            ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
204
+            ->execute();
205
+
206
+        $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
207
+        while ($row = $result->fetch()) {
208
+            if ($row['principaluri'] === $principalUri) {
209
+                continue;
210
+            }
211
+
212
+            $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
213
+            if (isset($addressBooks[$row['id']])) {
214
+                if ($readOnly) {
215
+                    // New share can not have more permissions then the old one.
216
+                    continue;
217
+                }
218
+                if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
219
+                    $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
220
+                    // Old share is already read-write, no more permissions can be gained
221
+                    continue;
222
+                }
223
+            }
224
+
225
+            list(, $name) = \Sabre\Uri\split($row['principaluri']);
226
+            $uri = $row['uri'] . '_shared_by_' . $name;
227
+            $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
228
+
229
+            $addressBooks[$row['id']] = [
230
+                'id' => $row['id'],
231
+                'uri' => $uri,
232
+                'principaluri' => $principalUriOriginal,
233
+                '{DAV:}displayname' => $displayName,
234
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
235
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
236
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
237
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
238
+                $readOnlyPropertyName => $readOnly,
239
+            ];
240
+
241
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
242
+        }
243
+        $result->closeCursor();
244
+
245
+        return array_values($addressBooks);
246
+    }
247
+
248
+    public function getUsersOwnAddressBooks($principalUri) {
249
+        $principalUri = $this->convertPrincipal($principalUri, true);
250
+        $query = $this->db->getQueryBuilder();
251
+        $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
252
+            ->from('addressbooks')
253
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
254
+
255
+        $addressBooks = [];
256
+
257
+        $result = $query->execute();
258
+        while ($row = $result->fetch()) {
259
+            $addressBooks[$row['id']] = [
260
+                'id' => $row['id'],
261
+                'uri' => $row['uri'],
262
+                'principaluri' => $this->convertPrincipal($row['principaluri'], false),
263
+                '{DAV:}displayname' => $row['displayname'],
264
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
265
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
266
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
267
+            ];
268
+
269
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
270
+        }
271
+        $result->closeCursor();
272
+
273
+        return array_values($addressBooks);
274
+    }
275
+
276
+    private function getUserDisplayName($uid) {
277
+        if (!isset($this->userDisplayNames[$uid])) {
278
+            $user = $this->userManager->get($uid);
279
+
280
+            if ($user instanceof IUser) {
281
+                $this->userDisplayNames[$uid] = $user->getDisplayName();
282
+            } else {
283
+                $this->userDisplayNames[$uid] = $uid;
284
+            }
285
+        }
286
+
287
+        return $this->userDisplayNames[$uid];
288
+    }
289
+
290
+    /**
291
+     * @param int $addressBookId
292
+     */
293
+    public function getAddressBookById($addressBookId) {
294
+        $query = $this->db->getQueryBuilder();
295
+        $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
296
+            ->from('addressbooks')
297
+            ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
298
+            ->execute();
299
+
300
+        $row = $result->fetch();
301
+        $result->closeCursor();
302
+        if ($row === false) {
303
+            return null;
304
+        }
305
+
306
+        $addressBook = [
307
+            'id' => $row['id'],
308
+            'uri' => $row['uri'],
309
+            'principaluri' => $row['principaluri'],
310
+            '{DAV:}displayname' => $row['displayname'],
311
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
312
+            '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
313
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
314
+        ];
315
+
316
+        $this->addOwnerPrincipal($addressBook);
317
+
318
+        return $addressBook;
319
+    }
320
+
321
+    /**
322
+     * @param $addressBookUri
323
+     * @return array|null
324
+     */
325
+    public function getAddressBooksByUri($principal, $addressBookUri) {
326
+        $query = $this->db->getQueryBuilder();
327
+        $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
328
+            ->from('addressbooks')
329
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
330
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
331
+            ->setMaxResults(1)
332
+            ->execute();
333
+
334
+        $row = $result->fetch();
335
+        $result->closeCursor();
336
+        if ($row === false) {
337
+            return null;
338
+        }
339
+
340
+        $addressBook = [
341
+            'id' => $row['id'],
342
+            'uri' => $row['uri'],
343
+            'principaluri' => $row['principaluri'],
344
+            '{DAV:}displayname' => $row['displayname'],
345
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
346
+            '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
347
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
348
+        ];
349
+
350
+        $this->addOwnerPrincipal($addressBook);
351
+
352
+        return $addressBook;
353
+    }
354
+
355
+    /**
356
+     * Updates properties for an address book.
357
+     *
358
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
359
+     * To do the actual updates, you must tell this object which properties
360
+     * you're going to process with the handle() method.
361
+     *
362
+     * Calling the handle method is like telling the PropPatch object "I
363
+     * promise I can handle updating this property".
364
+     *
365
+     * Read the PropPatch documentation for more info and examples.
366
+     *
367
+     * @param string $addressBookId
368
+     * @param \Sabre\DAV\PropPatch $propPatch
369
+     * @return void
370
+     */
371
+    public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
372
+        $supportedProperties = [
373
+            '{DAV:}displayname',
374
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description',
375
+        ];
376
+
377
+        $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
378
+            $updates = [];
379
+            foreach ($mutations as $property => $newValue) {
380
+                switch ($property) {
381
+                    case '{DAV:}displayname':
382
+                        $updates['displayname'] = $newValue;
383
+                        break;
384
+                    case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
385
+                        $updates['description'] = $newValue;
386
+                        break;
387
+                }
388
+            }
389
+            $query = $this->db->getQueryBuilder();
390
+            $query->update('addressbooks');
391
+
392
+            foreach ($updates as $key => $value) {
393
+                $query->set($key, $query->createNamedParameter($value));
394
+            }
395
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
396
+                ->execute();
397
+
398
+            $this->addChange($addressBookId, "", 2);
399
+
400
+            $addressBookRow = $this->getAddressBookById((int)$addressBookId);
401
+            $shares = $this->getShares($addressBookId);
402
+            $this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
403
+
404
+            return true;
405
+        });
406
+    }
407
+
408
+    /**
409
+     * Creates a new address book
410
+     *
411
+     * @param string $principalUri
412
+     * @param string $url Just the 'basename' of the url.
413
+     * @param array $properties
414
+     * @return int
415
+     * @throws BadRequest
416
+     */
417
+    public function createAddressBook($principalUri, $url, array $properties) {
418
+        $values = [
419
+            'displayname' => null,
420
+            'description' => null,
421
+            'principaluri' => $principalUri,
422
+            'uri' => $url,
423
+            'synctoken' => 1
424
+        ];
425
+
426
+        foreach ($properties as $property => $newValue) {
427
+            switch ($property) {
428
+                case '{DAV:}displayname':
429
+                    $values['displayname'] = $newValue;
430
+                    break;
431
+                case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
432
+                    $values['description'] = $newValue;
433
+                    break;
434
+                default:
435
+                    throw new BadRequest('Unknown property: ' . $property);
436
+            }
437
+        }
438
+
439
+        // Fallback to make sure the displayname is set. Some clients may refuse
440
+        // to work with addressbooks not having a displayname.
441
+        if (is_null($values['displayname'])) {
442
+            $values['displayname'] = $url;
443
+        }
444
+
445
+        $query = $this->db->getQueryBuilder();
446
+        $query->insert('addressbooks')
447
+            ->values([
448
+                'uri' => $query->createParameter('uri'),
449
+                'displayname' => $query->createParameter('displayname'),
450
+                'description' => $query->createParameter('description'),
451
+                'principaluri' => $query->createParameter('principaluri'),
452
+                'synctoken' => $query->createParameter('synctoken'),
453
+            ])
454
+            ->setParameters($values)
455
+            ->execute();
456
+
457
+        $addressBookId = $query->getLastInsertId();
458
+        $addressBookRow = $this->getAddressBookById($addressBookId);
459
+        $this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
460
+
461
+        return $addressBookId;
462
+    }
463
+
464
+    /**
465
+     * Deletes an entire addressbook and all its contents
466
+     *
467
+     * @param mixed $addressBookId
468
+     * @return void
469
+     */
470
+    public function deleteAddressBook($addressBookId) {
471
+        $addressBookData = $this->getAddressBookById($addressBookId);
472
+        $shares = $this->getShares($addressBookId);
473
+
474
+        $query = $this->db->getQueryBuilder();
475
+        $query->delete($this->dbCardsTable)
476
+            ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
477
+            ->setParameter('addressbookid', $addressBookId)
478
+            ->execute();
479
+
480
+        $query->delete('addressbookchanges')
481
+            ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
482
+            ->setParameter('addressbookid', $addressBookId)
483
+            ->execute();
484
+
485
+        $query->delete('addressbooks')
486
+            ->where($query->expr()->eq('id', $query->createParameter('id')))
487
+            ->setParameter('id', $addressBookId)
488
+            ->execute();
489
+
490
+        $this->sharingBackend->deleteAllShares($addressBookId);
491
+
492
+        $query->delete($this->dbCardsPropertiesTable)
493
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
494
+            ->execute();
495
+
496
+        if ($addressBookData) {
497
+            $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent((int) $addressBookId, $addressBookData, $shares));
498
+        }
499
+    }
500
+
501
+    /**
502
+     * Returns all cards for a specific addressbook id.
503
+     *
504
+     * This method should return the following properties for each card:
505
+     *   * carddata - raw vcard data
506
+     *   * uri - Some unique url
507
+     *   * lastmodified - A unix timestamp
508
+     *
509
+     * It's recommended to also return the following properties:
510
+     *   * etag - A unique etag. This must change every time the card changes.
511
+     *   * size - The size of the card in bytes.
512
+     *
513
+     * If these last two properties are provided, less time will be spent
514
+     * calculating them. If they are specified, you can also ommit carddata.
515
+     * This may speed up certain requests, especially with large cards.
516
+     *
517
+     * @param mixed $addressBookId
518
+     * @return array
519
+     */
520
+    public function getCards($addressBookId) {
521
+        $query = $this->db->getQueryBuilder();
522
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
523
+            ->from($this->dbCardsTable)
524
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
525
+
526
+        $cards = [];
527
+
528
+        $result = $query->execute();
529
+        while ($row = $result->fetch()) {
530
+            $row['etag'] = '"' . $row['etag'] . '"';
531
+
532
+            $modified = false;
533
+            $row['carddata'] = $this->readBlob($row['carddata'], $modified);
534
+            if ($modified) {
535
+                $row['size'] = strlen($row['carddata']);
536
+            }
537
+
538
+            $cards[] = $row;
539
+        }
540
+        $result->closeCursor();
541
+
542
+        return $cards;
543
+    }
544
+
545
+    /**
546
+     * Returns a specific card.
547
+     *
548
+     * The same set of properties must be returned as with getCards. The only
549
+     * exception is that 'carddata' is absolutely required.
550
+     *
551
+     * If the card does not exist, you must return false.
552
+     *
553
+     * @param mixed $addressBookId
554
+     * @param string $cardUri
555
+     * @return array
556
+     */
557
+    public function getCard($addressBookId, $cardUri) {
558
+        $query = $this->db->getQueryBuilder();
559
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
560
+            ->from($this->dbCardsTable)
561
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
562
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
563
+            ->setMaxResults(1);
564
+
565
+        $result = $query->execute();
566
+        $row = $result->fetch();
567
+        if (!$row) {
568
+            return false;
569
+        }
570
+        $row['etag'] = '"' . $row['etag'] . '"';
571
+
572
+        $modified = false;
573
+        $row['carddata'] = $this->readBlob($row['carddata'], $modified);
574
+        if ($modified) {
575
+            $row['size'] = strlen($row['carddata']);
576
+        }
577
+
578
+        return $row;
579
+    }
580
+
581
+    /**
582
+     * Returns a list of cards.
583
+     *
584
+     * This method should work identical to getCard, but instead return all the
585
+     * cards in the list as an array.
586
+     *
587
+     * If the backend supports this, it may allow for some speed-ups.
588
+     *
589
+     * @param mixed $addressBookId
590
+     * @param string[] $uris
591
+     * @return array
592
+     */
593
+    public function getMultipleCards($addressBookId, array $uris) {
594
+        if (empty($uris)) {
595
+            return [];
596
+        }
597
+
598
+        $chunks = array_chunk($uris, 100);
599
+        $cards = [];
600
+
601
+        $query = $this->db->getQueryBuilder();
602
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
603
+            ->from($this->dbCardsTable)
604
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
605
+            ->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
606
+
607
+        foreach ($chunks as $uris) {
608
+            $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
609
+            $result = $query->execute();
610
+
611
+            while ($row = $result->fetch()) {
612
+                $row['etag'] = '"' . $row['etag'] . '"';
613
+
614
+                $modified = false;
615
+                $row['carddata'] = $this->readBlob($row['carddata'], $modified);
616
+                if ($modified) {
617
+                    $row['size'] = strlen($row['carddata']);
618
+                }
619
+
620
+                $cards[] = $row;
621
+            }
622
+            $result->closeCursor();
623
+        }
624
+        return $cards;
625
+    }
626
+
627
+    /**
628
+     * Creates a new card.
629
+     *
630
+     * The addressbook id will be passed as the first argument. This is the
631
+     * same id as it is returned from the getAddressBooksForUser method.
632
+     *
633
+     * The cardUri is a base uri, and doesn't include the full path. The
634
+     * cardData argument is the vcard body, and is passed as a string.
635
+     *
636
+     * It is possible to return an ETag from this method. This ETag is for the
637
+     * newly created resource, and must be enclosed with double quotes (that
638
+     * is, the string itself must contain the double quotes).
639
+     *
640
+     * You should only return the ETag if you store the carddata as-is. If a
641
+     * subsequent GET request on the same card does not have the same body,
642
+     * byte-by-byte and you did return an ETag here, clients tend to get
643
+     * confused.
644
+     *
645
+     * If you don't return an ETag, you can just return null.
646
+     *
647
+     * @param mixed $addressBookId
648
+     * @param string $cardUri
649
+     * @param string $cardData
650
+     * @return string
651
+     */
652
+    public function createCard($addressBookId, $cardUri, $cardData) {
653
+        $etag = md5($cardData);
654
+        $uid = $this->getUID($cardData);
655
+
656
+        $q = $this->db->getQueryBuilder();
657
+        $q->select('uid')
658
+            ->from($this->dbCardsTable)
659
+            ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
660
+            ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
661
+            ->setMaxResults(1);
662
+        $result = $q->execute();
663
+        $count = (bool)$result->fetchColumn();
664
+        $result->closeCursor();
665
+        if ($count) {
666
+            throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
667
+        }
668
+
669
+        $query = $this->db->getQueryBuilder();
670
+        $query->insert('cards')
671
+            ->values([
672
+                'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
673
+                'uri' => $query->createNamedParameter($cardUri),
674
+                'lastmodified' => $query->createNamedParameter(time()),
675
+                'addressbookid' => $query->createNamedParameter($addressBookId),
676
+                'size' => $query->createNamedParameter(strlen($cardData)),
677
+                'etag' => $query->createNamedParameter($etag),
678
+                'uid' => $query->createNamedParameter($uid),
679
+            ])
680
+            ->execute();
681
+
682
+        $etagCacheKey = "$addressBookId#$cardUri";
683
+        $this->etagCache[$etagCacheKey] = $etag;
684
+
685
+        $this->addChange($addressBookId, $cardUri, 1);
686
+        $this->updateProperties($addressBookId, $cardUri, $cardData);
687
+
688
+        $addressBookData = $this->getAddressBookById($addressBookId);
689
+        $shares = $this->getShares($addressBookId);
690
+        $objectRow = $this->getCard($addressBookId, $cardUri);
691
+        $this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
692
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
693
+            new GenericEvent(null, [
694
+                'addressBookId' => $addressBookId,
695
+                'cardUri' => $cardUri,
696
+                'cardData' => $cardData]));
697
+
698
+        return '"' . $etag . '"';
699
+    }
700
+
701
+    /**
702
+     * Updates a card.
703
+     *
704
+     * The addressbook id will be passed as the first argument. This is the
705
+     * same id as it is returned from the getAddressBooksForUser method.
706
+     *
707
+     * The cardUri is a base uri, and doesn't include the full path. The
708
+     * cardData argument is the vcard body, and is passed as a string.
709
+     *
710
+     * It is possible to return an ETag from this method. This ETag should
711
+     * match that of the updated resource, and must be enclosed with double
712
+     * quotes (that is: the string itself must contain the actual quotes).
713
+     *
714
+     * You should only return the ETag if you store the carddata as-is. If a
715
+     * subsequent GET request on the same card does not have the same body,
716
+     * byte-by-byte and you did return an ETag here, clients tend to get
717
+     * confused.
718
+     *
719
+     * If you don't return an ETag, you can just return null.
720
+     *
721
+     * @param mixed $addressBookId
722
+     * @param string $cardUri
723
+     * @param string $cardData
724
+     * @return string
725
+     */
726
+    public function updateCard($addressBookId, $cardUri, $cardData) {
727
+        $uid = $this->getUID($cardData);
728
+        $etag = md5($cardData);
729
+        $query = $this->db->getQueryBuilder();
730
+
731
+        // check for recently stored etag and stop if it is the same
732
+        $etagCacheKey = "$addressBookId#$cardUri";
733
+        if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
734
+            return '"' . $etag . '"';
735
+        }
736
+
737
+        $query->update($this->dbCardsTable)
738
+            ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
739
+            ->set('lastmodified', $query->createNamedParameter(time()))
740
+            ->set('size', $query->createNamedParameter(strlen($cardData)))
741
+            ->set('etag', $query->createNamedParameter($etag))
742
+            ->set('uid', $query->createNamedParameter($uid))
743
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
744
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
745
+            ->execute();
746
+
747
+        $this->etagCache[$etagCacheKey] = $etag;
748
+
749
+        $this->addChange($addressBookId, $cardUri, 2);
750
+        $this->updateProperties($addressBookId, $cardUri, $cardData);
751
+
752
+        $addressBookData = $this->getAddressBookById($addressBookId);
753
+        $shares = $this->getShares($addressBookId);
754
+        $objectRow = $this->getCard($addressBookId, $cardUri);
755
+        $this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
756
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
757
+            new GenericEvent(null, [
758
+                'addressBookId' => $addressBookId,
759
+                'cardUri' => $cardUri,
760
+                'cardData' => $cardData]));
761
+
762
+        return '"' . $etag . '"';
763
+    }
764
+
765
+    /**
766
+     * Deletes a card
767
+     *
768
+     * @param mixed $addressBookId
769
+     * @param string $cardUri
770
+     * @return bool
771
+     */
772
+    public function deleteCard($addressBookId, $cardUri) {
773
+        $addressBookData = $this->getAddressBookById($addressBookId);
774
+        $shares = $this->getShares($addressBookId);
775
+        $objectRow = $this->getCard($addressBookId, $cardUri);
776
+
777
+        try {
778
+            $cardId = $this->getCardId($addressBookId, $cardUri);
779
+        } catch (\InvalidArgumentException $e) {
780
+            $cardId = null;
781
+        }
782
+        $query = $this->db->getQueryBuilder();
783
+        $ret = $query->delete($this->dbCardsTable)
784
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
785
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
786
+            ->execute();
787
+
788
+        $this->addChange($addressBookId, $cardUri, 3);
789
+
790
+        if ($ret === 1) {
791
+            if ($cardId !== null) {
792
+                $this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
793
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
794
+                    new GenericEvent(null, [
795
+                        'addressBookId' => $addressBookId,
796
+                        'cardUri' => $cardUri]));
797
+
798
+                $this->purgeProperties($addressBookId, $cardId);
799
+            }
800
+            return true;
801
+        }
802
+
803
+        return false;
804
+    }
805
+
806
+    /**
807
+     * The getChanges method returns all the changes that have happened, since
808
+     * the specified syncToken in the specified address book.
809
+     *
810
+     * This function should return an array, such as the following:
811
+     *
812
+     * [
813
+     *   'syncToken' => 'The current synctoken',
814
+     *   'added'   => [
815
+     *      'new.txt',
816
+     *   ],
817
+     *   'modified'   => [
818
+     *      'modified.txt',
819
+     *   ],
820
+     *   'deleted' => [
821
+     *      'foo.php.bak',
822
+     *      'old.txt'
823
+     *   ]
824
+     * ];
825
+     *
826
+     * The returned syncToken property should reflect the *current* syncToken
827
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
828
+     * property. This is needed here too, to ensure the operation is atomic.
829
+     *
830
+     * If the $syncToken argument is specified as null, this is an initial
831
+     * sync, and all members should be reported.
832
+     *
833
+     * The modified property is an array of nodenames that have changed since
834
+     * the last token.
835
+     *
836
+     * The deleted property is an array with nodenames, that have been deleted
837
+     * from collection.
838
+     *
839
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
840
+     * 1, you only have to report changes that happened only directly in
841
+     * immediate descendants. If it's 2, it should also include changes from
842
+     * the nodes below the child collections. (grandchildren)
843
+     *
844
+     * The $limit argument allows a client to specify how many results should
845
+     * be returned at most. If the limit is not specified, it should be treated
846
+     * as infinite.
847
+     *
848
+     * If the limit (infinite or not) is higher than you're willing to return,
849
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
850
+     *
851
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
852
+     * return null.
853
+     *
854
+     * The limit is 'suggestive'. You are free to ignore it.
855
+     *
856
+     * @param string $addressBookId
857
+     * @param string $syncToken
858
+     * @param int $syncLevel
859
+     * @param int $limit
860
+     * @return array
861
+     */
862
+    public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
863
+        // Current synctoken
864
+        $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
865
+        $stmt->execute([$addressBookId]);
866
+        $currentToken = $stmt->fetchColumn(0);
867
+
868
+        if (is_null($currentToken)) {
869
+            return null;
870
+        }
871
+
872
+        $result = [
873
+            'syncToken' => $currentToken,
874
+            'added' => [],
875
+            'modified' => [],
876
+            'deleted' => [],
877
+        ];
878
+
879
+        if ($syncToken) {
880
+            $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
881
+            if ($limit > 0) {
882
+                $query .= " LIMIT " . (int)$limit;
883
+            }
884
+
885
+            // Fetching all changes
886
+            $stmt = $this->db->prepare($query);
887
+            $stmt->execute([$syncToken, $currentToken, $addressBookId]);
888
+
889
+            $changes = [];
890
+
891
+            // This loop ensures that any duplicates are overwritten, only the
892
+            // last change on a node is relevant.
893
+            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
894
+                $changes[$row['uri']] = $row['operation'];
895
+            }
896
+
897
+            foreach ($changes as $uri => $operation) {
898
+                switch ($operation) {
899
+                    case 1:
900
+                        $result['added'][] = $uri;
901
+                        break;
902
+                    case 2:
903
+                        $result['modified'][] = $uri;
904
+                        break;
905
+                    case 3:
906
+                        $result['deleted'][] = $uri;
907
+                        break;
908
+                }
909
+            }
910
+        } else {
911
+            // No synctoken supplied, this is the initial sync.
912
+            $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
913
+            $stmt = $this->db->prepare($query);
914
+            $stmt->execute([$addressBookId]);
915
+
916
+            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
917
+        }
918
+        return $result;
919
+    }
920
+
921
+    /**
922
+     * Adds a change record to the addressbookchanges table.
923
+     *
924
+     * @param mixed $addressBookId
925
+     * @param string $objectUri
926
+     * @param int $operation 1 = add, 2 = modify, 3 = delete
927
+     * @return void
928
+     */
929
+    protected function addChange($addressBookId, $objectUri, $operation) {
930
+        $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
931
+        $stmt = $this->db->prepare($sql);
932
+        $stmt->execute([
933
+            $objectUri,
934
+            $addressBookId,
935
+            $operation,
936
+            $addressBookId
937
+        ]);
938
+        $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
939
+        $stmt->execute([
940
+            $addressBookId
941
+        ]);
942
+    }
943
+
944
+    /**
945
+     * @param resource|string $cardData
946
+     * @param bool $modified
947
+     * @return string
948
+     */
949
+    private function readBlob($cardData, &$modified = false) {
950
+        if (is_resource($cardData)) {
951
+            $cardData = stream_get_contents($cardData);
952
+        }
953
+
954
+        $cardDataArray = explode("\r\n", $cardData);
955
+
956
+        $cardDataFiltered = [];
957
+        $removingPhoto = false;
958
+        foreach ($cardDataArray as $line) {
959
+            if (strpos($line, 'PHOTO:data:') === 0
960
+                && strpos($line, 'PHOTO:data:image/') !== 0) {
961
+                // Filter out PHOTO data of non-images
962
+                $removingPhoto = true;
963
+                $modified = true;
964
+                continue;
965
+            }
966
+
967
+            if ($removingPhoto) {
968
+                if (strpos($line, ' ') === 0) {
969
+                    continue;
970
+                }
971
+                // No leading space means this is a new property
972
+                $removingPhoto = false;
973
+            }
974
+
975
+            $cardDataFiltered[] = $line;
976
+        }
977
+
978
+        return implode("\r\n", $cardDataFiltered);
979
+    }
980
+
981
+    /**
982
+     * @param IShareable $shareable
983
+     * @param string[] $add
984
+     * @param string[] $remove
985
+     */
986
+    public function updateShares(IShareable $shareable, $add, $remove) {
987
+        $addressBookId = $shareable->getResourceId();
988
+        $addressBookData = $this->getAddressBookById($addressBookId);
989
+        $oldShares = $this->getShares($addressBookId);
990
+
991
+        $this->sharingBackend->updateShares($shareable, $add, $remove);
992
+
993
+        $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
994
+    }
995
+
996
+    /**
997
+     * Search contacts in a specific address-book
998
+     *
999
+     * @param int $addressBookId
1000
+     * @param string $pattern which should match within the $searchProperties
1001
+     * @param array $searchProperties defines the properties within the query pattern should match
1002
+     * @param array $options = array() to define the search behavior
1003
+     *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1004
+     *    - 'limit' - Set a numeric limit for the search results
1005
+     *    - 'offset' - Set the offset for the limited search results
1006
+     * @return array an array of contacts which are arrays of key-value-pairs
1007
+     */
1008
+    public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1009
+        return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1010
+    }
1011
+
1012
+    /**
1013
+     * Search contacts in all address-books accessible by a user
1014
+     *
1015
+     * @param string $principalUri
1016
+     * @param string $pattern
1017
+     * @param array $searchProperties
1018
+     * @param array $options
1019
+     * @return array
1020
+     */
1021
+    public function searchPrincipalUri(string $principalUri,
1022
+                                        string $pattern,
1023
+                                        array $searchProperties,
1024
+                                        array $options = []): array {
1025
+        $addressBookIds = array_map(static function ($row):int {
1026
+            return (int) $row['id'];
1027
+        }, $this->getAddressBooksForUser($principalUri));
1028
+
1029
+        return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1030
+    }
1031
+
1032
+    /**
1033
+     * @param array $addressBookIds
1034
+     * @param string $pattern
1035
+     * @param array $searchProperties
1036
+     * @param array $options
1037
+     * @return array
1038
+     */
1039
+    private function searchByAddressBookIds(array $addressBookIds,
1040
+                                            string $pattern,
1041
+                                            array $searchProperties,
1042
+                                            array $options = []): array {
1043
+        $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1044
+
1045
+        $query2 = $this->db->getQueryBuilder();
1046
+
1047
+        $addressBookOr = $query2->expr()->orX();
1048
+        foreach ($addressBookIds as $addressBookId) {
1049
+            $addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1050
+        }
1051
+
1052
+        if ($addressBookOr->count() === 0) {
1053
+            return [];
1054
+        }
1055
+
1056
+        $propertyOr = $query2->expr()->orX();
1057
+        foreach ($searchProperties as $property) {
1058
+            if ($escapePattern) {
1059
+                if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1060
+                    // There can be no spaces in emails
1061
+                    continue;
1062
+                }
1063
+
1064
+                if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1065
+                    // There can be no chars in cloud ids which are not valid for user ids plus :/
1066
+                    // worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1067
+                    continue;
1068
+                }
1069
+            }
1070
+
1071
+            $propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1072
+        }
1073
+
1074
+        if ($propertyOr->count() === 0) {
1075
+            return [];
1076
+        }
1077
+
1078
+        $query2->selectDistinct('cp.cardid')
1079
+            ->from($this->dbCardsPropertiesTable, 'cp')
1080
+            ->andWhere($addressBookOr)
1081
+            ->andWhere($propertyOr);
1082
+
1083
+        // No need for like when the pattern is empty
1084
+        if ('' !== $pattern) {
1085
+            if (!$escapePattern) {
1086
+                $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1087
+            } else {
1088
+                $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1089
+            }
1090
+        }
1091
+
1092
+        if (isset($options['limit'])) {
1093
+            $query2->setMaxResults($options['limit']);
1094
+        }
1095
+        if (isset($options['offset'])) {
1096
+            $query2->setFirstResult($options['offset']);
1097
+        }
1098
+
1099
+        $result = $query2->execute();
1100
+        $matches = $result->fetchAll();
1101
+        $result->closeCursor();
1102
+        $matches = array_map(function ($match) {
1103
+            return (int)$match['cardid'];
1104
+        }, $matches);
1105
+
1106
+        $query = $this->db->getQueryBuilder();
1107
+        $query->select('c.addressbookid', 'c.carddata', 'c.uri')
1108
+            ->from($this->dbCardsTable, 'c')
1109
+            ->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1110
+
1111
+        $result = $query->execute();
1112
+        $cards = $result->fetchAll();
1113
+
1114
+        $result->closeCursor();
1115
+
1116
+        return array_map(function ($array) {
1117
+            $array['addressbookid'] = (int) $array['addressbookid'];
1118
+            $modified = false;
1119
+            $array['carddata'] = $this->readBlob($array['carddata'], $modified);
1120
+            if ($modified) {
1121
+                $array['size'] = strlen($array['carddata']);
1122
+            }
1123
+            return $array;
1124
+        }, $cards);
1125
+    }
1126
+
1127
+    /**
1128
+     * @param int $bookId
1129
+     * @param string $name
1130
+     * @return array
1131
+     */
1132
+    public function collectCardProperties($bookId, $name) {
1133
+        $query = $this->db->getQueryBuilder();
1134
+        $result = $query->selectDistinct('value')
1135
+            ->from($this->dbCardsPropertiesTable)
1136
+            ->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1137
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1138
+            ->execute();
1139
+
1140
+        $all = $result->fetchAll(PDO::FETCH_COLUMN);
1141
+        $result->closeCursor();
1142
+
1143
+        return $all;
1144
+    }
1145
+
1146
+    /**
1147
+     * get URI from a given contact
1148
+     *
1149
+     * @param int $id
1150
+     * @return string
1151
+     */
1152
+    public function getCardUri($id) {
1153
+        $query = $this->db->getQueryBuilder();
1154
+        $query->select('uri')->from($this->dbCardsTable)
1155
+            ->where($query->expr()->eq('id', $query->createParameter('id')))
1156
+            ->setParameter('id', $id);
1157
+
1158
+        $result = $query->execute();
1159
+        $uri = $result->fetch();
1160
+        $result->closeCursor();
1161
+
1162
+        if (!isset($uri['uri'])) {
1163
+            throw new \InvalidArgumentException('Card does not exists: ' . $id);
1164
+        }
1165
+
1166
+        return $uri['uri'];
1167
+    }
1168
+
1169
+    /**
1170
+     * return contact with the given URI
1171
+     *
1172
+     * @param int $addressBookId
1173
+     * @param string $uri
1174
+     * @returns array
1175
+     */
1176
+    public function getContact($addressBookId, $uri) {
1177
+        $result = [];
1178
+        $query = $this->db->getQueryBuilder();
1179
+        $query->select('*')->from($this->dbCardsTable)
1180
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1181
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1182
+        $queryResult = $query->execute();
1183
+        $contact = $queryResult->fetch();
1184
+        $queryResult->closeCursor();
1185
+
1186
+        if (is_array($contact)) {
1187
+            $modified = false;
1188
+            $contact['etag'] = '"' . $contact['etag'] . '"';
1189
+            $contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1190
+            if ($modified) {
1191
+                $contact['size'] = strlen($contact['carddata']);
1192
+            }
1193
+
1194
+            $result = $contact;
1195
+        }
1196
+
1197
+        return $result;
1198
+    }
1199
+
1200
+    /**
1201
+     * Returns the list of people whom this address book is shared with.
1202
+     *
1203
+     * Every element in this array should have the following properties:
1204
+     *   * href - Often a mailto: address
1205
+     *   * commonName - Optional, for example a first + last name
1206
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1207
+     *   * readOnly - boolean
1208
+     *   * summary - Optional, a description for the share
1209
+     *
1210
+     * @return array
1211
+     */
1212
+    public function getShares($addressBookId) {
1213
+        return $this->sharingBackend->getShares($addressBookId);
1214
+    }
1215
+
1216
+    /**
1217
+     * update properties table
1218
+     *
1219
+     * @param int $addressBookId
1220
+     * @param string $cardUri
1221
+     * @param string $vCardSerialized
1222
+     */
1223
+    protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1224
+        $cardId = $this->getCardId($addressBookId, $cardUri);
1225
+        $vCard = $this->readCard($vCardSerialized);
1226
+
1227
+        $this->purgeProperties($addressBookId, $cardId);
1228
+
1229
+        $query = $this->db->getQueryBuilder();
1230
+        $query->insert($this->dbCardsPropertiesTable)
1231
+            ->values(
1232
+                [
1233
+                    'addressbookid' => $query->createNamedParameter($addressBookId),
1234
+                    'cardid' => $query->createNamedParameter($cardId),
1235
+                    'name' => $query->createParameter('name'),
1236
+                    'value' => $query->createParameter('value'),
1237
+                    'preferred' => $query->createParameter('preferred')
1238
+                ]
1239
+            );
1240
+
1241
+        foreach ($vCard->children() as $property) {
1242
+            if (!in_array($property->name, self::$indexProperties)) {
1243
+                continue;
1244
+            }
1245
+            $preferred = 0;
1246
+            foreach ($property->parameters as $parameter) {
1247
+                if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1248
+                    $preferred = 1;
1249
+                    break;
1250
+                }
1251
+            }
1252
+            $query->setParameter('name', $property->name);
1253
+            $query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1254
+            $query->setParameter('preferred', $preferred);
1255
+            $query->execute();
1256
+        }
1257
+    }
1258
+
1259
+    /**
1260
+     * read vCard data into a vCard object
1261
+     *
1262
+     * @param string $cardData
1263
+     * @return VCard
1264
+     */
1265
+    protected function readCard($cardData) {
1266
+        return Reader::read($cardData);
1267
+    }
1268
+
1269
+    /**
1270
+     * delete all properties from a given card
1271
+     *
1272
+     * @param int $addressBookId
1273
+     * @param int $cardId
1274
+     */
1275
+    protected function purgeProperties($addressBookId, $cardId) {
1276
+        $query = $this->db->getQueryBuilder();
1277
+        $query->delete($this->dbCardsPropertiesTable)
1278
+            ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1279
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1280
+        $query->execute();
1281
+    }
1282
+
1283
+    /**
1284
+     * get ID from a given contact
1285
+     *
1286
+     * @param int $addressBookId
1287
+     * @param string $uri
1288
+     * @return int
1289
+     */
1290
+    protected function getCardId($addressBookId, $uri) {
1291
+        $query = $this->db->getQueryBuilder();
1292
+        $query->select('id')->from($this->dbCardsTable)
1293
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1294
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1295
+
1296
+        $result = $query->execute();
1297
+        $cardIds = $result->fetch();
1298
+        $result->closeCursor();
1299
+
1300
+        if (!isset($cardIds['id'])) {
1301
+            throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1302
+        }
1303
+
1304
+        return (int)$cardIds['id'];
1305
+    }
1306
+
1307
+    /**
1308
+     * For shared address books the sharee is set in the ACL of the address book
1309
+     *
1310
+     * @param $addressBookId
1311
+     * @param $acl
1312
+     * @return array
1313
+     */
1314
+    public function applyShareAcl($addressBookId, $acl) {
1315
+        return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1316
+    }
1317
+
1318
+    private function convertPrincipal($principalUri, $toV2) {
1319
+        if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1320
+            list(, $name) = \Sabre\Uri\split($principalUri);
1321
+            if ($toV2 === true) {
1322
+                return "principals/users/$name";
1323
+            }
1324
+            return "principals/$name";
1325
+        }
1326
+        return $principalUri;
1327
+    }
1328
+
1329
+    private function addOwnerPrincipal(&$addressbookInfo) {
1330
+        $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1331
+        $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1332
+        if (isset($addressbookInfo[$ownerPrincipalKey])) {
1333
+            $uri = $addressbookInfo[$ownerPrincipalKey];
1334
+        } else {
1335
+            $uri = $addressbookInfo['principaluri'];
1336
+        }
1337
+
1338
+        $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1339
+        if (isset($principalInformation['{DAV:}displayname'])) {
1340
+            $addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1341
+        }
1342
+    }
1343
+
1344
+    /**
1345
+     * Extract UID from vcard
1346
+     *
1347
+     * @param string $cardData the vcard raw data
1348
+     * @return string the uid
1349
+     * @throws BadRequest if no UID is available
1350
+     */
1351
+    private function getUID($cardData) {
1352
+        if ($cardData != '') {
1353
+            $vCard = Reader::read($cardData);
1354
+            if ($vCard->UID) {
1355
+                $uid = $vCard->UID->getValue();
1356
+                return $uid;
1357
+            }
1358
+            // should already be handled, but just in case
1359
+            throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1360
+        }
1361
+        // should already be handled, but just in case
1362
+        throw new BadRequest('vCard can not be empty');
1363
+    }
1364 1364
 }
Please login to merge, or discard this patch.