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