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