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