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