Passed
Push — master ( bbb39c...5026d2 )
by Christoph
27:34 queued 12:27
created
apps/dav/lib/CardDAV/CardDavBackend.php 2 patches
Indentation   +1320 added lines, -1320 removed lines patch added patch discarded remove patch
@@ -62,1324 +62,1324 @@
 block discarded – undo
62 62
 use Symfony\Component\EventDispatcher\GenericEvent;
63 63
 
64 64
 class CardDavBackend implements BackendInterface, SyncSupport {
65
-	public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
66
-	public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
67
-
68
-	/** @var Principal */
69
-	private $principalBackend;
70
-
71
-	/** @var string */
72
-	private $dbCardsTable = 'cards';
73
-
74
-	/** @var string */
75
-	private $dbCardsPropertiesTable = 'cards_properties';
76
-
77
-	/** @var IDBConnection */
78
-	private $db;
79
-
80
-	/** @var Backend */
81
-	private $sharingBackend;
82
-
83
-	/** @var array properties to index */
84
-	public static $indexProperties = [
85
-		'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
86
-		'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO',
87
-		'CLOUD', 'X-SOCIALPROFILE'];
88
-
89
-	/**
90
-	 * @var string[] Map of uid => display name
91
-	 */
92
-	protected $userDisplayNames;
93
-
94
-	/** @var IUserManager */
95
-	private $userManager;
96
-
97
-	/** @var IEventDispatcher */
98
-	private $dispatcher;
99
-
100
-	/** @var EventDispatcherInterface */
101
-	private $legacyDispatcher;
102
-
103
-	private $etagCache = [];
104
-
105
-	/**
106
-	 * CardDavBackend constructor.
107
-	 *
108
-	 * @param IDBConnection $db
109
-	 * @param Principal $principalBackend
110
-	 * @param IUserManager $userManager
111
-	 * @param IGroupManager $groupManager
112
-	 * @param IEventDispatcher $dispatcher
113
-	 * @param EventDispatcherInterface $legacyDispatcher
114
-	 */
115
-	public function __construct(IDBConnection $db,
116
-								Principal $principalBackend,
117
-								IUserManager $userManager,
118
-								IGroupManager $groupManager,
119
-								IEventDispatcher $dispatcher,
120
-								EventDispatcherInterface $legacyDispatcher) {
121
-		$this->db = $db;
122
-		$this->principalBackend = $principalBackend;
123
-		$this->userManager = $userManager;
124
-		$this->dispatcher = $dispatcher;
125
-		$this->legacyDispatcher = $legacyDispatcher;
126
-		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
127
-	}
128
-
129
-	/**
130
-	 * Return the number of address books for a principal
131
-	 *
132
-	 * @param $principalUri
133
-	 * @return int
134
-	 */
135
-	public function getAddressBooksForUserCount($principalUri) {
136
-		$principalUri = $this->convertPrincipal($principalUri, true);
137
-		$query = $this->db->getQueryBuilder();
138
-		$query->select($query->func()->count('*'))
139
-			->from('addressbooks')
140
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
141
-
142
-		$result = $query->execute();
143
-		$column = (int) $result->fetchOne();
144
-		$result->closeCursor();
145
-		return $column;
146
-	}
147
-
148
-	/**
149
-	 * Returns the list of address books for a specific user.
150
-	 *
151
-	 * Every addressbook should have the following properties:
152
-	 *   id - an arbitrary unique id
153
-	 *   uri - the 'basename' part of the url
154
-	 *   principaluri - Same as the passed parameter
155
-	 *
156
-	 * Any additional clark-notation property may be passed besides this. Some
157
-	 * common ones are :
158
-	 *   {DAV:}displayname
159
-	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
160
-	 *   {http://calendarserver.org/ns/}getctag
161
-	 *
162
-	 * @param string $principalUri
163
-	 * @return array
164
-	 */
165
-	public function getAddressBooksForUser($principalUri) {
166
-		$principalUriOriginal = $principalUri;
167
-		$principalUri = $this->convertPrincipal($principalUri, true);
168
-		$query = $this->db->getQueryBuilder();
169
-		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
170
-			->from('addressbooks')
171
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
172
-
173
-		$addressBooks = [];
174
-
175
-		$result = $query->execute();
176
-		while ($row = $result->fetch()) {
177
-			$addressBooks[$row['id']] = [
178
-				'id' => $row['id'],
179
-				'uri' => $row['uri'],
180
-				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
181
-				'{DAV:}displayname' => $row['displayname'],
182
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
183
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
184
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
185
-			];
186
-
187
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
188
-		}
189
-		$result->closeCursor();
190
-
191
-		// query for shared addressbooks
192
-		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
193
-		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
194
-
195
-		$principals[] = $principalUri;
196
-
197
-		$query = $this->db->getQueryBuilder();
198
-		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
199
-			->from('dav_shares', 's')
200
-			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
201
-			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
202
-			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
203
-			->setParameter('type', 'addressbook')
204
-			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
205
-			->execute();
206
-
207
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
208
-		while ($row = $result->fetch()) {
209
-			if ($row['principaluri'] === $principalUri) {
210
-				continue;
211
-			}
212
-
213
-			$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
214
-			if (isset($addressBooks[$row['id']])) {
215
-				if ($readOnly) {
216
-					// New share can not have more permissions then the old one.
217
-					continue;
218
-				}
219
-				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
220
-					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
221
-					// Old share is already read-write, no more permissions can be gained
222
-					continue;
223
-				}
224
-			}
225
-
226
-			[, $name] = \Sabre\Uri\split($row['principaluri']);
227
-			$uri = $row['uri'] . '_shared_by_' . $name;
228
-			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
229
-
230
-			$addressBooks[$row['id']] = [
231
-				'id' => $row['id'],
232
-				'uri' => $uri,
233
-				'principaluri' => $principalUriOriginal,
234
-				'{DAV:}displayname' => $displayName,
235
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
236
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
237
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
238
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
239
-				$readOnlyPropertyName => $readOnly,
240
-			];
241
-
242
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
243
-		}
244
-		$result->closeCursor();
245
-
246
-		return array_values($addressBooks);
247
-	}
248
-
249
-	public function getUsersOwnAddressBooks($principalUri) {
250
-		$principalUri = $this->convertPrincipal($principalUri, true);
251
-		$query = $this->db->getQueryBuilder();
252
-		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
253
-			->from('addressbooks')
254
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
255
-
256
-		$addressBooks = [];
257
-
258
-		$result = $query->execute();
259
-		while ($row = $result->fetch()) {
260
-			$addressBooks[$row['id']] = [
261
-				'id' => $row['id'],
262
-				'uri' => $row['uri'],
263
-				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
264
-				'{DAV:}displayname' => $row['displayname'],
265
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
266
-				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
267
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
268
-			];
269
-
270
-			$this->addOwnerPrincipal($addressBooks[$row['id']]);
271
-		}
272
-		$result->closeCursor();
273
-
274
-		return array_values($addressBooks);
275
-	}
276
-
277
-	private function getUserDisplayName($uid) {
278
-		if (!isset($this->userDisplayNames[$uid])) {
279
-			$user = $this->userManager->get($uid);
280
-
281
-			if ($user instanceof IUser) {
282
-				$this->userDisplayNames[$uid] = $user->getDisplayName();
283
-			} else {
284
-				$this->userDisplayNames[$uid] = $uid;
285
-			}
286
-		}
287
-
288
-		return $this->userDisplayNames[$uid];
289
-	}
290
-
291
-	/**
292
-	 * @param int $addressBookId
293
-	 */
294
-	public function getAddressBookById($addressBookId) {
295
-		$query = $this->db->getQueryBuilder();
296
-		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
297
-			->from('addressbooks')
298
-			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
299
-			->execute();
300
-
301
-		$row = $result->fetch();
302
-		$result->closeCursor();
303
-		if ($row === false) {
304
-			return null;
305
-		}
306
-
307
-		$addressBook = [
308
-			'id' => $row['id'],
309
-			'uri' => $row['uri'],
310
-			'principaluri' => $row['principaluri'],
311
-			'{DAV:}displayname' => $row['displayname'],
312
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
313
-			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
314
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
315
-		];
316
-
317
-		$this->addOwnerPrincipal($addressBook);
318
-
319
-		return $addressBook;
320
-	}
321
-
322
-	/**
323
-	 * @param $addressBookUri
324
-	 * @return array|null
325
-	 */
326
-	public function getAddressBooksByUri($principal, $addressBookUri) {
327
-		$query = $this->db->getQueryBuilder();
328
-		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
329
-			->from('addressbooks')
330
-			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
331
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
332
-			->setMaxResults(1)
333
-			->execute();
334
-
335
-		$row = $result->fetch();
336
-		$result->closeCursor();
337
-		if ($row === false) {
338
-			return null;
339
-		}
340
-
341
-		$addressBook = [
342
-			'id' => $row['id'],
343
-			'uri' => $row['uri'],
344
-			'principaluri' => $row['principaluri'],
345
-			'{DAV:}displayname' => $row['displayname'],
346
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
347
-			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
348
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
349
-		];
350
-
351
-		$this->addOwnerPrincipal($addressBook);
352
-
353
-		return $addressBook;
354
-	}
355
-
356
-	/**
357
-	 * Updates properties for an address book.
358
-	 *
359
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
360
-	 * To do the actual updates, you must tell this object which properties
361
-	 * you're going to process with the handle() method.
362
-	 *
363
-	 * Calling the handle method is like telling the PropPatch object "I
364
-	 * promise I can handle updating this property".
365
-	 *
366
-	 * Read the PropPatch documentation for more info and examples.
367
-	 *
368
-	 * @param string $addressBookId
369
-	 * @param \Sabre\DAV\PropPatch $propPatch
370
-	 * @return void
371
-	 */
372
-	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
373
-		$supportedProperties = [
374
-			'{DAV:}displayname',
375
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
376
-		];
377
-
378
-		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
379
-			$updates = [];
380
-			foreach ($mutations as $property => $newValue) {
381
-				switch ($property) {
382
-					case '{DAV:}displayname':
383
-						$updates['displayname'] = $newValue;
384
-						break;
385
-					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
386
-						$updates['description'] = $newValue;
387
-						break;
388
-				}
389
-			}
390
-			$query = $this->db->getQueryBuilder();
391
-			$query->update('addressbooks');
392
-
393
-			foreach ($updates as $key => $value) {
394
-				$query->set($key, $query->createNamedParameter($value));
395
-			}
396
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
397
-				->execute();
398
-
399
-			$this->addChange($addressBookId, "", 2);
400
-
401
-			$addressBookRow = $this->getAddressBookById((int)$addressBookId);
402
-			$shares = $this->getShares($addressBookId);
403
-			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
404
-
405
-			return true;
406
-		});
407
-	}
408
-
409
-	/**
410
-	 * Creates a new address book
411
-	 *
412
-	 * @param string $principalUri
413
-	 * @param string $url Just the 'basename' of the url.
414
-	 * @param array $properties
415
-	 * @return int
416
-	 * @throws BadRequest
417
-	 */
418
-	public function createAddressBook($principalUri, $url, array $properties) {
419
-		$values = [
420
-			'displayname' => null,
421
-			'description' => null,
422
-			'principaluri' => $principalUri,
423
-			'uri' => $url,
424
-			'synctoken' => 1
425
-		];
426
-
427
-		foreach ($properties as $property => $newValue) {
428
-			switch ($property) {
429
-				case '{DAV:}displayname':
430
-					$values['displayname'] = $newValue;
431
-					break;
432
-				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
433
-					$values['description'] = $newValue;
434
-					break;
435
-				default:
436
-					throw new BadRequest('Unknown property: ' . $property);
437
-			}
438
-		}
439
-
440
-		// Fallback to make sure the displayname is set. Some clients may refuse
441
-		// to work with addressbooks not having a displayname.
442
-		if (is_null($values['displayname'])) {
443
-			$values['displayname'] = $url;
444
-		}
445
-
446
-		$query = $this->db->getQueryBuilder();
447
-		$query->insert('addressbooks')
448
-			->values([
449
-				'uri' => $query->createParameter('uri'),
450
-				'displayname' => $query->createParameter('displayname'),
451
-				'description' => $query->createParameter('description'),
452
-				'principaluri' => $query->createParameter('principaluri'),
453
-				'synctoken' => $query->createParameter('synctoken'),
454
-			])
455
-			->setParameters($values)
456
-			->execute();
457
-
458
-		$addressBookId = $query->getLastInsertId();
459
-		$addressBookRow = $this->getAddressBookById($addressBookId);
460
-		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
461
-
462
-		return $addressBookId;
463
-	}
464
-
465
-	/**
466
-	 * Deletes an entire addressbook and all its contents
467
-	 *
468
-	 * @param mixed $addressBookId
469
-	 * @return void
470
-	 */
471
-	public function deleteAddressBook($addressBookId) {
472
-		$addressBookData = $this->getAddressBookById($addressBookId);
473
-		$shares = $this->getShares($addressBookId);
474
-
475
-		$query = $this->db->getQueryBuilder();
476
-		$query->delete($this->dbCardsTable)
477
-			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
478
-			->setParameter('addressbookid', $addressBookId)
479
-			->execute();
480
-
481
-		$query->delete('addressbookchanges')
482
-			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
483
-			->setParameter('addressbookid', $addressBookId)
484
-			->execute();
485
-
486
-		$query->delete('addressbooks')
487
-			->where($query->expr()->eq('id', $query->createParameter('id')))
488
-			->setParameter('id', $addressBookId)
489
-			->execute();
490
-
491
-		$this->sharingBackend->deleteAllShares($addressBookId);
492
-
493
-		$query->delete($this->dbCardsPropertiesTable)
494
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
495
-			->execute();
496
-
497
-		if ($addressBookData) {
498
-			$this->dispatcher->dispatchTyped(new AddressBookDeletedEvent((int) $addressBookId, $addressBookData, $shares));
499
-		}
500
-	}
501
-
502
-	/**
503
-	 * Returns all cards for a specific addressbook id.
504
-	 *
505
-	 * This method should return the following properties for each card:
506
-	 *   * carddata - raw vcard data
507
-	 *   * uri - Some unique url
508
-	 *   * lastmodified - A unix timestamp
509
-	 *
510
-	 * It's recommended to also return the following properties:
511
-	 *   * etag - A unique etag. This must change every time the card changes.
512
-	 *   * size - The size of the card in bytes.
513
-	 *
514
-	 * If these last two properties are provided, less time will be spent
515
-	 * calculating them. If they are specified, you can also ommit carddata.
516
-	 * This may speed up certain requests, especially with large cards.
517
-	 *
518
-	 * @param mixed $addressBookId
519
-	 * @return array
520
-	 */
521
-	public function getCards($addressBookId) {
522
-		$query = $this->db->getQueryBuilder();
523
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
524
-			->from($this->dbCardsTable)
525
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
526
-
527
-		$cards = [];
528
-
529
-		$result = $query->execute();
530
-		while ($row = $result->fetch()) {
531
-			$row['etag'] = '"' . $row['etag'] . '"';
532
-
533
-			$modified = false;
534
-			$row['carddata'] = $this->readBlob($row['carddata'], $modified);
535
-			if ($modified) {
536
-				$row['size'] = strlen($row['carddata']);
537
-			}
538
-
539
-			$cards[] = $row;
540
-		}
541
-		$result->closeCursor();
542
-
543
-		return $cards;
544
-	}
545
-
546
-	/**
547
-	 * Returns a specific card.
548
-	 *
549
-	 * The same set of properties must be returned as with getCards. The only
550
-	 * exception is that 'carddata' is absolutely required.
551
-	 *
552
-	 * If the card does not exist, you must return false.
553
-	 *
554
-	 * @param mixed $addressBookId
555
-	 * @param string $cardUri
556
-	 * @return array
557
-	 */
558
-	public function getCard($addressBookId, $cardUri) {
559
-		$query = $this->db->getQueryBuilder();
560
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
561
-			->from($this->dbCardsTable)
562
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
563
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
564
-			->setMaxResults(1);
565
-
566
-		$result = $query->execute();
567
-		$row = $result->fetch();
568
-		if (!$row) {
569
-			return false;
570
-		}
571
-		$row['etag'] = '"' . $row['etag'] . '"';
572
-
573
-		$modified = false;
574
-		$row['carddata'] = $this->readBlob($row['carddata'], $modified);
575
-		if ($modified) {
576
-			$row['size'] = strlen($row['carddata']);
577
-		}
578
-
579
-		return $row;
580
-	}
581
-
582
-	/**
583
-	 * Returns a list of cards.
584
-	 *
585
-	 * This method should work identical to getCard, but instead return all the
586
-	 * cards in the list as an array.
587
-	 *
588
-	 * If the backend supports this, it may allow for some speed-ups.
589
-	 *
590
-	 * @param mixed $addressBookId
591
-	 * @param string[] $uris
592
-	 * @return array
593
-	 */
594
-	public function getMultipleCards($addressBookId, array $uris) {
595
-		if (empty($uris)) {
596
-			return [];
597
-		}
598
-
599
-		$chunks = array_chunk($uris, 100);
600
-		$cards = [];
601
-
602
-		$query = $this->db->getQueryBuilder();
603
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
604
-			->from($this->dbCardsTable)
605
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
606
-			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
607
-
608
-		foreach ($chunks as $uris) {
609
-			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
610
-			$result = $query->execute();
611
-
612
-			while ($row = $result->fetch()) {
613
-				$row['etag'] = '"' . $row['etag'] . '"';
614
-
615
-				$modified = false;
616
-				$row['carddata'] = $this->readBlob($row['carddata'], $modified);
617
-				if ($modified) {
618
-					$row['size'] = strlen($row['carddata']);
619
-				}
620
-
621
-				$cards[] = $row;
622
-			}
623
-			$result->closeCursor();
624
-		}
625
-		return $cards;
626
-	}
627
-
628
-	/**
629
-	 * Creates a new card.
630
-	 *
631
-	 * The addressbook id will be passed as the first argument. This is the
632
-	 * same id as it is returned from the getAddressBooksForUser method.
633
-	 *
634
-	 * The cardUri is a base uri, and doesn't include the full path. The
635
-	 * cardData argument is the vcard body, and is passed as a string.
636
-	 *
637
-	 * It is possible to return an ETag from this method. This ETag is for the
638
-	 * newly created resource, and must be enclosed with double quotes (that
639
-	 * is, the string itself must contain the double quotes).
640
-	 *
641
-	 * You should only return the ETag if you store the carddata as-is. If a
642
-	 * subsequent GET request on the same card does not have the same body,
643
-	 * byte-by-byte and you did return an ETag here, clients tend to get
644
-	 * confused.
645
-	 *
646
-	 * If you don't return an ETag, you can just return null.
647
-	 *
648
-	 * @param mixed $addressBookId
649
-	 * @param string $cardUri
650
-	 * @param string $cardData
651
-	 * @return string
652
-	 */
653
-	public function createCard($addressBookId, $cardUri, $cardData) {
654
-		$etag = md5($cardData);
655
-		$uid = $this->getUID($cardData);
656
-
657
-		$q = $this->db->getQueryBuilder();
658
-		$q->select('uid')
659
-			->from($this->dbCardsTable)
660
-			->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
661
-			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
662
-			->setMaxResults(1);
663
-		$result = $q->execute();
664
-		$count = (bool)$result->fetchOne();
665
-		$result->closeCursor();
666
-		if ($count) {
667
-			throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
668
-		}
669
-
670
-		$query = $this->db->getQueryBuilder();
671
-		$query->insert('cards')
672
-			->values([
673
-				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
674
-				'uri' => $query->createNamedParameter($cardUri),
675
-				'lastmodified' => $query->createNamedParameter(time()),
676
-				'addressbookid' => $query->createNamedParameter($addressBookId),
677
-				'size' => $query->createNamedParameter(strlen($cardData)),
678
-				'etag' => $query->createNamedParameter($etag),
679
-				'uid' => $query->createNamedParameter($uid),
680
-			])
681
-			->execute();
682
-
683
-		$etagCacheKey = "$addressBookId#$cardUri";
684
-		$this->etagCache[$etagCacheKey] = $etag;
685
-
686
-		$this->addChange($addressBookId, $cardUri, 1);
687
-		$this->updateProperties($addressBookId, $cardUri, $cardData);
688
-
689
-		$addressBookData = $this->getAddressBookById($addressBookId);
690
-		$shares = $this->getShares($addressBookId);
691
-		$objectRow = $this->getCard($addressBookId, $cardUri);
692
-		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
693
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
694
-			new GenericEvent(null, [
695
-				'addressBookId' => $addressBookId,
696
-				'cardUri' => $cardUri,
697
-				'cardData' => $cardData]));
698
-
699
-		return '"' . $etag . '"';
700
-	}
701
-
702
-	/**
703
-	 * Updates a card.
704
-	 *
705
-	 * The addressbook id will be passed as the first argument. This is the
706
-	 * same id as it is returned from the getAddressBooksForUser method.
707
-	 *
708
-	 * The cardUri is a base uri, and doesn't include the full path. The
709
-	 * cardData argument is the vcard body, and is passed as a string.
710
-	 *
711
-	 * It is possible to return an ETag from this method. This ETag should
712
-	 * match that of the updated resource, and must be enclosed with double
713
-	 * quotes (that is: the string itself must contain the actual quotes).
714
-	 *
715
-	 * You should only return the ETag if you store the carddata as-is. If a
716
-	 * subsequent GET request on the same card does not have the same body,
717
-	 * byte-by-byte and you did return an ETag here, clients tend to get
718
-	 * confused.
719
-	 *
720
-	 * If you don't return an ETag, you can just return null.
721
-	 *
722
-	 * @param mixed $addressBookId
723
-	 * @param string $cardUri
724
-	 * @param string $cardData
725
-	 * @return string
726
-	 */
727
-	public function updateCard($addressBookId, $cardUri, $cardData) {
728
-		$uid = $this->getUID($cardData);
729
-		$etag = md5($cardData);
730
-		$query = $this->db->getQueryBuilder();
731
-
732
-		// check for recently stored etag and stop if it is the same
733
-		$etagCacheKey = "$addressBookId#$cardUri";
734
-		if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
735
-			return '"' . $etag . '"';
736
-		}
737
-
738
-		$query->update($this->dbCardsTable)
739
-			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
740
-			->set('lastmodified', $query->createNamedParameter(time()))
741
-			->set('size', $query->createNamedParameter(strlen($cardData)))
742
-			->set('etag', $query->createNamedParameter($etag))
743
-			->set('uid', $query->createNamedParameter($uid))
744
-			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
745
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
746
-			->execute();
747
-
748
-		$this->etagCache[$etagCacheKey] = $etag;
749
-
750
-		$this->addChange($addressBookId, $cardUri, 2);
751
-		$this->updateProperties($addressBookId, $cardUri, $cardData);
752
-
753
-		$addressBookData = $this->getAddressBookById($addressBookId);
754
-		$shares = $this->getShares($addressBookId);
755
-		$objectRow = $this->getCard($addressBookId, $cardUri);
756
-		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
757
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
758
-			new GenericEvent(null, [
759
-				'addressBookId' => $addressBookId,
760
-				'cardUri' => $cardUri,
761
-				'cardData' => $cardData]));
762
-
763
-		return '"' . $etag . '"';
764
-	}
765
-
766
-	/**
767
-	 * Deletes a card
768
-	 *
769
-	 * @param mixed $addressBookId
770
-	 * @param string $cardUri
771
-	 * @return bool
772
-	 */
773
-	public function deleteCard($addressBookId, $cardUri) {
774
-		$addressBookData = $this->getAddressBookById($addressBookId);
775
-		$shares = $this->getShares($addressBookId);
776
-		$objectRow = $this->getCard($addressBookId, $cardUri);
777
-
778
-		try {
779
-			$cardId = $this->getCardId($addressBookId, $cardUri);
780
-		} catch (\InvalidArgumentException $e) {
781
-			$cardId = null;
782
-		}
783
-		$query = $this->db->getQueryBuilder();
784
-		$ret = $query->delete($this->dbCardsTable)
785
-			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
786
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
787
-			->execute();
788
-
789
-		$this->addChange($addressBookId, $cardUri, 3);
790
-
791
-		if ($ret === 1) {
792
-			if ($cardId !== null) {
793
-				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
794
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
795
-					new GenericEvent(null, [
796
-						'addressBookId' => $addressBookId,
797
-						'cardUri' => $cardUri]));
798
-
799
-				$this->purgeProperties($addressBookId, $cardId);
800
-			}
801
-			return true;
802
-		}
803
-
804
-		return false;
805
-	}
806
-
807
-	/**
808
-	 * The getChanges method returns all the changes that have happened, since
809
-	 * the specified syncToken in the specified address book.
810
-	 *
811
-	 * This function should return an array, such as the following:
812
-	 *
813
-	 * [
814
-	 *   'syncToken' => 'The current synctoken',
815
-	 *   'added'   => [
816
-	 *      'new.txt',
817
-	 *   ],
818
-	 *   'modified'   => [
819
-	 *      'modified.txt',
820
-	 *   ],
821
-	 *   'deleted' => [
822
-	 *      'foo.php.bak',
823
-	 *      'old.txt'
824
-	 *   ]
825
-	 * ];
826
-	 *
827
-	 * The returned syncToken property should reflect the *current* syncToken
828
-	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
829
-	 * property. This is needed here too, to ensure the operation is atomic.
830
-	 *
831
-	 * If the $syncToken argument is specified as null, this is an initial
832
-	 * sync, and all members should be reported.
833
-	 *
834
-	 * The modified property is an array of nodenames that have changed since
835
-	 * the last token.
836
-	 *
837
-	 * The deleted property is an array with nodenames, that have been deleted
838
-	 * from collection.
839
-	 *
840
-	 * The $syncLevel argument is basically the 'depth' of the report. If it's
841
-	 * 1, you only have to report changes that happened only directly in
842
-	 * immediate descendants. If it's 2, it should also include changes from
843
-	 * the nodes below the child collections. (grandchildren)
844
-	 *
845
-	 * The $limit argument allows a client to specify how many results should
846
-	 * be returned at most. If the limit is not specified, it should be treated
847
-	 * as infinite.
848
-	 *
849
-	 * If the limit (infinite or not) is higher than you're willing to return,
850
-	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
851
-	 *
852
-	 * If the syncToken is expired (due to data cleanup) or unknown, you must
853
-	 * return null.
854
-	 *
855
-	 * The limit is 'suggestive'. You are free to ignore it.
856
-	 *
857
-	 * @param string $addressBookId
858
-	 * @param string $syncToken
859
-	 * @param int $syncLevel
860
-	 * @param int|null $limit
861
-	 * @return array
862
-	 */
863
-	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
864
-		// Current synctoken
865
-		$qb = $this->db->getQueryBuilder();
866
-		$qb->select('synctoken')
867
-			->from('addressbooks')
868
-			->where(
869
-				$qb->expr()->eq('id', $qb->createNamedParameter($addressBookId))
870
-			);
871
-		$stmt = $qb->execute();
872
-		$currentToken = $stmt->fetchOne();
873
-		$stmt->closeCursor();
874
-
875
-		if (is_null($currentToken)) {
876
-			return null;
877
-		}
878
-
879
-		$result = [
880
-			'syncToken' => $currentToken,
881
-			'added' => [],
882
-			'modified' => [],
883
-			'deleted' => [],
884
-		];
885
-
886
-		if ($syncToken) {
887
-			$qb = $this->db->getQueryBuilder();
888
-			$qb->select('uri', 'operation')
889
-				->from('addressbookchanges')
890
-				->where(
891
-					$qb->expr()->andX(
892
-						$qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
893
-						$qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
894
-						$qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
895
-					)
896
-				)->orderBy('synctoken');
897
-
898
-			if (is_int($limit) && $limit > 0) {
899
-				$qb->setMaxResults($limit);
900
-			}
901
-
902
-			// Fetching all changes
903
-			$stmt = $qb->execute();
904
-
905
-			$changes = [];
906
-
907
-			// This loop ensures that any duplicates are overwritten, only the
908
-			// last change on a node is relevant.
909
-			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
910
-				$changes[$row['uri']] = $row['operation'];
911
-			}
912
-			$stmt->closeCursor();
913
-
914
-			foreach ($changes as $uri => $operation) {
915
-				switch ($operation) {
916
-					case 1:
917
-						$result['added'][] = $uri;
918
-						break;
919
-					case 2:
920
-						$result['modified'][] = $uri;
921
-						break;
922
-					case 3:
923
-						$result['deleted'][] = $uri;
924
-						break;
925
-				}
926
-			}
927
-		} else {
928
-			$qb = $this->db->getQueryBuilder();
929
-			$qb->select('uri')
930
-				->from('cards')
931
-				->where(
932
-					$qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
933
-				);
934
-			// No synctoken supplied, this is the initial sync.
935
-			$stmt = $qb->execute();
936
-			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
937
-			$stmt->closeCursor();
938
-		}
939
-		return $result;
940
-	}
941
-
942
-	/**
943
-	 * Adds a change record to the addressbookchanges table.
944
-	 *
945
-	 * @param mixed $addressBookId
946
-	 * @param string $objectUri
947
-	 * @param int $operation 1 = add, 2 = modify, 3 = delete
948
-	 * @return void
949
-	 */
950
-	protected function addChange($addressBookId, $objectUri, $operation) {
951
-		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
952
-		$stmt = $this->db->prepare($sql);
953
-		$stmt->execute([
954
-			$objectUri,
955
-			$addressBookId,
956
-			$operation,
957
-			$addressBookId
958
-		]);
959
-		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
960
-		$stmt->execute([
961
-			$addressBookId
962
-		]);
963
-	}
964
-
965
-	/**
966
-	 * @param resource|string $cardData
967
-	 * @param bool $modified
968
-	 * @return string
969
-	 */
970
-	private function readBlob($cardData, &$modified = false) {
971
-		if (is_resource($cardData)) {
972
-			$cardData = stream_get_contents($cardData);
973
-		}
974
-
975
-		$cardDataArray = explode("\r\n", $cardData);
976
-
977
-		$cardDataFiltered = [];
978
-		$removingPhoto = false;
979
-		foreach ($cardDataArray as $line) {
980
-			if (strpos($line, 'PHOTO:data:') === 0
981
-				&& strpos($line, 'PHOTO:data:image/') !== 0) {
982
-				// Filter out PHOTO data of non-images
983
-				$removingPhoto = true;
984
-				$modified = true;
985
-				continue;
986
-			}
987
-
988
-			if ($removingPhoto) {
989
-				if (strpos($line, ' ') === 0) {
990
-					continue;
991
-				}
992
-				// No leading space means this is a new property
993
-				$removingPhoto = false;
994
-			}
995
-
996
-			$cardDataFiltered[] = $line;
997
-		}
998
-
999
-		return implode("\r\n", $cardDataFiltered);
1000
-	}
1001
-
1002
-	/**
1003
-	 * @param IShareable $shareable
1004
-	 * @param string[] $add
1005
-	 * @param string[] $remove
1006
-	 */
1007
-	public function updateShares(IShareable $shareable, $add, $remove) {
1008
-		$addressBookId = $shareable->getResourceId();
1009
-		$addressBookData = $this->getAddressBookById($addressBookId);
1010
-		$oldShares = $this->getShares($addressBookId);
1011
-
1012
-		$this->sharingBackend->updateShares($shareable, $add, $remove);
1013
-
1014
-		$this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
1015
-	}
1016
-
1017
-	/**
1018
-	 * Search contacts in a specific address-book
1019
-	 *
1020
-	 * @param int $addressBookId
1021
-	 * @param string $pattern which should match within the $searchProperties
1022
-	 * @param array $searchProperties defines the properties within the query pattern should match
1023
-	 * @param array $options = array() to define the search behavior
1024
-	 *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1025
-	 *    - 'limit' - Set a numeric limit for the search results
1026
-	 *    - 'offset' - Set the offset for the limited search results
1027
-	 * @return array an array of contacts which are arrays of key-value-pairs
1028
-	 */
1029
-	public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1030
-		return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1031
-	}
1032
-
1033
-	/**
1034
-	 * Search contacts in all address-books accessible by a user
1035
-	 *
1036
-	 * @param string $principalUri
1037
-	 * @param string $pattern
1038
-	 * @param array $searchProperties
1039
-	 * @param array $options
1040
-	 * @return array
1041
-	 */
1042
-	public function searchPrincipalUri(string $principalUri,
1043
-									   string $pattern,
1044
-									   array $searchProperties,
1045
-									   array $options = []): array {
1046
-		$addressBookIds = array_map(static function ($row):int {
1047
-			return (int) $row['id'];
1048
-		}, $this->getAddressBooksForUser($principalUri));
1049
-
1050
-		return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1051
-	}
1052
-
1053
-	/**
1054
-	 * @param array $addressBookIds
1055
-	 * @param string $pattern
1056
-	 * @param array $searchProperties
1057
-	 * @param array $options
1058
-	 * @return array
1059
-	 */
1060
-	private function searchByAddressBookIds(array $addressBookIds,
1061
-											string $pattern,
1062
-											array $searchProperties,
1063
-											array $options = []): array {
1064
-		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1065
-
1066
-		$query2 = $this->db->getQueryBuilder();
1067
-
1068
-		$addressBookOr = $query2->expr()->orX();
1069
-		foreach ($addressBookIds as $addressBookId) {
1070
-			$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1071
-		}
1072
-
1073
-		if ($addressBookOr->count() === 0) {
1074
-			return [];
1075
-		}
1076
-
1077
-		$propertyOr = $query2->expr()->orX();
1078
-		foreach ($searchProperties as $property) {
1079
-			if ($escapePattern) {
1080
-				if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1081
-					// There can be no spaces in emails
1082
-					continue;
1083
-				}
1084
-
1085
-				if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1086
-					// There can be no chars in cloud ids which are not valid for user ids plus :/
1087
-					// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1088
-					continue;
1089
-				}
1090
-			}
1091
-
1092
-			$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1093
-		}
1094
-
1095
-		if ($propertyOr->count() === 0) {
1096
-			return [];
1097
-		}
1098
-
1099
-		$query2->selectDistinct('cp.cardid')
1100
-			->from($this->dbCardsPropertiesTable, 'cp')
1101
-			->andWhere($addressBookOr)
1102
-			->andWhere($propertyOr);
1103
-
1104
-		// No need for like when the pattern is empty
1105
-		if ('' !== $pattern) {
1106
-			if (!$escapePattern) {
1107
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1108
-			} else {
1109
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1110
-			}
1111
-		}
1112
-
1113
-		if (isset($options['limit'])) {
1114
-			$query2->setMaxResults($options['limit']);
1115
-		}
1116
-		if (isset($options['offset'])) {
1117
-			$query2->setFirstResult($options['offset']);
1118
-		}
1119
-
1120
-		$result = $query2->execute();
1121
-		$matches = $result->fetchAll();
1122
-		$result->closeCursor();
1123
-		$matches = array_map(function ($match) {
1124
-			return (int)$match['cardid'];
1125
-		}, $matches);
1126
-
1127
-		$query = $this->db->getQueryBuilder();
1128
-		$query->select('c.addressbookid', 'c.carddata', 'c.uri')
1129
-			->from($this->dbCardsTable, 'c')
1130
-			->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1131
-
1132
-		$result = $query->execute();
1133
-		$cards = $result->fetchAll();
1134
-
1135
-		$result->closeCursor();
1136
-
1137
-		return array_map(function ($array) {
1138
-			$array['addressbookid'] = (int) $array['addressbookid'];
1139
-			$modified = false;
1140
-			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
1141
-			if ($modified) {
1142
-				$array['size'] = strlen($array['carddata']);
1143
-			}
1144
-			return $array;
1145
-		}, $cards);
1146
-	}
1147
-
1148
-	/**
1149
-	 * @param int $bookId
1150
-	 * @param string $name
1151
-	 * @return array
1152
-	 */
1153
-	public function collectCardProperties($bookId, $name) {
1154
-		$query = $this->db->getQueryBuilder();
1155
-		$result = $query->selectDistinct('value')
1156
-			->from($this->dbCardsPropertiesTable)
1157
-			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1158
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1159
-			->execute();
1160
-
1161
-		$all = $result->fetchAll(PDO::FETCH_COLUMN);
1162
-		$result->closeCursor();
1163
-
1164
-		return $all;
1165
-	}
1166
-
1167
-	/**
1168
-	 * get URI from a given contact
1169
-	 *
1170
-	 * @param int $id
1171
-	 * @return string
1172
-	 */
1173
-	public function getCardUri($id) {
1174
-		$query = $this->db->getQueryBuilder();
1175
-		$query->select('uri')->from($this->dbCardsTable)
1176
-			->where($query->expr()->eq('id', $query->createParameter('id')))
1177
-			->setParameter('id', $id);
1178
-
1179
-		$result = $query->execute();
1180
-		$uri = $result->fetch();
1181
-		$result->closeCursor();
1182
-
1183
-		if (!isset($uri['uri'])) {
1184
-			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1185
-		}
1186
-
1187
-		return $uri['uri'];
1188
-	}
1189
-
1190
-	/**
1191
-	 * return contact with the given URI
1192
-	 *
1193
-	 * @param int $addressBookId
1194
-	 * @param string $uri
1195
-	 * @returns array
1196
-	 */
1197
-	public function getContact($addressBookId, $uri) {
1198
-		$result = [];
1199
-		$query = $this->db->getQueryBuilder();
1200
-		$query->select('*')->from($this->dbCardsTable)
1201
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1202
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1203
-		$queryResult = $query->execute();
1204
-		$contact = $queryResult->fetch();
1205
-		$queryResult->closeCursor();
1206
-
1207
-		if (is_array($contact)) {
1208
-			$modified = false;
1209
-			$contact['etag'] = '"' . $contact['etag'] . '"';
1210
-			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1211
-			if ($modified) {
1212
-				$contact['size'] = strlen($contact['carddata']);
1213
-			}
1214
-
1215
-			$result = $contact;
1216
-		}
1217
-
1218
-		return $result;
1219
-	}
1220
-
1221
-	/**
1222
-	 * Returns the list of people whom this address book is shared with.
1223
-	 *
1224
-	 * Every element in this array should have the following properties:
1225
-	 *   * href - Often a mailto: address
1226
-	 *   * commonName - Optional, for example a first + last name
1227
-	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1228
-	 *   * readOnly - boolean
1229
-	 *   * summary - Optional, a description for the share
1230
-	 *
1231
-	 * @return array
1232
-	 */
1233
-	public function getShares($addressBookId) {
1234
-		return $this->sharingBackend->getShares($addressBookId);
1235
-	}
1236
-
1237
-	/**
1238
-	 * update properties table
1239
-	 *
1240
-	 * @param int $addressBookId
1241
-	 * @param string $cardUri
1242
-	 * @param string $vCardSerialized
1243
-	 */
1244
-	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1245
-		$cardId = $this->getCardId($addressBookId, $cardUri);
1246
-		$vCard = $this->readCard($vCardSerialized);
1247
-
1248
-		$this->purgeProperties($addressBookId, $cardId);
1249
-
1250
-		$query = $this->db->getQueryBuilder();
1251
-		$query->insert($this->dbCardsPropertiesTable)
1252
-			->values(
1253
-				[
1254
-					'addressbookid' => $query->createNamedParameter($addressBookId),
1255
-					'cardid' => $query->createNamedParameter($cardId),
1256
-					'name' => $query->createParameter('name'),
1257
-					'value' => $query->createParameter('value'),
1258
-					'preferred' => $query->createParameter('preferred')
1259
-				]
1260
-			);
1261
-
1262
-		foreach ($vCard->children() as $property) {
1263
-			if (!in_array($property->name, self::$indexProperties)) {
1264
-				continue;
1265
-			}
1266
-			$preferred = 0;
1267
-			foreach ($property->parameters as $parameter) {
1268
-				if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1269
-					$preferred = 1;
1270
-					break;
1271
-				}
1272
-			}
1273
-			$query->setParameter('name', $property->name);
1274
-			$query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1275
-			$query->setParameter('preferred', $preferred);
1276
-			$query->execute();
1277
-		}
1278
-	}
1279
-
1280
-	/**
1281
-	 * read vCard data into a vCard object
1282
-	 *
1283
-	 * @param string $cardData
1284
-	 * @return VCard
1285
-	 */
1286
-	protected function readCard($cardData) {
1287
-		return Reader::read($cardData);
1288
-	}
1289
-
1290
-	/**
1291
-	 * delete all properties from a given card
1292
-	 *
1293
-	 * @param int $addressBookId
1294
-	 * @param int $cardId
1295
-	 */
1296
-	protected function purgeProperties($addressBookId, $cardId) {
1297
-		$query = $this->db->getQueryBuilder();
1298
-		$query->delete($this->dbCardsPropertiesTable)
1299
-			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1300
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1301
-		$query->execute();
1302
-	}
1303
-
1304
-	/**
1305
-	 * get ID from a given contact
1306
-	 *
1307
-	 * @param int $addressBookId
1308
-	 * @param string $uri
1309
-	 * @return int
1310
-	 */
1311
-	protected function getCardId($addressBookId, $uri) {
1312
-		$query = $this->db->getQueryBuilder();
1313
-		$query->select('id')->from($this->dbCardsTable)
1314
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1315
-			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1316
-
1317
-		$result = $query->execute();
1318
-		$cardIds = $result->fetch();
1319
-		$result->closeCursor();
1320
-
1321
-		if (!isset($cardIds['id'])) {
1322
-			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1323
-		}
1324
-
1325
-		return (int)$cardIds['id'];
1326
-	}
1327
-
1328
-	/**
1329
-	 * For shared address books the sharee is set in the ACL of the address book
1330
-	 *
1331
-	 * @param $addressBookId
1332
-	 * @param $acl
1333
-	 * @return array
1334
-	 */
1335
-	public function applyShareAcl($addressBookId, $acl) {
1336
-		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1337
-	}
1338
-
1339
-	private function convertPrincipal($principalUri, $toV2) {
1340
-		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1341
-			[, $name] = \Sabre\Uri\split($principalUri);
1342
-			if ($toV2 === true) {
1343
-				return "principals/users/$name";
1344
-			}
1345
-			return "principals/$name";
1346
-		}
1347
-		return $principalUri;
1348
-	}
1349
-
1350
-	private function addOwnerPrincipal(&$addressbookInfo) {
1351
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1352
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1353
-		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1354
-			$uri = $addressbookInfo[$ownerPrincipalKey];
1355
-		} else {
1356
-			$uri = $addressbookInfo['principaluri'];
1357
-		}
1358
-
1359
-		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1360
-		if (isset($principalInformation['{DAV:}displayname'])) {
1361
-			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1362
-		}
1363
-	}
1364
-
1365
-	/**
1366
-	 * Extract UID from vcard
1367
-	 *
1368
-	 * @param string $cardData the vcard raw data
1369
-	 * @return string the uid
1370
-	 * @throws BadRequest if no UID is available
1371
-	 */
1372
-	private function getUID($cardData) {
1373
-		if ($cardData != '') {
1374
-			$vCard = Reader::read($cardData);
1375
-			if ($vCard->UID) {
1376
-				$uid = $vCard->UID->getValue();
1377
-				return $uid;
1378
-			}
1379
-			// should already be handled, but just in case
1380
-			throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1381
-		}
1382
-		// should already be handled, but just in case
1383
-		throw new BadRequest('vCard can not be empty');
1384
-	}
65
+    public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
66
+    public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
67
+
68
+    /** @var Principal */
69
+    private $principalBackend;
70
+
71
+    /** @var string */
72
+    private $dbCardsTable = 'cards';
73
+
74
+    /** @var string */
75
+    private $dbCardsPropertiesTable = 'cards_properties';
76
+
77
+    /** @var IDBConnection */
78
+    private $db;
79
+
80
+    /** @var Backend */
81
+    private $sharingBackend;
82
+
83
+    /** @var array properties to index */
84
+    public static $indexProperties = [
85
+        'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
86
+        'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO',
87
+        'CLOUD', 'X-SOCIALPROFILE'];
88
+
89
+    /**
90
+     * @var string[] Map of uid => display name
91
+     */
92
+    protected $userDisplayNames;
93
+
94
+    /** @var IUserManager */
95
+    private $userManager;
96
+
97
+    /** @var IEventDispatcher */
98
+    private $dispatcher;
99
+
100
+    /** @var EventDispatcherInterface */
101
+    private $legacyDispatcher;
102
+
103
+    private $etagCache = [];
104
+
105
+    /**
106
+     * CardDavBackend constructor.
107
+     *
108
+     * @param IDBConnection $db
109
+     * @param Principal $principalBackend
110
+     * @param IUserManager $userManager
111
+     * @param IGroupManager $groupManager
112
+     * @param IEventDispatcher $dispatcher
113
+     * @param EventDispatcherInterface $legacyDispatcher
114
+     */
115
+    public function __construct(IDBConnection $db,
116
+                                Principal $principalBackend,
117
+                                IUserManager $userManager,
118
+                                IGroupManager $groupManager,
119
+                                IEventDispatcher $dispatcher,
120
+                                EventDispatcherInterface $legacyDispatcher) {
121
+        $this->db = $db;
122
+        $this->principalBackend = $principalBackend;
123
+        $this->userManager = $userManager;
124
+        $this->dispatcher = $dispatcher;
125
+        $this->legacyDispatcher = $legacyDispatcher;
126
+        $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
127
+    }
128
+
129
+    /**
130
+     * Return the number of address books for a principal
131
+     *
132
+     * @param $principalUri
133
+     * @return int
134
+     */
135
+    public function getAddressBooksForUserCount($principalUri) {
136
+        $principalUri = $this->convertPrincipal($principalUri, true);
137
+        $query = $this->db->getQueryBuilder();
138
+        $query->select($query->func()->count('*'))
139
+            ->from('addressbooks')
140
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
141
+
142
+        $result = $query->execute();
143
+        $column = (int) $result->fetchOne();
144
+        $result->closeCursor();
145
+        return $column;
146
+    }
147
+
148
+    /**
149
+     * Returns the list of address books for a specific user.
150
+     *
151
+     * Every addressbook should have the following properties:
152
+     *   id - an arbitrary unique id
153
+     *   uri - the 'basename' part of the url
154
+     *   principaluri - Same as the passed parameter
155
+     *
156
+     * Any additional clark-notation property may be passed besides this. Some
157
+     * common ones are :
158
+     *   {DAV:}displayname
159
+     *   {urn:ietf:params:xml:ns:carddav}addressbook-description
160
+     *   {http://calendarserver.org/ns/}getctag
161
+     *
162
+     * @param string $principalUri
163
+     * @return array
164
+     */
165
+    public function getAddressBooksForUser($principalUri) {
166
+        $principalUriOriginal = $principalUri;
167
+        $principalUri = $this->convertPrincipal($principalUri, true);
168
+        $query = $this->db->getQueryBuilder();
169
+        $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
170
+            ->from('addressbooks')
171
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
172
+
173
+        $addressBooks = [];
174
+
175
+        $result = $query->execute();
176
+        while ($row = $result->fetch()) {
177
+            $addressBooks[$row['id']] = [
178
+                'id' => $row['id'],
179
+                'uri' => $row['uri'],
180
+                'principaluri' => $this->convertPrincipal($row['principaluri'], false),
181
+                '{DAV:}displayname' => $row['displayname'],
182
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
183
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
184
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
185
+            ];
186
+
187
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
188
+        }
189
+        $result->closeCursor();
190
+
191
+        // query for shared addressbooks
192
+        $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
193
+        $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
194
+
195
+        $principals[] = $principalUri;
196
+
197
+        $query = $this->db->getQueryBuilder();
198
+        $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
199
+            ->from('dav_shares', 's')
200
+            ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
201
+            ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
202
+            ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
203
+            ->setParameter('type', 'addressbook')
204
+            ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
205
+            ->execute();
206
+
207
+        $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
208
+        while ($row = $result->fetch()) {
209
+            if ($row['principaluri'] === $principalUri) {
210
+                continue;
211
+            }
212
+
213
+            $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
214
+            if (isset($addressBooks[$row['id']])) {
215
+                if ($readOnly) {
216
+                    // New share can not have more permissions then the old one.
217
+                    continue;
218
+                }
219
+                if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
220
+                    $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
221
+                    // Old share is already read-write, no more permissions can be gained
222
+                    continue;
223
+                }
224
+            }
225
+
226
+            [, $name] = \Sabre\Uri\split($row['principaluri']);
227
+            $uri = $row['uri'] . '_shared_by_' . $name;
228
+            $displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
229
+
230
+            $addressBooks[$row['id']] = [
231
+                'id' => $row['id'],
232
+                'uri' => $uri,
233
+                'principaluri' => $principalUriOriginal,
234
+                '{DAV:}displayname' => $displayName,
235
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
236
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
237
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
238
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
239
+                $readOnlyPropertyName => $readOnly,
240
+            ];
241
+
242
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
243
+        }
244
+        $result->closeCursor();
245
+
246
+        return array_values($addressBooks);
247
+    }
248
+
249
+    public function getUsersOwnAddressBooks($principalUri) {
250
+        $principalUri = $this->convertPrincipal($principalUri, true);
251
+        $query = $this->db->getQueryBuilder();
252
+        $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
253
+            ->from('addressbooks')
254
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
255
+
256
+        $addressBooks = [];
257
+
258
+        $result = $query->execute();
259
+        while ($row = $result->fetch()) {
260
+            $addressBooks[$row['id']] = [
261
+                'id' => $row['id'],
262
+                'uri' => $row['uri'],
263
+                'principaluri' => $this->convertPrincipal($row['principaluri'], false),
264
+                '{DAV:}displayname' => $row['displayname'],
265
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
266
+                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
267
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
268
+            ];
269
+
270
+            $this->addOwnerPrincipal($addressBooks[$row['id']]);
271
+        }
272
+        $result->closeCursor();
273
+
274
+        return array_values($addressBooks);
275
+    }
276
+
277
+    private function getUserDisplayName($uid) {
278
+        if (!isset($this->userDisplayNames[$uid])) {
279
+            $user = $this->userManager->get($uid);
280
+
281
+            if ($user instanceof IUser) {
282
+                $this->userDisplayNames[$uid] = $user->getDisplayName();
283
+            } else {
284
+                $this->userDisplayNames[$uid] = $uid;
285
+            }
286
+        }
287
+
288
+        return $this->userDisplayNames[$uid];
289
+    }
290
+
291
+    /**
292
+     * @param int $addressBookId
293
+     */
294
+    public function getAddressBookById($addressBookId) {
295
+        $query = $this->db->getQueryBuilder();
296
+        $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
297
+            ->from('addressbooks')
298
+            ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
299
+            ->execute();
300
+
301
+        $row = $result->fetch();
302
+        $result->closeCursor();
303
+        if ($row === false) {
304
+            return null;
305
+        }
306
+
307
+        $addressBook = [
308
+            'id' => $row['id'],
309
+            'uri' => $row['uri'],
310
+            'principaluri' => $row['principaluri'],
311
+            '{DAV:}displayname' => $row['displayname'],
312
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
313
+            '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
314
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
315
+        ];
316
+
317
+        $this->addOwnerPrincipal($addressBook);
318
+
319
+        return $addressBook;
320
+    }
321
+
322
+    /**
323
+     * @param $addressBookUri
324
+     * @return array|null
325
+     */
326
+    public function getAddressBooksByUri($principal, $addressBookUri) {
327
+        $query = $this->db->getQueryBuilder();
328
+        $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
329
+            ->from('addressbooks')
330
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
331
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
332
+            ->setMaxResults(1)
333
+            ->execute();
334
+
335
+        $row = $result->fetch();
336
+        $result->closeCursor();
337
+        if ($row === false) {
338
+            return null;
339
+        }
340
+
341
+        $addressBook = [
342
+            'id' => $row['id'],
343
+            'uri' => $row['uri'],
344
+            'principaluri' => $row['principaluri'],
345
+            '{DAV:}displayname' => $row['displayname'],
346
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
347
+            '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
348
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
349
+        ];
350
+
351
+        $this->addOwnerPrincipal($addressBook);
352
+
353
+        return $addressBook;
354
+    }
355
+
356
+    /**
357
+     * Updates properties for an address book.
358
+     *
359
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
360
+     * To do the actual updates, you must tell this object which properties
361
+     * you're going to process with the handle() method.
362
+     *
363
+     * Calling the handle method is like telling the PropPatch object "I
364
+     * promise I can handle updating this property".
365
+     *
366
+     * Read the PropPatch documentation for more info and examples.
367
+     *
368
+     * @param string $addressBookId
369
+     * @param \Sabre\DAV\PropPatch $propPatch
370
+     * @return void
371
+     */
372
+    public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
373
+        $supportedProperties = [
374
+            '{DAV:}displayname',
375
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description',
376
+        ];
377
+
378
+        $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
379
+            $updates = [];
380
+            foreach ($mutations as $property => $newValue) {
381
+                switch ($property) {
382
+                    case '{DAV:}displayname':
383
+                        $updates['displayname'] = $newValue;
384
+                        break;
385
+                    case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
386
+                        $updates['description'] = $newValue;
387
+                        break;
388
+                }
389
+            }
390
+            $query = $this->db->getQueryBuilder();
391
+            $query->update('addressbooks');
392
+
393
+            foreach ($updates as $key => $value) {
394
+                $query->set($key, $query->createNamedParameter($value));
395
+            }
396
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
397
+                ->execute();
398
+
399
+            $this->addChange($addressBookId, "", 2);
400
+
401
+            $addressBookRow = $this->getAddressBookById((int)$addressBookId);
402
+            $shares = $this->getShares($addressBookId);
403
+            $this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
404
+
405
+            return true;
406
+        });
407
+    }
408
+
409
+    /**
410
+     * Creates a new address book
411
+     *
412
+     * @param string $principalUri
413
+     * @param string $url Just the 'basename' of the url.
414
+     * @param array $properties
415
+     * @return int
416
+     * @throws BadRequest
417
+     */
418
+    public function createAddressBook($principalUri, $url, array $properties) {
419
+        $values = [
420
+            'displayname' => null,
421
+            'description' => null,
422
+            'principaluri' => $principalUri,
423
+            'uri' => $url,
424
+            'synctoken' => 1
425
+        ];
426
+
427
+        foreach ($properties as $property => $newValue) {
428
+            switch ($property) {
429
+                case '{DAV:}displayname':
430
+                    $values['displayname'] = $newValue;
431
+                    break;
432
+                case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
433
+                    $values['description'] = $newValue;
434
+                    break;
435
+                default:
436
+                    throw new BadRequest('Unknown property: ' . $property);
437
+            }
438
+        }
439
+
440
+        // Fallback to make sure the displayname is set. Some clients may refuse
441
+        // to work with addressbooks not having a displayname.
442
+        if (is_null($values['displayname'])) {
443
+            $values['displayname'] = $url;
444
+        }
445
+
446
+        $query = $this->db->getQueryBuilder();
447
+        $query->insert('addressbooks')
448
+            ->values([
449
+                'uri' => $query->createParameter('uri'),
450
+                'displayname' => $query->createParameter('displayname'),
451
+                'description' => $query->createParameter('description'),
452
+                'principaluri' => $query->createParameter('principaluri'),
453
+                'synctoken' => $query->createParameter('synctoken'),
454
+            ])
455
+            ->setParameters($values)
456
+            ->execute();
457
+
458
+        $addressBookId = $query->getLastInsertId();
459
+        $addressBookRow = $this->getAddressBookById($addressBookId);
460
+        $this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
461
+
462
+        return $addressBookId;
463
+    }
464
+
465
+    /**
466
+     * Deletes an entire addressbook and all its contents
467
+     *
468
+     * @param mixed $addressBookId
469
+     * @return void
470
+     */
471
+    public function deleteAddressBook($addressBookId) {
472
+        $addressBookData = $this->getAddressBookById($addressBookId);
473
+        $shares = $this->getShares($addressBookId);
474
+
475
+        $query = $this->db->getQueryBuilder();
476
+        $query->delete($this->dbCardsTable)
477
+            ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
478
+            ->setParameter('addressbookid', $addressBookId)
479
+            ->execute();
480
+
481
+        $query->delete('addressbookchanges')
482
+            ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
483
+            ->setParameter('addressbookid', $addressBookId)
484
+            ->execute();
485
+
486
+        $query->delete('addressbooks')
487
+            ->where($query->expr()->eq('id', $query->createParameter('id')))
488
+            ->setParameter('id', $addressBookId)
489
+            ->execute();
490
+
491
+        $this->sharingBackend->deleteAllShares($addressBookId);
492
+
493
+        $query->delete($this->dbCardsPropertiesTable)
494
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
495
+            ->execute();
496
+
497
+        if ($addressBookData) {
498
+            $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent((int) $addressBookId, $addressBookData, $shares));
499
+        }
500
+    }
501
+
502
+    /**
503
+     * Returns all cards for a specific addressbook id.
504
+     *
505
+     * This method should return the following properties for each card:
506
+     *   * carddata - raw vcard data
507
+     *   * uri - Some unique url
508
+     *   * lastmodified - A unix timestamp
509
+     *
510
+     * It's recommended to also return the following properties:
511
+     *   * etag - A unique etag. This must change every time the card changes.
512
+     *   * size - The size of the card in bytes.
513
+     *
514
+     * If these last two properties are provided, less time will be spent
515
+     * calculating them. If they are specified, you can also ommit carddata.
516
+     * This may speed up certain requests, especially with large cards.
517
+     *
518
+     * @param mixed $addressBookId
519
+     * @return array
520
+     */
521
+    public function getCards($addressBookId) {
522
+        $query = $this->db->getQueryBuilder();
523
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
524
+            ->from($this->dbCardsTable)
525
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
526
+
527
+        $cards = [];
528
+
529
+        $result = $query->execute();
530
+        while ($row = $result->fetch()) {
531
+            $row['etag'] = '"' . $row['etag'] . '"';
532
+
533
+            $modified = false;
534
+            $row['carddata'] = $this->readBlob($row['carddata'], $modified);
535
+            if ($modified) {
536
+                $row['size'] = strlen($row['carddata']);
537
+            }
538
+
539
+            $cards[] = $row;
540
+        }
541
+        $result->closeCursor();
542
+
543
+        return $cards;
544
+    }
545
+
546
+    /**
547
+     * Returns a specific card.
548
+     *
549
+     * The same set of properties must be returned as with getCards. The only
550
+     * exception is that 'carddata' is absolutely required.
551
+     *
552
+     * If the card does not exist, you must return false.
553
+     *
554
+     * @param mixed $addressBookId
555
+     * @param string $cardUri
556
+     * @return array
557
+     */
558
+    public function getCard($addressBookId, $cardUri) {
559
+        $query = $this->db->getQueryBuilder();
560
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
561
+            ->from($this->dbCardsTable)
562
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
563
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
564
+            ->setMaxResults(1);
565
+
566
+        $result = $query->execute();
567
+        $row = $result->fetch();
568
+        if (!$row) {
569
+            return false;
570
+        }
571
+        $row['etag'] = '"' . $row['etag'] . '"';
572
+
573
+        $modified = false;
574
+        $row['carddata'] = $this->readBlob($row['carddata'], $modified);
575
+        if ($modified) {
576
+            $row['size'] = strlen($row['carddata']);
577
+        }
578
+
579
+        return $row;
580
+    }
581
+
582
+    /**
583
+     * Returns a list of cards.
584
+     *
585
+     * This method should work identical to getCard, but instead return all the
586
+     * cards in the list as an array.
587
+     *
588
+     * If the backend supports this, it may allow for some speed-ups.
589
+     *
590
+     * @param mixed $addressBookId
591
+     * @param string[] $uris
592
+     * @return array
593
+     */
594
+    public function getMultipleCards($addressBookId, array $uris) {
595
+        if (empty($uris)) {
596
+            return [];
597
+        }
598
+
599
+        $chunks = array_chunk($uris, 100);
600
+        $cards = [];
601
+
602
+        $query = $this->db->getQueryBuilder();
603
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
604
+            ->from($this->dbCardsTable)
605
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
606
+            ->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
607
+
608
+        foreach ($chunks as $uris) {
609
+            $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
610
+            $result = $query->execute();
611
+
612
+            while ($row = $result->fetch()) {
613
+                $row['etag'] = '"' . $row['etag'] . '"';
614
+
615
+                $modified = false;
616
+                $row['carddata'] = $this->readBlob($row['carddata'], $modified);
617
+                if ($modified) {
618
+                    $row['size'] = strlen($row['carddata']);
619
+                }
620
+
621
+                $cards[] = $row;
622
+            }
623
+            $result->closeCursor();
624
+        }
625
+        return $cards;
626
+    }
627
+
628
+    /**
629
+     * Creates a new card.
630
+     *
631
+     * The addressbook id will be passed as the first argument. This is the
632
+     * same id as it is returned from the getAddressBooksForUser method.
633
+     *
634
+     * The cardUri is a base uri, and doesn't include the full path. The
635
+     * cardData argument is the vcard body, and is passed as a string.
636
+     *
637
+     * It is possible to return an ETag from this method. This ETag is for the
638
+     * newly created resource, and must be enclosed with double quotes (that
639
+     * is, the string itself must contain the double quotes).
640
+     *
641
+     * You should only return the ETag if you store the carddata as-is. If a
642
+     * subsequent GET request on the same card does not have the same body,
643
+     * byte-by-byte and you did return an ETag here, clients tend to get
644
+     * confused.
645
+     *
646
+     * If you don't return an ETag, you can just return null.
647
+     *
648
+     * @param mixed $addressBookId
649
+     * @param string $cardUri
650
+     * @param string $cardData
651
+     * @return string
652
+     */
653
+    public function createCard($addressBookId, $cardUri, $cardData) {
654
+        $etag = md5($cardData);
655
+        $uid = $this->getUID($cardData);
656
+
657
+        $q = $this->db->getQueryBuilder();
658
+        $q->select('uid')
659
+            ->from($this->dbCardsTable)
660
+            ->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
661
+            ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
662
+            ->setMaxResults(1);
663
+        $result = $q->execute();
664
+        $count = (bool)$result->fetchOne();
665
+        $result->closeCursor();
666
+        if ($count) {
667
+            throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
668
+        }
669
+
670
+        $query = $this->db->getQueryBuilder();
671
+        $query->insert('cards')
672
+            ->values([
673
+                'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
674
+                'uri' => $query->createNamedParameter($cardUri),
675
+                'lastmodified' => $query->createNamedParameter(time()),
676
+                'addressbookid' => $query->createNamedParameter($addressBookId),
677
+                'size' => $query->createNamedParameter(strlen($cardData)),
678
+                'etag' => $query->createNamedParameter($etag),
679
+                'uid' => $query->createNamedParameter($uid),
680
+            ])
681
+            ->execute();
682
+
683
+        $etagCacheKey = "$addressBookId#$cardUri";
684
+        $this->etagCache[$etagCacheKey] = $etag;
685
+
686
+        $this->addChange($addressBookId, $cardUri, 1);
687
+        $this->updateProperties($addressBookId, $cardUri, $cardData);
688
+
689
+        $addressBookData = $this->getAddressBookById($addressBookId);
690
+        $shares = $this->getShares($addressBookId);
691
+        $objectRow = $this->getCard($addressBookId, $cardUri);
692
+        $this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
693
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
694
+            new GenericEvent(null, [
695
+                'addressBookId' => $addressBookId,
696
+                'cardUri' => $cardUri,
697
+                'cardData' => $cardData]));
698
+
699
+        return '"' . $etag . '"';
700
+    }
701
+
702
+    /**
703
+     * Updates a card.
704
+     *
705
+     * The addressbook id will be passed as the first argument. This is the
706
+     * same id as it is returned from the getAddressBooksForUser method.
707
+     *
708
+     * The cardUri is a base uri, and doesn't include the full path. The
709
+     * cardData argument is the vcard body, and is passed as a string.
710
+     *
711
+     * It is possible to return an ETag from this method. This ETag should
712
+     * match that of the updated resource, and must be enclosed with double
713
+     * quotes (that is: the string itself must contain the actual quotes).
714
+     *
715
+     * You should only return the ETag if you store the carddata as-is. If a
716
+     * subsequent GET request on the same card does not have the same body,
717
+     * byte-by-byte and you did return an ETag here, clients tend to get
718
+     * confused.
719
+     *
720
+     * If you don't return an ETag, you can just return null.
721
+     *
722
+     * @param mixed $addressBookId
723
+     * @param string $cardUri
724
+     * @param string $cardData
725
+     * @return string
726
+     */
727
+    public function updateCard($addressBookId, $cardUri, $cardData) {
728
+        $uid = $this->getUID($cardData);
729
+        $etag = md5($cardData);
730
+        $query = $this->db->getQueryBuilder();
731
+
732
+        // check for recently stored etag and stop if it is the same
733
+        $etagCacheKey = "$addressBookId#$cardUri";
734
+        if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
735
+            return '"' . $etag . '"';
736
+        }
737
+
738
+        $query->update($this->dbCardsTable)
739
+            ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
740
+            ->set('lastmodified', $query->createNamedParameter(time()))
741
+            ->set('size', $query->createNamedParameter(strlen($cardData)))
742
+            ->set('etag', $query->createNamedParameter($etag))
743
+            ->set('uid', $query->createNamedParameter($uid))
744
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
745
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
746
+            ->execute();
747
+
748
+        $this->etagCache[$etagCacheKey] = $etag;
749
+
750
+        $this->addChange($addressBookId, $cardUri, 2);
751
+        $this->updateProperties($addressBookId, $cardUri, $cardData);
752
+
753
+        $addressBookData = $this->getAddressBookById($addressBookId);
754
+        $shares = $this->getShares($addressBookId);
755
+        $objectRow = $this->getCard($addressBookId, $cardUri);
756
+        $this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
757
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
758
+            new GenericEvent(null, [
759
+                'addressBookId' => $addressBookId,
760
+                'cardUri' => $cardUri,
761
+                'cardData' => $cardData]));
762
+
763
+        return '"' . $etag . '"';
764
+    }
765
+
766
+    /**
767
+     * Deletes a card
768
+     *
769
+     * @param mixed $addressBookId
770
+     * @param string $cardUri
771
+     * @return bool
772
+     */
773
+    public function deleteCard($addressBookId, $cardUri) {
774
+        $addressBookData = $this->getAddressBookById($addressBookId);
775
+        $shares = $this->getShares($addressBookId);
776
+        $objectRow = $this->getCard($addressBookId, $cardUri);
777
+
778
+        try {
779
+            $cardId = $this->getCardId($addressBookId, $cardUri);
780
+        } catch (\InvalidArgumentException $e) {
781
+            $cardId = null;
782
+        }
783
+        $query = $this->db->getQueryBuilder();
784
+        $ret = $query->delete($this->dbCardsTable)
785
+            ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
786
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
787
+            ->execute();
788
+
789
+        $this->addChange($addressBookId, $cardUri, 3);
790
+
791
+        if ($ret === 1) {
792
+            if ($cardId !== null) {
793
+                $this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
794
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
795
+                    new GenericEvent(null, [
796
+                        'addressBookId' => $addressBookId,
797
+                        'cardUri' => $cardUri]));
798
+
799
+                $this->purgeProperties($addressBookId, $cardId);
800
+            }
801
+            return true;
802
+        }
803
+
804
+        return false;
805
+    }
806
+
807
+    /**
808
+     * The getChanges method returns all the changes that have happened, since
809
+     * the specified syncToken in the specified address book.
810
+     *
811
+     * This function should return an array, such as the following:
812
+     *
813
+     * [
814
+     *   'syncToken' => 'The current synctoken',
815
+     *   'added'   => [
816
+     *      'new.txt',
817
+     *   ],
818
+     *   'modified'   => [
819
+     *      'modified.txt',
820
+     *   ],
821
+     *   'deleted' => [
822
+     *      'foo.php.bak',
823
+     *      'old.txt'
824
+     *   ]
825
+     * ];
826
+     *
827
+     * The returned syncToken property should reflect the *current* syncToken
828
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
829
+     * property. This is needed here too, to ensure the operation is atomic.
830
+     *
831
+     * If the $syncToken argument is specified as null, this is an initial
832
+     * sync, and all members should be reported.
833
+     *
834
+     * The modified property is an array of nodenames that have changed since
835
+     * the last token.
836
+     *
837
+     * The deleted property is an array with nodenames, that have been deleted
838
+     * from collection.
839
+     *
840
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
841
+     * 1, you only have to report changes that happened only directly in
842
+     * immediate descendants. If it's 2, it should also include changes from
843
+     * the nodes below the child collections. (grandchildren)
844
+     *
845
+     * The $limit argument allows a client to specify how many results should
846
+     * be returned at most. If the limit is not specified, it should be treated
847
+     * as infinite.
848
+     *
849
+     * If the limit (infinite or not) is higher than you're willing to return,
850
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
851
+     *
852
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
853
+     * return null.
854
+     *
855
+     * The limit is 'suggestive'. You are free to ignore it.
856
+     *
857
+     * @param string $addressBookId
858
+     * @param string $syncToken
859
+     * @param int $syncLevel
860
+     * @param int|null $limit
861
+     * @return array
862
+     */
863
+    public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
864
+        // Current synctoken
865
+        $qb = $this->db->getQueryBuilder();
866
+        $qb->select('synctoken')
867
+            ->from('addressbooks')
868
+            ->where(
869
+                $qb->expr()->eq('id', $qb->createNamedParameter($addressBookId))
870
+            );
871
+        $stmt = $qb->execute();
872
+        $currentToken = $stmt->fetchOne();
873
+        $stmt->closeCursor();
874
+
875
+        if (is_null($currentToken)) {
876
+            return null;
877
+        }
878
+
879
+        $result = [
880
+            'syncToken' => $currentToken,
881
+            'added' => [],
882
+            'modified' => [],
883
+            'deleted' => [],
884
+        ];
885
+
886
+        if ($syncToken) {
887
+            $qb = $this->db->getQueryBuilder();
888
+            $qb->select('uri', 'operation')
889
+                ->from('addressbookchanges')
890
+                ->where(
891
+                    $qb->expr()->andX(
892
+                        $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
893
+                        $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
894
+                        $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
895
+                    )
896
+                )->orderBy('synctoken');
897
+
898
+            if (is_int($limit) && $limit > 0) {
899
+                $qb->setMaxResults($limit);
900
+            }
901
+
902
+            // Fetching all changes
903
+            $stmt = $qb->execute();
904
+
905
+            $changes = [];
906
+
907
+            // This loop ensures that any duplicates are overwritten, only the
908
+            // last change on a node is relevant.
909
+            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
910
+                $changes[$row['uri']] = $row['operation'];
911
+            }
912
+            $stmt->closeCursor();
913
+
914
+            foreach ($changes as $uri => $operation) {
915
+                switch ($operation) {
916
+                    case 1:
917
+                        $result['added'][] = $uri;
918
+                        break;
919
+                    case 2:
920
+                        $result['modified'][] = $uri;
921
+                        break;
922
+                    case 3:
923
+                        $result['deleted'][] = $uri;
924
+                        break;
925
+                }
926
+            }
927
+        } else {
928
+            $qb = $this->db->getQueryBuilder();
929
+            $qb->select('uri')
930
+                ->from('cards')
931
+                ->where(
932
+                    $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
933
+                );
934
+            // No synctoken supplied, this is the initial sync.
935
+            $stmt = $qb->execute();
936
+            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
937
+            $stmt->closeCursor();
938
+        }
939
+        return $result;
940
+    }
941
+
942
+    /**
943
+     * Adds a change record to the addressbookchanges table.
944
+     *
945
+     * @param mixed $addressBookId
946
+     * @param string $objectUri
947
+     * @param int $operation 1 = add, 2 = modify, 3 = delete
948
+     * @return void
949
+     */
950
+    protected function addChange($addressBookId, $objectUri, $operation) {
951
+        $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
952
+        $stmt = $this->db->prepare($sql);
953
+        $stmt->execute([
954
+            $objectUri,
955
+            $addressBookId,
956
+            $operation,
957
+            $addressBookId
958
+        ]);
959
+        $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
960
+        $stmt->execute([
961
+            $addressBookId
962
+        ]);
963
+    }
964
+
965
+    /**
966
+     * @param resource|string $cardData
967
+     * @param bool $modified
968
+     * @return string
969
+     */
970
+    private function readBlob($cardData, &$modified = false) {
971
+        if (is_resource($cardData)) {
972
+            $cardData = stream_get_contents($cardData);
973
+        }
974
+
975
+        $cardDataArray = explode("\r\n", $cardData);
976
+
977
+        $cardDataFiltered = [];
978
+        $removingPhoto = false;
979
+        foreach ($cardDataArray as $line) {
980
+            if (strpos($line, 'PHOTO:data:') === 0
981
+                && strpos($line, 'PHOTO:data:image/') !== 0) {
982
+                // Filter out PHOTO data of non-images
983
+                $removingPhoto = true;
984
+                $modified = true;
985
+                continue;
986
+            }
987
+
988
+            if ($removingPhoto) {
989
+                if (strpos($line, ' ') === 0) {
990
+                    continue;
991
+                }
992
+                // No leading space means this is a new property
993
+                $removingPhoto = false;
994
+            }
995
+
996
+            $cardDataFiltered[] = $line;
997
+        }
998
+
999
+        return implode("\r\n", $cardDataFiltered);
1000
+    }
1001
+
1002
+    /**
1003
+     * @param IShareable $shareable
1004
+     * @param string[] $add
1005
+     * @param string[] $remove
1006
+     */
1007
+    public function updateShares(IShareable $shareable, $add, $remove) {
1008
+        $addressBookId = $shareable->getResourceId();
1009
+        $addressBookData = $this->getAddressBookById($addressBookId);
1010
+        $oldShares = $this->getShares($addressBookId);
1011
+
1012
+        $this->sharingBackend->updateShares($shareable, $add, $remove);
1013
+
1014
+        $this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
1015
+    }
1016
+
1017
+    /**
1018
+     * Search contacts in a specific address-book
1019
+     *
1020
+     * @param int $addressBookId
1021
+     * @param string $pattern which should match within the $searchProperties
1022
+     * @param array $searchProperties defines the properties within the query pattern should match
1023
+     * @param array $options = array() to define the search behavior
1024
+     *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1025
+     *    - 'limit' - Set a numeric limit for the search results
1026
+     *    - 'offset' - Set the offset for the limited search results
1027
+     * @return array an array of contacts which are arrays of key-value-pairs
1028
+     */
1029
+    public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1030
+        return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1031
+    }
1032
+
1033
+    /**
1034
+     * Search contacts in all address-books accessible by a user
1035
+     *
1036
+     * @param string $principalUri
1037
+     * @param string $pattern
1038
+     * @param array $searchProperties
1039
+     * @param array $options
1040
+     * @return array
1041
+     */
1042
+    public function searchPrincipalUri(string $principalUri,
1043
+                                        string $pattern,
1044
+                                        array $searchProperties,
1045
+                                        array $options = []): array {
1046
+        $addressBookIds = array_map(static function ($row):int {
1047
+            return (int) $row['id'];
1048
+        }, $this->getAddressBooksForUser($principalUri));
1049
+
1050
+        return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1051
+    }
1052
+
1053
+    /**
1054
+     * @param array $addressBookIds
1055
+     * @param string $pattern
1056
+     * @param array $searchProperties
1057
+     * @param array $options
1058
+     * @return array
1059
+     */
1060
+    private function searchByAddressBookIds(array $addressBookIds,
1061
+                                            string $pattern,
1062
+                                            array $searchProperties,
1063
+                                            array $options = []): array {
1064
+        $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1065
+
1066
+        $query2 = $this->db->getQueryBuilder();
1067
+
1068
+        $addressBookOr = $query2->expr()->orX();
1069
+        foreach ($addressBookIds as $addressBookId) {
1070
+            $addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1071
+        }
1072
+
1073
+        if ($addressBookOr->count() === 0) {
1074
+            return [];
1075
+        }
1076
+
1077
+        $propertyOr = $query2->expr()->orX();
1078
+        foreach ($searchProperties as $property) {
1079
+            if ($escapePattern) {
1080
+                if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1081
+                    // There can be no spaces in emails
1082
+                    continue;
1083
+                }
1084
+
1085
+                if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1086
+                    // There can be no chars in cloud ids which are not valid for user ids plus :/
1087
+                    // worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1088
+                    continue;
1089
+                }
1090
+            }
1091
+
1092
+            $propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1093
+        }
1094
+
1095
+        if ($propertyOr->count() === 0) {
1096
+            return [];
1097
+        }
1098
+
1099
+        $query2->selectDistinct('cp.cardid')
1100
+            ->from($this->dbCardsPropertiesTable, 'cp')
1101
+            ->andWhere($addressBookOr)
1102
+            ->andWhere($propertyOr);
1103
+
1104
+        // No need for like when the pattern is empty
1105
+        if ('' !== $pattern) {
1106
+            if (!$escapePattern) {
1107
+                $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1108
+            } else {
1109
+                $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1110
+            }
1111
+        }
1112
+
1113
+        if (isset($options['limit'])) {
1114
+            $query2->setMaxResults($options['limit']);
1115
+        }
1116
+        if (isset($options['offset'])) {
1117
+            $query2->setFirstResult($options['offset']);
1118
+        }
1119
+
1120
+        $result = $query2->execute();
1121
+        $matches = $result->fetchAll();
1122
+        $result->closeCursor();
1123
+        $matches = array_map(function ($match) {
1124
+            return (int)$match['cardid'];
1125
+        }, $matches);
1126
+
1127
+        $query = $this->db->getQueryBuilder();
1128
+        $query->select('c.addressbookid', 'c.carddata', 'c.uri')
1129
+            ->from($this->dbCardsTable, 'c')
1130
+            ->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1131
+
1132
+        $result = $query->execute();
1133
+        $cards = $result->fetchAll();
1134
+
1135
+        $result->closeCursor();
1136
+
1137
+        return array_map(function ($array) {
1138
+            $array['addressbookid'] = (int) $array['addressbookid'];
1139
+            $modified = false;
1140
+            $array['carddata'] = $this->readBlob($array['carddata'], $modified);
1141
+            if ($modified) {
1142
+                $array['size'] = strlen($array['carddata']);
1143
+            }
1144
+            return $array;
1145
+        }, $cards);
1146
+    }
1147
+
1148
+    /**
1149
+     * @param int $bookId
1150
+     * @param string $name
1151
+     * @return array
1152
+     */
1153
+    public function collectCardProperties($bookId, $name) {
1154
+        $query = $this->db->getQueryBuilder();
1155
+        $result = $query->selectDistinct('value')
1156
+            ->from($this->dbCardsPropertiesTable)
1157
+            ->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1158
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1159
+            ->execute();
1160
+
1161
+        $all = $result->fetchAll(PDO::FETCH_COLUMN);
1162
+        $result->closeCursor();
1163
+
1164
+        return $all;
1165
+    }
1166
+
1167
+    /**
1168
+     * get URI from a given contact
1169
+     *
1170
+     * @param int $id
1171
+     * @return string
1172
+     */
1173
+    public function getCardUri($id) {
1174
+        $query = $this->db->getQueryBuilder();
1175
+        $query->select('uri')->from($this->dbCardsTable)
1176
+            ->where($query->expr()->eq('id', $query->createParameter('id')))
1177
+            ->setParameter('id', $id);
1178
+
1179
+        $result = $query->execute();
1180
+        $uri = $result->fetch();
1181
+        $result->closeCursor();
1182
+
1183
+        if (!isset($uri['uri'])) {
1184
+            throw new \InvalidArgumentException('Card does not exists: ' . $id);
1185
+        }
1186
+
1187
+        return $uri['uri'];
1188
+    }
1189
+
1190
+    /**
1191
+     * return contact with the given URI
1192
+     *
1193
+     * @param int $addressBookId
1194
+     * @param string $uri
1195
+     * @returns array
1196
+     */
1197
+    public function getContact($addressBookId, $uri) {
1198
+        $result = [];
1199
+        $query = $this->db->getQueryBuilder();
1200
+        $query->select('*')->from($this->dbCardsTable)
1201
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1202
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1203
+        $queryResult = $query->execute();
1204
+        $contact = $queryResult->fetch();
1205
+        $queryResult->closeCursor();
1206
+
1207
+        if (is_array($contact)) {
1208
+            $modified = false;
1209
+            $contact['etag'] = '"' . $contact['etag'] . '"';
1210
+            $contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1211
+            if ($modified) {
1212
+                $contact['size'] = strlen($contact['carddata']);
1213
+            }
1214
+
1215
+            $result = $contact;
1216
+        }
1217
+
1218
+        return $result;
1219
+    }
1220
+
1221
+    /**
1222
+     * Returns the list of people whom this address book is shared with.
1223
+     *
1224
+     * Every element in this array should have the following properties:
1225
+     *   * href - Often a mailto: address
1226
+     *   * commonName - Optional, for example a first + last name
1227
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1228
+     *   * readOnly - boolean
1229
+     *   * summary - Optional, a description for the share
1230
+     *
1231
+     * @return array
1232
+     */
1233
+    public function getShares($addressBookId) {
1234
+        return $this->sharingBackend->getShares($addressBookId);
1235
+    }
1236
+
1237
+    /**
1238
+     * update properties table
1239
+     *
1240
+     * @param int $addressBookId
1241
+     * @param string $cardUri
1242
+     * @param string $vCardSerialized
1243
+     */
1244
+    protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1245
+        $cardId = $this->getCardId($addressBookId, $cardUri);
1246
+        $vCard = $this->readCard($vCardSerialized);
1247
+
1248
+        $this->purgeProperties($addressBookId, $cardId);
1249
+
1250
+        $query = $this->db->getQueryBuilder();
1251
+        $query->insert($this->dbCardsPropertiesTable)
1252
+            ->values(
1253
+                [
1254
+                    'addressbookid' => $query->createNamedParameter($addressBookId),
1255
+                    'cardid' => $query->createNamedParameter($cardId),
1256
+                    'name' => $query->createParameter('name'),
1257
+                    'value' => $query->createParameter('value'),
1258
+                    'preferred' => $query->createParameter('preferred')
1259
+                ]
1260
+            );
1261
+
1262
+        foreach ($vCard->children() as $property) {
1263
+            if (!in_array($property->name, self::$indexProperties)) {
1264
+                continue;
1265
+            }
1266
+            $preferred = 0;
1267
+            foreach ($property->parameters as $parameter) {
1268
+                if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1269
+                    $preferred = 1;
1270
+                    break;
1271
+                }
1272
+            }
1273
+            $query->setParameter('name', $property->name);
1274
+            $query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1275
+            $query->setParameter('preferred', $preferred);
1276
+            $query->execute();
1277
+        }
1278
+    }
1279
+
1280
+    /**
1281
+     * read vCard data into a vCard object
1282
+     *
1283
+     * @param string $cardData
1284
+     * @return VCard
1285
+     */
1286
+    protected function readCard($cardData) {
1287
+        return Reader::read($cardData);
1288
+    }
1289
+
1290
+    /**
1291
+     * delete all properties from a given card
1292
+     *
1293
+     * @param int $addressBookId
1294
+     * @param int $cardId
1295
+     */
1296
+    protected function purgeProperties($addressBookId, $cardId) {
1297
+        $query = $this->db->getQueryBuilder();
1298
+        $query->delete($this->dbCardsPropertiesTable)
1299
+            ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1300
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1301
+        $query->execute();
1302
+    }
1303
+
1304
+    /**
1305
+     * get ID from a given contact
1306
+     *
1307
+     * @param int $addressBookId
1308
+     * @param string $uri
1309
+     * @return int
1310
+     */
1311
+    protected function getCardId($addressBookId, $uri) {
1312
+        $query = $this->db->getQueryBuilder();
1313
+        $query->select('id')->from($this->dbCardsTable)
1314
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1315
+            ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1316
+
1317
+        $result = $query->execute();
1318
+        $cardIds = $result->fetch();
1319
+        $result->closeCursor();
1320
+
1321
+        if (!isset($cardIds['id'])) {
1322
+            throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1323
+        }
1324
+
1325
+        return (int)$cardIds['id'];
1326
+    }
1327
+
1328
+    /**
1329
+     * For shared address books the sharee is set in the ACL of the address book
1330
+     *
1331
+     * @param $addressBookId
1332
+     * @param $acl
1333
+     * @return array
1334
+     */
1335
+    public function applyShareAcl($addressBookId, $acl) {
1336
+        return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1337
+    }
1338
+
1339
+    private function convertPrincipal($principalUri, $toV2) {
1340
+        if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1341
+            [, $name] = \Sabre\Uri\split($principalUri);
1342
+            if ($toV2 === true) {
1343
+                return "principals/users/$name";
1344
+            }
1345
+            return "principals/$name";
1346
+        }
1347
+        return $principalUri;
1348
+    }
1349
+
1350
+    private function addOwnerPrincipal(&$addressbookInfo) {
1351
+        $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1352
+        $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1353
+        if (isset($addressbookInfo[$ownerPrincipalKey])) {
1354
+            $uri = $addressbookInfo[$ownerPrincipalKey];
1355
+        } else {
1356
+            $uri = $addressbookInfo['principaluri'];
1357
+        }
1358
+
1359
+        $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1360
+        if (isset($principalInformation['{DAV:}displayname'])) {
1361
+            $addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1362
+        }
1363
+    }
1364
+
1365
+    /**
1366
+     * Extract UID from vcard
1367
+     *
1368
+     * @param string $cardData the vcard raw data
1369
+     * @return string the uid
1370
+     * @throws BadRequest if no UID is available
1371
+     */
1372
+    private function getUID($cardData) {
1373
+        if ($cardData != '') {
1374
+            $vCard = Reader::read($cardData);
1375
+            if ($vCard->UID) {
1376
+                $uid = $vCard->UID->getValue();
1377
+                return $uid;
1378
+            }
1379
+            // should already be handled, but just in case
1380
+            throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1381
+        }
1382
+        // should already be handled, but just in case
1383
+        throw new BadRequest('vCard can not be empty');
1384
+    }
1385 1385
 }
Please login to merge, or discard this patch.
Spacing   +39 added lines, -39 removed lines patch added patch discarded remove patch
@@ -179,7 +179,7 @@  discard block
 block discarded – undo
179 179
 				'uri' => $row['uri'],
180 180
 				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
181 181
 				'{DAV:}displayname' => $row['displayname'],
182
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
182
+				'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
183 183
 				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
184 184
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
185 185
 			];
@@ -204,13 +204,13 @@  discard block
 block discarded – undo
204 204
 			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
205 205
 			->execute();
206 206
 
207
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
207
+		$readOnlyPropertyName = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only';
208 208
 		while ($row = $result->fetch()) {
209 209
 			if ($row['principaluri'] === $principalUri) {
210 210
 				continue;
211 211
 			}
212 212
 
213
-			$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
213
+			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
214 214
 			if (isset($addressBooks[$row['id']])) {
215 215
 				if ($readOnly) {
216 216
 					// New share can not have more permissions then the old one.
@@ -224,18 +224,18 @@  discard block
 block discarded – undo
224 224
 			}
225 225
 
226 226
 			[, $name] = \Sabre\Uri\split($row['principaluri']);
227
-			$uri = $row['uri'] . '_shared_by_' . $name;
228
-			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
227
+			$uri = $row['uri'].'_shared_by_'.$name;
228
+			$displayName = $row['displayname'].' ('.$this->getUserDisplayName($name).')';
229 229
 
230 230
 			$addressBooks[$row['id']] = [
231 231
 				'id' => $row['id'],
232 232
 				'uri' => $uri,
233 233
 				'principaluri' => $principalUriOriginal,
234 234
 				'{DAV:}displayname' => $displayName,
235
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
235
+				'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
236 236
 				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
237 237
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
238
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
238
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $row['principaluri'],
239 239
 				$readOnlyPropertyName => $readOnly,
240 240
 			];
241 241
 
@@ -262,7 +262,7 @@  discard block
 block discarded – undo
262 262
 				'uri' => $row['uri'],
263 263
 				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
264 264
 				'{DAV:}displayname' => $row['displayname'],
265
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
265
+				'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
266 266
 				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
267 267
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
268 268
 			];
@@ -309,7 +309,7 @@  discard block
 block discarded – undo
309 309
 			'uri' => $row['uri'],
310 310
 			'principaluri' => $row['principaluri'],
311 311
 			'{DAV:}displayname' => $row['displayname'],
312
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
312
+			'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
313 313
 			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
314 314
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
315 315
 		];
@@ -343,7 +343,7 @@  discard block
 block discarded – undo
343 343
 			'uri' => $row['uri'],
344 344
 			'principaluri' => $row['principaluri'],
345 345
 			'{DAV:}displayname' => $row['displayname'],
346
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
346
+			'{'.Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
347 347
 			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
348 348
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
349 349
 		];
@@ -372,17 +372,17 @@  discard block
 block discarded – undo
372 372
 	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
373 373
 		$supportedProperties = [
374 374
 			'{DAV:}displayname',
375
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
375
+			'{'.Plugin::NS_CARDDAV.'}addressbook-description',
376 376
 		];
377 377
 
378
-		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
378
+		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
379 379
 			$updates = [];
380 380
 			foreach ($mutations as $property => $newValue) {
381 381
 				switch ($property) {
382 382
 					case '{DAV:}displayname':
383 383
 						$updates['displayname'] = $newValue;
384 384
 						break;
385
-					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
385
+					case '{'.Plugin::NS_CARDDAV.'}addressbook-description':
386 386
 						$updates['description'] = $newValue;
387 387
 						break;
388 388
 				}
@@ -398,9 +398,9 @@  discard block
 block discarded – undo
398 398
 
399 399
 			$this->addChange($addressBookId, "", 2);
400 400
 
401
-			$addressBookRow = $this->getAddressBookById((int)$addressBookId);
401
+			$addressBookRow = $this->getAddressBookById((int) $addressBookId);
402 402
 			$shares = $this->getShares($addressBookId);
403
-			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
403
+			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int) $addressBookId, $addressBookRow, $shares, $mutations));
404 404
 
405 405
 			return true;
406 406
 		});
@@ -429,11 +429,11 @@  discard block
 block discarded – undo
429 429
 				case '{DAV:}displayname':
430 430
 					$values['displayname'] = $newValue;
431 431
 					break;
432
-				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
432
+				case '{'.Plugin::NS_CARDDAV.'}addressbook-description':
433 433
 					$values['description'] = $newValue;
434 434
 					break;
435 435
 				default:
436
-					throw new BadRequest('Unknown property: ' . $property);
436
+					throw new BadRequest('Unknown property: '.$property);
437 437
 			}
438 438
 		}
439 439
 
@@ -457,7 +457,7 @@  discard block
 block discarded – undo
457 457
 
458 458
 		$addressBookId = $query->getLastInsertId();
459 459
 		$addressBookRow = $this->getAddressBookById($addressBookId);
460
-		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
460
+		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int) $addressBookId, $addressBookRow));
461 461
 
462 462
 		return $addressBookId;
463 463
 	}
@@ -528,7 +528,7 @@  discard block
 block discarded – undo
528 528
 
529 529
 		$result = $query->execute();
530 530
 		while ($row = $result->fetch()) {
531
-			$row['etag'] = '"' . $row['etag'] . '"';
531
+			$row['etag'] = '"'.$row['etag'].'"';
532 532
 
533 533
 			$modified = false;
534 534
 			$row['carddata'] = $this->readBlob($row['carddata'], $modified);
@@ -568,7 +568,7 @@  discard block
 block discarded – undo
568 568
 		if (!$row) {
569 569
 			return false;
570 570
 		}
571
-		$row['etag'] = '"' . $row['etag'] . '"';
571
+		$row['etag'] = '"'.$row['etag'].'"';
572 572
 
573 573
 		$modified = false;
574 574
 		$row['carddata'] = $this->readBlob($row['carddata'], $modified);
@@ -610,7 +610,7 @@  discard block
 block discarded – undo
610 610
 			$result = $query->execute();
611 611
 
612 612
 			while ($row = $result->fetch()) {
613
-				$row['etag'] = '"' . $row['etag'] . '"';
613
+				$row['etag'] = '"'.$row['etag'].'"';
614 614
 
615 615
 				$modified = false;
616 616
 				$row['carddata'] = $this->readBlob($row['carddata'], $modified);
@@ -661,7 +661,7 @@  discard block
 block discarded – undo
661 661
 			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
662 662
 			->setMaxResults(1);
663 663
 		$result = $q->execute();
664
-		$count = (bool)$result->fetchOne();
664
+		$count = (bool) $result->fetchOne();
665 665
 		$result->closeCursor();
666 666
 		if ($count) {
667 667
 			throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
@@ -689,14 +689,14 @@  discard block
 block discarded – undo
689 689
 		$addressBookData = $this->getAddressBookById($addressBookId);
690 690
 		$shares = $this->getShares($addressBookId);
691 691
 		$objectRow = $this->getCard($addressBookId, $cardUri);
692
-		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
692
+		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int) $addressBookId, $addressBookData, $shares, $objectRow));
693 693
 		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
694 694
 			new GenericEvent(null, [
695 695
 				'addressBookId' => $addressBookId,
696 696
 				'cardUri' => $cardUri,
697 697
 				'cardData' => $cardData]));
698 698
 
699
-		return '"' . $etag . '"';
699
+		return '"'.$etag.'"';
700 700
 	}
701 701
 
702 702
 	/**
@@ -732,7 +732,7 @@  discard block
 block discarded – undo
732 732
 		// check for recently stored etag and stop if it is the same
733 733
 		$etagCacheKey = "$addressBookId#$cardUri";
734 734
 		if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
735
-			return '"' . $etag . '"';
735
+			return '"'.$etag.'"';
736 736
 		}
737 737
 
738 738
 		$query->update($this->dbCardsTable)
@@ -753,14 +753,14 @@  discard block
 block discarded – undo
753 753
 		$addressBookData = $this->getAddressBookById($addressBookId);
754 754
 		$shares = $this->getShares($addressBookId);
755 755
 		$objectRow = $this->getCard($addressBookId, $cardUri);
756
-		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
756
+		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int) $addressBookId, $addressBookData, $shares, $objectRow));
757 757
 		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
758 758
 			new GenericEvent(null, [
759 759
 				'addressBookId' => $addressBookId,
760 760
 				'cardUri' => $cardUri,
761 761
 				'cardData' => $cardData]));
762 762
 
763
-		return '"' . $etag . '"';
763
+		return '"'.$etag.'"';
764 764
 	}
765 765
 
766 766
 	/**
@@ -790,7 +790,7 @@  discard block
 block discarded – undo
790 790
 
791 791
 		if ($ret === 1) {
792 792
 			if ($cardId !== null) {
793
-				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, $addressBookData, $shares, $objectRow));
793
+				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int) $addressBookId, $addressBookData, $shares, $objectRow));
794 794
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
795 795
 					new GenericEvent(null, [
796 796
 						'addressBookId' => $addressBookId,
@@ -1043,7 +1043,7 @@  discard block
 block discarded – undo
1043 1043
 									   string $pattern,
1044 1044
 									   array $searchProperties,
1045 1045
 									   array $options = []): array {
1046
-		$addressBookIds = array_map(static function ($row):int {
1046
+		$addressBookIds = array_map(static function($row):int {
1047 1047
 			return (int) $row['id'];
1048 1048
 		}, $this->getAddressBooksForUser($principalUri));
1049 1049
 
@@ -1106,7 +1106,7 @@  discard block
 block discarded – undo
1106 1106
 			if (!$escapePattern) {
1107 1107
 				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1108 1108
 			} else {
1109
-				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1109
+				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%'.$this->db->escapeLikeParameter($pattern).'%')));
1110 1110
 			}
1111 1111
 		}
1112 1112
 
@@ -1120,8 +1120,8 @@  discard block
 block discarded – undo
1120 1120
 		$result = $query2->execute();
1121 1121
 		$matches = $result->fetchAll();
1122 1122
 		$result->closeCursor();
1123
-		$matches = array_map(function ($match) {
1124
-			return (int)$match['cardid'];
1123
+		$matches = array_map(function($match) {
1124
+			return (int) $match['cardid'];
1125 1125
 		}, $matches);
1126 1126
 
1127 1127
 		$query = $this->db->getQueryBuilder();
@@ -1134,7 +1134,7 @@  discard block
 block discarded – undo
1134 1134
 
1135 1135
 		$result->closeCursor();
1136 1136
 
1137
-		return array_map(function ($array) {
1137
+		return array_map(function($array) {
1138 1138
 			$array['addressbookid'] = (int) $array['addressbookid'];
1139 1139
 			$modified = false;
1140 1140
 			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
@@ -1181,7 +1181,7 @@  discard block
 block discarded – undo
1181 1181
 		$result->closeCursor();
1182 1182
 
1183 1183
 		if (!isset($uri['uri'])) {
1184
-			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1184
+			throw new \InvalidArgumentException('Card does not exists: '.$id);
1185 1185
 		}
1186 1186
 
1187 1187
 		return $uri['uri'];
@@ -1206,7 +1206,7 @@  discard block
 block discarded – undo
1206 1206
 
1207 1207
 		if (is_array($contact)) {
1208 1208
 			$modified = false;
1209
-			$contact['etag'] = '"' . $contact['etag'] . '"';
1209
+			$contact['etag'] = '"'.$contact['etag'].'"';
1210 1210
 			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1211 1211
 			if ($modified) {
1212 1212
 				$contact['size'] = strlen($contact['carddata']);
@@ -1319,10 +1319,10 @@  discard block
 block discarded – undo
1319 1319
 		$result->closeCursor();
1320 1320
 
1321 1321
 		if (!isset($cardIds['id'])) {
1322
-			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1322
+			throw new \InvalidArgumentException('Card does not exists: '.$uri);
1323 1323
 		}
1324 1324
 
1325
-		return (int)$cardIds['id'];
1325
+		return (int) $cardIds['id'];
1326 1326
 	}
1327 1327
 
1328 1328
 	/**
@@ -1348,8 +1348,8 @@  discard block
 block discarded – undo
1348 1348
 	}
1349 1349
 
1350 1350
 	private function addOwnerPrincipal(&$addressbookInfo) {
1351
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1352
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1351
+		$ownerPrincipalKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal';
1352
+		$displaynameKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}owner-displayname';
1353 1353
 		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1354 1354
 			$uri = $addressbookInfo[$ownerPrincipalKey];
1355 1355
 		} else {
Please login to merge, or discard this patch.
apps/dav/lib/CardDAV/Plugin.php 2 patches
Indentation   +39 added lines, -39 removed lines patch added patch discarded remove patch
@@ -31,46 +31,46 @@
 block discarded – undo
31 31
 use Sabre\DAV\Server;
32 32
 
33 33
 class Plugin extends \Sabre\CardDAV\Plugin {
34
-	public function initialize(Server $server) {
35
-		$server->on('propFind', [$this, 'propFind']);
36
-		parent::initialize($server);
37
-	}
34
+    public function initialize(Server $server) {
35
+        $server->on('propFind', [$this, 'propFind']);
36
+        parent::initialize($server);
37
+    }
38 38
 
39
-	/**
40
-	 * Returns the addressbook home for a given principal
41
-	 *
42
-	 * @param string $principal
43
-	 * @return string|null
44
-	 */
45
-	protected function getAddressbookHomeForPrincipal($principal) {
46
-		if (strrpos($principal, 'principals/users', -strlen($principal)) !== false) {
47
-			[, $principalId] = \Sabre\Uri\split($principal);
48
-			return self::ADDRESSBOOK_ROOT . '/users/' . $principalId;
49
-		}
50
-		if (strrpos($principal, 'principals/groups', -strlen($principal)) !== false) {
51
-			[, $principalId] = \Sabre\Uri\split($principal);
52
-			return self::ADDRESSBOOK_ROOT . '/groups/' . $principalId;
53
-		}
54
-		if (strrpos($principal, 'principals/system', -strlen($principal)) !== false) {
55
-			[, $principalId] = \Sabre\Uri\split($principal);
56
-			return self::ADDRESSBOOK_ROOT . '/system/' . $principalId;
57
-		}
58
-	}
39
+    /**
40
+     * Returns the addressbook home for a given principal
41
+     *
42
+     * @param string $principal
43
+     * @return string|null
44
+     */
45
+    protected function getAddressbookHomeForPrincipal($principal) {
46
+        if (strrpos($principal, 'principals/users', -strlen($principal)) !== false) {
47
+            [, $principalId] = \Sabre\Uri\split($principal);
48
+            return self::ADDRESSBOOK_ROOT . '/users/' . $principalId;
49
+        }
50
+        if (strrpos($principal, 'principals/groups', -strlen($principal)) !== false) {
51
+            [, $principalId] = \Sabre\Uri\split($principal);
52
+            return self::ADDRESSBOOK_ROOT . '/groups/' . $principalId;
53
+        }
54
+        if (strrpos($principal, 'principals/system', -strlen($principal)) !== false) {
55
+            [, $principalId] = \Sabre\Uri\split($principal);
56
+            return self::ADDRESSBOOK_ROOT . '/system/' . $principalId;
57
+        }
58
+    }
59 59
 
60
-	/**
61
-	 * Adds all CardDAV-specific properties
62
-	 *
63
-	 * @param PropFind $propFind
64
-	 * @param INode $node
65
-	 * @return void
66
-	 */
67
-	public function propFind(PropFind $propFind, INode $node) {
68
-		$ns = '{http://owncloud.org/ns}';
60
+    /**
61
+     * Adds all CardDAV-specific properties
62
+     *
63
+     * @param PropFind $propFind
64
+     * @param INode $node
65
+     * @return void
66
+     */
67
+    public function propFind(PropFind $propFind, INode $node) {
68
+        $ns = '{http://owncloud.org/ns}';
69 69
 
70
-		if ($node instanceof AddressBook) {
71
-			$propFind->handle($ns . 'groups', function () use ($node) {
72
-				return new Groups($node->getContactsGroups());
73
-			});
74
-		}
75
-	}
70
+        if ($node instanceof AddressBook) {
71
+            $propFind->handle($ns . 'groups', function () use ($node) {
72
+                return new Groups($node->getContactsGroups());
73
+            });
74
+        }
75
+    }
76 76
 }
Please login to merge, or discard this patch.
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -45,15 +45,15 @@  discard block
 block discarded – undo
45 45
 	protected function getAddressbookHomeForPrincipal($principal) {
46 46
 		if (strrpos($principal, 'principals/users', -strlen($principal)) !== false) {
47 47
 			[, $principalId] = \Sabre\Uri\split($principal);
48
-			return self::ADDRESSBOOK_ROOT . '/users/' . $principalId;
48
+			return self::ADDRESSBOOK_ROOT.'/users/'.$principalId;
49 49
 		}
50 50
 		if (strrpos($principal, 'principals/groups', -strlen($principal)) !== false) {
51 51
 			[, $principalId] = \Sabre\Uri\split($principal);
52
-			return self::ADDRESSBOOK_ROOT . '/groups/' . $principalId;
52
+			return self::ADDRESSBOOK_ROOT.'/groups/'.$principalId;
53 53
 		}
54 54
 		if (strrpos($principal, 'principals/system', -strlen($principal)) !== false) {
55 55
 			[, $principalId] = \Sabre\Uri\split($principal);
56
-			return self::ADDRESSBOOK_ROOT . '/system/' . $principalId;
56
+			return self::ADDRESSBOOK_ROOT.'/system/'.$principalId;
57 57
 		}
58 58
 	}
59 59
 
@@ -68,7 +68,7 @@  discard block
 block discarded – undo
68 68
 		$ns = '{http://owncloud.org/ns}';
69 69
 
70 70
 		if ($node instanceof AddressBook) {
71
-			$propFind->handle($ns . 'groups', function () use ($node) {
71
+			$propFind->handle($ns.'groups', function() use ($node) {
72 72
 				return new Groups($node->getContactsGroups());
73 73
 			});
74 74
 		}
Please login to merge, or discard this patch.
apps/dav/lib/CardDAV/PhotoCache.php 1 patch
Indentation   +248 added lines, -248 removed lines patch added patch discarded remove patch
@@ -43,252 +43,252 @@
 block discarded – undo
43 43
 
44 44
 class PhotoCache {
45 45
 
46
-	/** @var array  */
47
-	public const ALLOWED_CONTENT_TYPES = [
48
-		'image/png' => 'png',
49
-		'image/jpeg' => 'jpg',
50
-		'image/gif' => 'gif',
51
-		'image/vnd.microsoft.icon' => 'ico',
52
-	];
53
-
54
-	/** @var IAppData */
55
-	protected $appData;
56
-
57
-	/** @var ILogger */
58
-	protected $logger;
59
-
60
-	/**
61
-	 * PhotoCache constructor.
62
-	 *
63
-	 * @param IAppData $appData
64
-	 * @param ILogger $logger
65
-	 */
66
-	public function __construct(IAppData $appData, ILogger $logger) {
67
-		$this->appData = $appData;
68
-		$this->logger = $logger;
69
-	}
70
-
71
-	/**
72
-	 * @param int $addressBookId
73
-	 * @param string $cardUri
74
-	 * @param int $size
75
-	 * @param Card $card
76
-	 *
77
-	 * @return ISimpleFile
78
-	 * @throws NotFoundException
79
-	 */
80
-	public function get($addressBookId, $cardUri, $size, Card $card) {
81
-		$folder = $this->getFolder($addressBookId, $cardUri);
82
-
83
-		if ($this->isEmpty($folder)) {
84
-			$this->init($folder, $card);
85
-		}
86
-
87
-		if (!$this->hasPhoto($folder)) {
88
-			throw new NotFoundException();
89
-		}
90
-
91
-		if ($size !== -1) {
92
-			$size = 2 ** ceil(log($size) / log(2));
93
-		}
94
-
95
-		return $this->getFile($folder, $size);
96
-	}
97
-
98
-	/**
99
-	 * @param ISimpleFolder $folder
100
-	 * @return bool
101
-	 */
102
-	private function isEmpty(ISimpleFolder $folder) {
103
-		return $folder->getDirectoryListing() === [];
104
-	}
105
-
106
-	/**
107
-	 * @param ISimpleFolder $folder
108
-	 * @param Card $card
109
-	 * @throws NotPermittedException
110
-	 */
111
-	private function init(ISimpleFolder $folder, Card $card): void {
112
-		$data = $this->getPhoto($card);
113
-
114
-		if ($data === false || !isset($data['Content-Type'])) {
115
-			$folder->newFile('nophoto', '');
116
-			return;
117
-		}
118
-
119
-		$contentType = $data['Content-Type'];
120
-		$extension = self::ALLOWED_CONTENT_TYPES[$contentType] ?? null;
121
-
122
-		if ($extension === null) {
123
-			$folder->newFile('nophoto', '');
124
-			return;
125
-		}
126
-
127
-		$file = $folder->newFile('photo.' . $extension);
128
-		$file->putContent($data['body']);
129
-	}
130
-
131
-	private function hasPhoto(ISimpleFolder $folder) {
132
-		return !$folder->fileExists('nophoto');
133
-	}
134
-
135
-	private function getFile(ISimpleFolder $folder, $size) {
136
-		$ext = $this->getExtension($folder);
137
-
138
-		if ($size === -1) {
139
-			$path = 'photo.' . $ext;
140
-		} else {
141
-			$path = 'photo.' . $size . '.' . $ext;
142
-		}
143
-
144
-		try {
145
-			$file = $folder->getFile($path);
146
-		} catch (NotFoundException $e) {
147
-			if ($size <= 0) {
148
-				throw new NotFoundException;
149
-			}
150
-
151
-			$photo = new \OC_Image();
152
-			/** @var ISimpleFile $file */
153
-			$file = $folder->getFile('photo.' . $ext);
154
-			$photo->loadFromData($file->getContent());
155
-
156
-			$ratio = $photo->width() / $photo->height();
157
-			if ($ratio < 1) {
158
-				$ratio = 1 / $ratio;
159
-			}
160
-
161
-			$size = (int) ($size * $ratio);
162
-			if ($size !== -1) {
163
-				$photo->resize($size);
164
-			}
165
-
166
-			try {
167
-				$file = $folder->newFile($path);
168
-				$file->putContent($photo->data());
169
-			} catch (NotPermittedException $e) {
170
-			}
171
-		}
172
-
173
-		return $file;
174
-	}
175
-
176
-	/**
177
-	 * @throws NotFoundException
178
-	 * @throws NotPermittedException
179
-	 */
180
-	private function getFolder(int $addressBookId, string $cardUri, bool $createIfNotExists = true): ISimpleFolder {
181
-		$hash = md5($addressBookId . ' ' . $cardUri);
182
-		try {
183
-			return $this->appData->getFolder($hash);
184
-		} catch (NotFoundException $e) {
185
-			if ($createIfNotExists) {
186
-				return $this->appData->newFolder($hash);
187
-			} else {
188
-				throw $e;
189
-			}
190
-		}
191
-	}
192
-
193
-	/**
194
-	 * Get the extension of the avatar. If there is no avatar throw Exception
195
-	 *
196
-	 * @param ISimpleFolder $folder
197
-	 * @return string
198
-	 * @throws NotFoundException
199
-	 */
200
-	private function getExtension(ISimpleFolder $folder): string {
201
-		foreach (self::ALLOWED_CONTENT_TYPES as $extension) {
202
-			if ($folder->fileExists('photo.' . $extension)) {
203
-				return $extension;
204
-			}
205
-		}
206
-
207
-		throw new NotFoundException('Avatar not found');
208
-	}
209
-
210
-	private function getPhoto(Card $node) {
211
-		try {
212
-			$vObject = $this->readCard($node->get());
213
-			if (!$vObject->PHOTO) {
214
-				return false;
215
-			}
216
-
217
-			$photo = $vObject->PHOTO;
218
-			$val = $photo->getValue();
219
-
220
-			// handle data URI. e.g PHOTO;VALUE=URI:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQE
221
-			if ($photo->getValueType() === 'URI') {
222
-				$parsed = \Sabre\URI\parse($val);
223
-
224
-				// only allow data://
225
-				if ($parsed['scheme'] !== 'data') {
226
-					return false;
227
-				}
228
-				if (substr_count($parsed['path'], ';') === 1) {
229
-					[$type] = explode(';', $parsed['path']);
230
-				}
231
-				$val = file_get_contents($val);
232
-			} else {
233
-				// get type if binary data
234
-				$type = $this->getBinaryType($photo);
235
-			}
236
-
237
-			if (empty($type) || !isset(self::ALLOWED_CONTENT_TYPES[$type])) {
238
-				$type = 'application/octet-stream';
239
-			}
240
-
241
-			return [
242
-				'Content-Type' => $type,
243
-				'body' => $val
244
-			];
245
-		} catch (\Exception $e) {
246
-			$this->logger->logException($e, [
247
-				'message' => 'Exception during vcard photo parsing'
248
-			]);
249
-		}
250
-		return false;
251
-	}
252
-
253
-	/**
254
-	 * @param string $cardData
255
-	 * @return \Sabre\VObject\Document
256
-	 */
257
-	private function readCard($cardData) {
258
-		return Reader::read($cardData);
259
-	}
260
-
261
-	/**
262
-	 * @param Binary $photo
263
-	 * @return string
264
-	 */
265
-	private function getBinaryType(Binary $photo) {
266
-		$params = $photo->parameters();
267
-		if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) {
268
-			/** @var Parameter $typeParam */
269
-			$typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE'];
270
-			$type = $typeParam->getValue();
271
-
272
-			if (strpos($type, 'image/') === 0) {
273
-				return $type;
274
-			} else {
275
-				return 'image/' . strtolower($type);
276
-			}
277
-		}
278
-		return '';
279
-	}
280
-
281
-	/**
282
-	 * @param int $addressBookId
283
-	 * @param string $cardUri
284
-	 * @throws NotPermittedException
285
-	 */
286
-	public function delete($addressBookId, $cardUri) {
287
-		try {
288
-			$folder = $this->getFolder($addressBookId, $cardUri, false);
289
-			$folder->delete();
290
-		} catch (NotFoundException $e) {
291
-			// that's OK, nothing to do
292
-		}
293
-	}
46
+    /** @var array  */
47
+    public const ALLOWED_CONTENT_TYPES = [
48
+        'image/png' => 'png',
49
+        'image/jpeg' => 'jpg',
50
+        'image/gif' => 'gif',
51
+        'image/vnd.microsoft.icon' => 'ico',
52
+    ];
53
+
54
+    /** @var IAppData */
55
+    protected $appData;
56
+
57
+    /** @var ILogger */
58
+    protected $logger;
59
+
60
+    /**
61
+     * PhotoCache constructor.
62
+     *
63
+     * @param IAppData $appData
64
+     * @param ILogger $logger
65
+     */
66
+    public function __construct(IAppData $appData, ILogger $logger) {
67
+        $this->appData = $appData;
68
+        $this->logger = $logger;
69
+    }
70
+
71
+    /**
72
+     * @param int $addressBookId
73
+     * @param string $cardUri
74
+     * @param int $size
75
+     * @param Card $card
76
+     *
77
+     * @return ISimpleFile
78
+     * @throws NotFoundException
79
+     */
80
+    public function get($addressBookId, $cardUri, $size, Card $card) {
81
+        $folder = $this->getFolder($addressBookId, $cardUri);
82
+
83
+        if ($this->isEmpty($folder)) {
84
+            $this->init($folder, $card);
85
+        }
86
+
87
+        if (!$this->hasPhoto($folder)) {
88
+            throw new NotFoundException();
89
+        }
90
+
91
+        if ($size !== -1) {
92
+            $size = 2 ** ceil(log($size) / log(2));
93
+        }
94
+
95
+        return $this->getFile($folder, $size);
96
+    }
97
+
98
+    /**
99
+     * @param ISimpleFolder $folder
100
+     * @return bool
101
+     */
102
+    private function isEmpty(ISimpleFolder $folder) {
103
+        return $folder->getDirectoryListing() === [];
104
+    }
105
+
106
+    /**
107
+     * @param ISimpleFolder $folder
108
+     * @param Card $card
109
+     * @throws NotPermittedException
110
+     */
111
+    private function init(ISimpleFolder $folder, Card $card): void {
112
+        $data = $this->getPhoto($card);
113
+
114
+        if ($data === false || !isset($data['Content-Type'])) {
115
+            $folder->newFile('nophoto', '');
116
+            return;
117
+        }
118
+
119
+        $contentType = $data['Content-Type'];
120
+        $extension = self::ALLOWED_CONTENT_TYPES[$contentType] ?? null;
121
+
122
+        if ($extension === null) {
123
+            $folder->newFile('nophoto', '');
124
+            return;
125
+        }
126
+
127
+        $file = $folder->newFile('photo.' . $extension);
128
+        $file->putContent($data['body']);
129
+    }
130
+
131
+    private function hasPhoto(ISimpleFolder $folder) {
132
+        return !$folder->fileExists('nophoto');
133
+    }
134
+
135
+    private function getFile(ISimpleFolder $folder, $size) {
136
+        $ext = $this->getExtension($folder);
137
+
138
+        if ($size === -1) {
139
+            $path = 'photo.' . $ext;
140
+        } else {
141
+            $path = 'photo.' . $size . '.' . $ext;
142
+        }
143
+
144
+        try {
145
+            $file = $folder->getFile($path);
146
+        } catch (NotFoundException $e) {
147
+            if ($size <= 0) {
148
+                throw new NotFoundException;
149
+            }
150
+
151
+            $photo = new \OC_Image();
152
+            /** @var ISimpleFile $file */
153
+            $file = $folder->getFile('photo.' . $ext);
154
+            $photo->loadFromData($file->getContent());
155
+
156
+            $ratio = $photo->width() / $photo->height();
157
+            if ($ratio < 1) {
158
+                $ratio = 1 / $ratio;
159
+            }
160
+
161
+            $size = (int) ($size * $ratio);
162
+            if ($size !== -1) {
163
+                $photo->resize($size);
164
+            }
165
+
166
+            try {
167
+                $file = $folder->newFile($path);
168
+                $file->putContent($photo->data());
169
+            } catch (NotPermittedException $e) {
170
+            }
171
+        }
172
+
173
+        return $file;
174
+    }
175
+
176
+    /**
177
+     * @throws NotFoundException
178
+     * @throws NotPermittedException
179
+     */
180
+    private function getFolder(int $addressBookId, string $cardUri, bool $createIfNotExists = true): ISimpleFolder {
181
+        $hash = md5($addressBookId . ' ' . $cardUri);
182
+        try {
183
+            return $this->appData->getFolder($hash);
184
+        } catch (NotFoundException $e) {
185
+            if ($createIfNotExists) {
186
+                return $this->appData->newFolder($hash);
187
+            } else {
188
+                throw $e;
189
+            }
190
+        }
191
+    }
192
+
193
+    /**
194
+     * Get the extension of the avatar. If there is no avatar throw Exception
195
+     *
196
+     * @param ISimpleFolder $folder
197
+     * @return string
198
+     * @throws NotFoundException
199
+     */
200
+    private function getExtension(ISimpleFolder $folder): string {
201
+        foreach (self::ALLOWED_CONTENT_TYPES as $extension) {
202
+            if ($folder->fileExists('photo.' . $extension)) {
203
+                return $extension;
204
+            }
205
+        }
206
+
207
+        throw new NotFoundException('Avatar not found');
208
+    }
209
+
210
+    private function getPhoto(Card $node) {
211
+        try {
212
+            $vObject = $this->readCard($node->get());
213
+            if (!$vObject->PHOTO) {
214
+                return false;
215
+            }
216
+
217
+            $photo = $vObject->PHOTO;
218
+            $val = $photo->getValue();
219
+
220
+            // handle data URI. e.g PHOTO;VALUE=URI:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQE
221
+            if ($photo->getValueType() === 'URI') {
222
+                $parsed = \Sabre\URI\parse($val);
223
+
224
+                // only allow data://
225
+                if ($parsed['scheme'] !== 'data') {
226
+                    return false;
227
+                }
228
+                if (substr_count($parsed['path'], ';') === 1) {
229
+                    [$type] = explode(';', $parsed['path']);
230
+                }
231
+                $val = file_get_contents($val);
232
+            } else {
233
+                // get type if binary data
234
+                $type = $this->getBinaryType($photo);
235
+            }
236
+
237
+            if (empty($type) || !isset(self::ALLOWED_CONTENT_TYPES[$type])) {
238
+                $type = 'application/octet-stream';
239
+            }
240
+
241
+            return [
242
+                'Content-Type' => $type,
243
+                'body' => $val
244
+            ];
245
+        } catch (\Exception $e) {
246
+            $this->logger->logException($e, [
247
+                'message' => 'Exception during vcard photo parsing'
248
+            ]);
249
+        }
250
+        return false;
251
+    }
252
+
253
+    /**
254
+     * @param string $cardData
255
+     * @return \Sabre\VObject\Document
256
+     */
257
+    private function readCard($cardData) {
258
+        return Reader::read($cardData);
259
+    }
260
+
261
+    /**
262
+     * @param Binary $photo
263
+     * @return string
264
+     */
265
+    private function getBinaryType(Binary $photo) {
266
+        $params = $photo->parameters();
267
+        if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) {
268
+            /** @var Parameter $typeParam */
269
+            $typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE'];
270
+            $type = $typeParam->getValue();
271
+
272
+            if (strpos($type, 'image/') === 0) {
273
+                return $type;
274
+            } else {
275
+                return 'image/' . strtolower($type);
276
+            }
277
+        }
278
+        return '';
279
+    }
280
+
281
+    /**
282
+     * @param int $addressBookId
283
+     * @param string $cardUri
284
+     * @throws NotPermittedException
285
+     */
286
+    public function delete($addressBookId, $cardUri) {
287
+        try {
288
+            $folder = $this->getFolder($addressBookId, $cardUri, false);
289
+            $folder->delete();
290
+        } catch (NotFoundException $e) {
291
+            // that's OK, nothing to do
292
+        }
293
+    }
294 294
 }
Please login to merge, or discard this patch.
apps/dav/lib/Command/MoveCalendar.php 1 patch
Indentation   +196 added lines, -196 removed lines patch added patch discarded remove patch
@@ -44,200 +44,200 @@
 block discarded – undo
44 44
 
45 45
 class MoveCalendar extends Command {
46 46
 
47
-	/** @var IUserManager */
48
-	private $userManager;
49
-
50
-	/** @var IGroupManager */
51
-	private $groupManager;
52
-
53
-	/** @var IShareManager */
54
-	private $shareManager;
55
-
56
-	/** @var IConfig $config */
57
-	private $config;
58
-
59
-	/** @var IL10N */
60
-	private $l10n;
61
-
62
-	/** @var SymfonyStyle */
63
-	private $io;
64
-
65
-	/** @var CalDavBackend */
66
-	private $calDav;
67
-
68
-	public const URI_USERS = 'principals/users/';
69
-
70
-	/**
71
-	 * @param IUserManager $userManager
72
-	 * @param IGroupManager $groupManager
73
-	 * @param IShareManager $shareManager
74
-	 * @param IConfig $config
75
-	 * @param IL10N $l10n
76
-	 * @param CalDavBackend $calDav
77
-	 */
78
-	public function __construct(
79
-		IUserManager $userManager,
80
-		IGroupManager $groupManager,
81
-		IShareManager $shareManager,
82
-		IConfig $config,
83
-		IL10N $l10n,
84
-		CalDavBackend $calDav
85
-	) {
86
-		parent::__construct();
87
-		$this->userManager = $userManager;
88
-		$this->groupManager = $groupManager;
89
-		$this->shareManager = $shareManager;
90
-		$this->config = $config;
91
-		$this->l10n = $l10n;
92
-		$this->calDav = $calDav;
93
-	}
94
-
95
-	protected function configure() {
96
-		$this
97
-			->setName('dav:move-calendar')
98
-			->setDescription('Move a calendar from an user to another')
99
-			->addArgument('name',
100
-				InputArgument::REQUIRED,
101
-				'Name of the calendar to move')
102
-			->addArgument('sourceuid',
103
-				InputArgument::REQUIRED,
104
-				'User who currently owns the calendar')
105
-			->addArgument('destinationuid',
106
-				InputArgument::REQUIRED,
107
-				'User who will receive the calendar')
108
-			->addOption('force', 'f', InputOption::VALUE_NONE, "Force the migration by removing existing shares and renaming calendars in case of conflicts");
109
-	}
110
-
111
-	protected function execute(InputInterface $input, OutputInterface $output): int {
112
-		$userOrigin = $input->getArgument('sourceuid');
113
-		$userDestination = $input->getArgument('destinationuid');
114
-
115
-		$this->io = new SymfonyStyle($input, $output);
116
-
117
-		if (!$this->userManager->userExists($userOrigin)) {
118
-			throw new \InvalidArgumentException("User <$userOrigin> is unknown.");
119
-		}
120
-
121
-		if (!$this->userManager->userExists($userDestination)) {
122
-			throw new \InvalidArgumentException("User <$userDestination> is unknown.");
123
-		}
124
-
125
-		$name = $input->getArgument('name');
126
-		$newName = null;
127
-
128
-		$calendar = $this->calDav->getCalendarByUri(self::URI_USERS . $userOrigin, $name);
129
-
130
-		if (null === $calendar) {
131
-			throw new \InvalidArgumentException("User <$userOrigin> has no calendar named <$name>. You can run occ dav:list-calendars to list calendars URIs for this user.");
132
-		}
133
-
134
-		// Calendar already exists
135
-		if ($this->calendarExists($userDestination, $name)) {
136
-			if ($input->getOption('force')) {
137
-				// Try to find a suitable name
138
-				$newName = $this->getNewCalendarName($userDestination, $name);
139
-
140
-				// If we didn't find a suitable value after all the iterations, give up
141
-				if ($this->calendarExists($userDestination, $newName)) {
142
-					throw new \InvalidArgumentException("Unable to find a suitable calendar name for <$userDestination> with initial name <$name>.");
143
-				}
144
-			} else {
145
-				throw new \InvalidArgumentException("User <$userDestination> already has a calendar named <$name>.");
146
-			}
147
-		}
148
-
149
-		$hadShares = $this->checkShares($calendar, $userOrigin, $userDestination, $input->getOption('force'));
150
-		if ($hadShares) {
151
-			/**
152
-			 * Warn that share links have changed if there are shares
153
-			 */
154
-			$this->io->note([
155
-				"Please note that moving calendar " . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.",
156
-				"Sharees will need to change \"example.com/remote.php/dav/calendars/uid/" . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\""
157
-			]);
158
-		}
159
-
160
-		$this->calDav->moveCalendar($name, self::URI_USERS . $userOrigin, self::URI_USERS . $userDestination, $newName);
161
-
162
-		$this->io->success("Calendar <$name> was moved from user <$userOrigin> to <$userDestination>" . ($newName ? " as <$newName>" : ''));
163
-		return 0;
164
-	}
165
-
166
-	/**
167
-	 * Check if the calendar exists for user
168
-	 *
169
-	 * @param string $userDestination
170
-	 * @param string $name
171
-	 * @return bool
172
-	 */
173
-	protected function calendarExists(string $userDestination, string $name): bool {
174
-		return null !== $this->calDav->getCalendarByUri(self::URI_USERS . $userDestination, $name);
175
-	}
176
-
177
-	/**
178
-	 * Try to find a suitable new calendar name that
179
-	 * doesn't exists for the provided user
180
-	 *
181
-	 * @param string $userDestination
182
-	 * @param string $name
183
-	 * @return string
184
-	 */
185
-	protected function getNewCalendarName(string $userDestination, string $name): string {
186
-		$increment = 1;
187
-		$newName = $name . '-' . $increment;
188
-		while ($increment <= 10) {
189
-			$this->io->writeln("Trying calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE);
190
-			if (!$this->calendarExists($userDestination, $newName)) {
191
-				// New name is good to go
192
-				$this->io->writeln("Found proper new calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE);
193
-				break;
194
-			}
195
-			$newName = $name . '-' . $increment;
196
-			$increment++;
197
-		}
198
-
199
-		return $newName;
200
-	}
201
-
202
-	/**
203
-	 * Check that moving the calendar won't break shares
204
-	 *
205
-	 * @param array $calendar
206
-	 * @param string $userOrigin
207
-	 * @param string $userDestination
208
-	 * @param bool $force
209
-	 * @return bool had any shares or not
210
-	 * @throws \InvalidArgumentException
211
-	 */
212
-	private function checkShares(array $calendar, string $userOrigin, string $userDestination, bool $force = false): bool {
213
-		$shares = $this->calDav->getShares($calendar['id']);
214
-		foreach ($shares as $share) {
215
-			[, $prefix, $userOrGroup] = explode('/', $share['href'], 3);
216
-
217
-			/**
218
-			 * Check that user destination is member of the groups which whom the calendar was shared
219
-			 * If we ask to force the migration, the share with the group is dropped
220
-			 */
221
-			if ($this->shareManager->shareWithGroupMembersOnly() === true && 'groups' === $prefix && !$this->groupManager->isInGroup($userDestination, $userOrGroup)) {
222
-				if ($force) {
223
-					$this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config), [], ['href' => 'principal:principals/groups/' . $userOrGroup]);
224
-				} else {
225
-					throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . "> was shared. You may use -f to move the calendar while deleting this share.");
226
-				}
227
-			}
228
-
229
-			/**
230
-			 * Check that calendar isn't already shared with user destination
231
-			 */
232
-			if ($userOrGroup === $userDestination) {
233
-				if ($force) {
234
-					$this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config), [], ['href' => 'principal:principals/users/' . $userOrGroup]);
235
-				} else {
236
-					throw new \InvalidArgumentException("The calendar <" . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share.");
237
-				}
238
-			}
239
-		}
240
-
241
-		return count($shares) > 0;
242
-	}
47
+    /** @var IUserManager */
48
+    private $userManager;
49
+
50
+    /** @var IGroupManager */
51
+    private $groupManager;
52
+
53
+    /** @var IShareManager */
54
+    private $shareManager;
55
+
56
+    /** @var IConfig $config */
57
+    private $config;
58
+
59
+    /** @var IL10N */
60
+    private $l10n;
61
+
62
+    /** @var SymfonyStyle */
63
+    private $io;
64
+
65
+    /** @var CalDavBackend */
66
+    private $calDav;
67
+
68
+    public const URI_USERS = 'principals/users/';
69
+
70
+    /**
71
+     * @param IUserManager $userManager
72
+     * @param IGroupManager $groupManager
73
+     * @param IShareManager $shareManager
74
+     * @param IConfig $config
75
+     * @param IL10N $l10n
76
+     * @param CalDavBackend $calDav
77
+     */
78
+    public function __construct(
79
+        IUserManager $userManager,
80
+        IGroupManager $groupManager,
81
+        IShareManager $shareManager,
82
+        IConfig $config,
83
+        IL10N $l10n,
84
+        CalDavBackend $calDav
85
+    ) {
86
+        parent::__construct();
87
+        $this->userManager = $userManager;
88
+        $this->groupManager = $groupManager;
89
+        $this->shareManager = $shareManager;
90
+        $this->config = $config;
91
+        $this->l10n = $l10n;
92
+        $this->calDav = $calDav;
93
+    }
94
+
95
+    protected function configure() {
96
+        $this
97
+            ->setName('dav:move-calendar')
98
+            ->setDescription('Move a calendar from an user to another')
99
+            ->addArgument('name',
100
+                InputArgument::REQUIRED,
101
+                'Name of the calendar to move')
102
+            ->addArgument('sourceuid',
103
+                InputArgument::REQUIRED,
104
+                'User who currently owns the calendar')
105
+            ->addArgument('destinationuid',
106
+                InputArgument::REQUIRED,
107
+                'User who will receive the calendar')
108
+            ->addOption('force', 'f', InputOption::VALUE_NONE, "Force the migration by removing existing shares and renaming calendars in case of conflicts");
109
+    }
110
+
111
+    protected function execute(InputInterface $input, OutputInterface $output): int {
112
+        $userOrigin = $input->getArgument('sourceuid');
113
+        $userDestination = $input->getArgument('destinationuid');
114
+
115
+        $this->io = new SymfonyStyle($input, $output);
116
+
117
+        if (!$this->userManager->userExists($userOrigin)) {
118
+            throw new \InvalidArgumentException("User <$userOrigin> is unknown.");
119
+        }
120
+
121
+        if (!$this->userManager->userExists($userDestination)) {
122
+            throw new \InvalidArgumentException("User <$userDestination> is unknown.");
123
+        }
124
+
125
+        $name = $input->getArgument('name');
126
+        $newName = null;
127
+
128
+        $calendar = $this->calDav->getCalendarByUri(self::URI_USERS . $userOrigin, $name);
129
+
130
+        if (null === $calendar) {
131
+            throw new \InvalidArgumentException("User <$userOrigin> has no calendar named <$name>. You can run occ dav:list-calendars to list calendars URIs for this user.");
132
+        }
133
+
134
+        // Calendar already exists
135
+        if ($this->calendarExists($userDestination, $name)) {
136
+            if ($input->getOption('force')) {
137
+                // Try to find a suitable name
138
+                $newName = $this->getNewCalendarName($userDestination, $name);
139
+
140
+                // If we didn't find a suitable value after all the iterations, give up
141
+                if ($this->calendarExists($userDestination, $newName)) {
142
+                    throw new \InvalidArgumentException("Unable to find a suitable calendar name for <$userDestination> with initial name <$name>.");
143
+                }
144
+            } else {
145
+                throw new \InvalidArgumentException("User <$userDestination> already has a calendar named <$name>.");
146
+            }
147
+        }
148
+
149
+        $hadShares = $this->checkShares($calendar, $userOrigin, $userDestination, $input->getOption('force'));
150
+        if ($hadShares) {
151
+            /**
152
+             * Warn that share links have changed if there are shares
153
+             */
154
+            $this->io->note([
155
+                "Please note that moving calendar " . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.",
156
+                "Sharees will need to change \"example.com/remote.php/dav/calendars/uid/" . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\""
157
+            ]);
158
+        }
159
+
160
+        $this->calDav->moveCalendar($name, self::URI_USERS . $userOrigin, self::URI_USERS . $userDestination, $newName);
161
+
162
+        $this->io->success("Calendar <$name> was moved from user <$userOrigin> to <$userDestination>" . ($newName ? " as <$newName>" : ''));
163
+        return 0;
164
+    }
165
+
166
+    /**
167
+     * Check if the calendar exists for user
168
+     *
169
+     * @param string $userDestination
170
+     * @param string $name
171
+     * @return bool
172
+     */
173
+    protected function calendarExists(string $userDestination, string $name): bool {
174
+        return null !== $this->calDav->getCalendarByUri(self::URI_USERS . $userDestination, $name);
175
+    }
176
+
177
+    /**
178
+     * Try to find a suitable new calendar name that
179
+     * doesn't exists for the provided user
180
+     *
181
+     * @param string $userDestination
182
+     * @param string $name
183
+     * @return string
184
+     */
185
+    protected function getNewCalendarName(string $userDestination, string $name): string {
186
+        $increment = 1;
187
+        $newName = $name . '-' . $increment;
188
+        while ($increment <= 10) {
189
+            $this->io->writeln("Trying calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE);
190
+            if (!$this->calendarExists($userDestination, $newName)) {
191
+                // New name is good to go
192
+                $this->io->writeln("Found proper new calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE);
193
+                break;
194
+            }
195
+            $newName = $name . '-' . $increment;
196
+            $increment++;
197
+        }
198
+
199
+        return $newName;
200
+    }
201
+
202
+    /**
203
+     * Check that moving the calendar won't break shares
204
+     *
205
+     * @param array $calendar
206
+     * @param string $userOrigin
207
+     * @param string $userDestination
208
+     * @param bool $force
209
+     * @return bool had any shares or not
210
+     * @throws \InvalidArgumentException
211
+     */
212
+    private function checkShares(array $calendar, string $userOrigin, string $userDestination, bool $force = false): bool {
213
+        $shares = $this->calDav->getShares($calendar['id']);
214
+        foreach ($shares as $share) {
215
+            [, $prefix, $userOrGroup] = explode('/', $share['href'], 3);
216
+
217
+            /**
218
+             * Check that user destination is member of the groups which whom the calendar was shared
219
+             * If we ask to force the migration, the share with the group is dropped
220
+             */
221
+            if ($this->shareManager->shareWithGroupMembersOnly() === true && 'groups' === $prefix && !$this->groupManager->isInGroup($userDestination, $userOrGroup)) {
222
+                if ($force) {
223
+                    $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config), [], ['href' => 'principal:principals/groups/' . $userOrGroup]);
224
+                } else {
225
+                    throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . "> was shared. You may use -f to move the calendar while deleting this share.");
226
+                }
227
+            }
228
+
229
+            /**
230
+             * Check that calendar isn't already shared with user destination
231
+             */
232
+            if ($userOrGroup === $userDestination) {
233
+                if ($force) {
234
+                    $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config), [], ['href' => 'principal:principals/users/' . $userOrGroup]);
235
+                } else {
236
+                    throw new \InvalidArgumentException("The calendar <" . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share.");
237
+                }
238
+            }
239
+        }
240
+
241
+        return count($shares) > 0;
242
+    }
243 243
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/UploadHome.php 1 patch
Indentation   +66 added lines, -66 removed lines patch added patch discarded remove patch
@@ -34,70 +34,70 @@
 block discarded – undo
34 34
 
35 35
 class UploadHome implements ICollection {
36 36
 
37
-	/** @var array */
38
-	private $principalInfo;
39
-	/** @var CleanupService */
40
-	private $cleanupService;
41
-
42
-	public function __construct(array $principalInfo, CleanupService $cleanupService) {
43
-		$this->principalInfo = $principalInfo;
44
-		$this->cleanupService = $cleanupService;
45
-	}
46
-
47
-	public function createFile($name, $data = null) {
48
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
49
-	}
50
-
51
-	public function createDirectory($name) {
52
-		$this->impl()->createDirectory($name);
53
-
54
-		// Add a cleanup job
55
-		$this->cleanupService->addJob($name);
56
-	}
57
-
58
-	public function getChild($name): UploadFolder {
59
-		return new UploadFolder($this->impl()->getChild($name), $this->cleanupService);
60
-	}
61
-
62
-	public function getChildren(): array {
63
-		return array_map(function ($node) {
64
-			return new UploadFolder($node, $this->cleanupService);
65
-		}, $this->impl()->getChildren());
66
-	}
67
-
68
-	public function childExists($name): bool {
69
-		return !is_null($this->getChild($name));
70
-	}
71
-
72
-	public function delete() {
73
-		$this->impl()->delete();
74
-	}
75
-
76
-	public function getName() {
77
-		[,$name] = \Sabre\Uri\split($this->principalInfo['uri']);
78
-		return $name;
79
-	}
80
-
81
-	public function setName($name) {
82
-		throw new Forbidden('Permission denied to rename this folder');
83
-	}
84
-
85
-	public function getLastModified() {
86
-		return $this->impl()->getLastModified();
87
-	}
88
-
89
-	/**
90
-	 * @return Directory
91
-	 */
92
-	private function impl() {
93
-		$rootView = new View();
94
-		$user = \OC::$server->getUserSession()->getUser();
95
-		Filesystem::initMountPoints($user->getUID());
96
-		if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
97
-			$rootView->mkdir('/' . $user->getUID() . '/uploads');
98
-		}
99
-		$view = new View('/' . $user->getUID() . '/uploads');
100
-		$rootInfo = $view->getFileInfo('');
101
-		return new Directory($view, $rootInfo);
102
-	}
37
+    /** @var array */
38
+    private $principalInfo;
39
+    /** @var CleanupService */
40
+    private $cleanupService;
41
+
42
+    public function __construct(array $principalInfo, CleanupService $cleanupService) {
43
+        $this->principalInfo = $principalInfo;
44
+        $this->cleanupService = $cleanupService;
45
+    }
46
+
47
+    public function createFile($name, $data = null) {
48
+        throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
49
+    }
50
+
51
+    public function createDirectory($name) {
52
+        $this->impl()->createDirectory($name);
53
+
54
+        // Add a cleanup job
55
+        $this->cleanupService->addJob($name);
56
+    }
57
+
58
+    public function getChild($name): UploadFolder {
59
+        return new UploadFolder($this->impl()->getChild($name), $this->cleanupService);
60
+    }
61
+
62
+    public function getChildren(): array {
63
+        return array_map(function ($node) {
64
+            return new UploadFolder($node, $this->cleanupService);
65
+        }, $this->impl()->getChildren());
66
+    }
67
+
68
+    public function childExists($name): bool {
69
+        return !is_null($this->getChild($name));
70
+    }
71
+
72
+    public function delete() {
73
+        $this->impl()->delete();
74
+    }
75
+
76
+    public function getName() {
77
+        [,$name] = \Sabre\Uri\split($this->principalInfo['uri']);
78
+        return $name;
79
+    }
80
+
81
+    public function setName($name) {
82
+        throw new Forbidden('Permission denied to rename this folder');
83
+    }
84
+
85
+    public function getLastModified() {
86
+        return $this->impl()->getLastModified();
87
+    }
88
+
89
+    /**
90
+     * @return Directory
91
+     */
92
+    private function impl() {
93
+        $rootView = new View();
94
+        $user = \OC::$server->getUserSession()->getUser();
95
+        Filesystem::initMountPoints($user->getUID());
96
+        if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
97
+            $rootView->mkdir('/' . $user->getUID() . '/uploads');
98
+        }
99
+        $view = new View('/' . $user->getUID() . '/uploads');
100
+        $rootInfo = $view->getFileInfo('');
101
+        return new Directory($view, $rootInfo);
102
+    }
103 103
 }
Please login to merge, or discard this patch.
apps/dav/lib/Traits/PrincipalProxyTrait.php 2 patches
Indentation   +186 added lines, -186 removed lines patch added patch discarded remove patch
@@ -36,190 +36,190 @@
 block discarded – undo
36 36
  */
37 37
 trait PrincipalProxyTrait {
38 38
 
39
-	/**
40
-	 * Returns the list of members for a group-principal
41
-	 *
42
-	 * @param string $principal
43
-	 * @return string[]
44
-	 * @throws Exception
45
-	 */
46
-	public function getGroupMemberSet($principal) {
47
-		$members = [];
48
-
49
-		if ($this->isProxyPrincipal($principal)) {
50
-			$realPrincipal = $this->getPrincipalUriFromProxyPrincipal($principal);
51
-			$principalArray = $this->getPrincipalByPath($realPrincipal);
52
-			if (!$principalArray) {
53
-				throw new Exception('Principal not found');
54
-			}
55
-
56
-			$proxies = $this->proxyMapper->getProxiesOf($principalArray['uri']);
57
-			foreach ($proxies as $proxy) {
58
-				if ($this->isReadProxyPrincipal($principal) && $proxy->getPermissions() === ProxyMapper::PERMISSION_READ) {
59
-					$members[] = $proxy->getProxyId();
60
-				}
61
-
62
-				if ($this->isWriteProxyPrincipal($principal) && $proxy->getPermissions() === (ProxyMapper::PERMISSION_READ | ProxyMapper::PERMISSION_WRITE)) {
63
-					$members[] = $proxy->getProxyId();
64
-				}
65
-			}
66
-		}
67
-
68
-		return $members;
69
-	}
70
-
71
-	/**
72
-	 * Returns the list of groups a principal is a member of
73
-	 *
74
-	 * @param string $principal
75
-	 * @param bool $needGroups
76
-	 * @return array
77
-	 * @throws Exception
78
-	 */
79
-	public function getGroupMembership($principal, $needGroups = false) {
80
-		[$prefix, $name] = \Sabre\Uri\split($principal);
81
-
82
-		if ($prefix !== $this->principalPrefix) {
83
-			return [];
84
-		}
85
-
86
-		$principalArray = $this->getPrincipalByPath($principal);
87
-		if (!$principalArray) {
88
-			throw new Exception('Principal not found');
89
-		}
90
-
91
-		$groups = [];
92
-		$proxies = $this->proxyMapper->getProxiesFor($principal);
93
-		foreach ($proxies as $proxy) {
94
-			if ($proxy->getPermissions() === ProxyMapper::PERMISSION_READ) {
95
-				$groups[] = $proxy->getOwnerId() . '/calendar-proxy-read';
96
-			}
97
-
98
-			if ($proxy->getPermissions() === (ProxyMapper::PERMISSION_READ | ProxyMapper::PERMISSION_WRITE)) {
99
-				$groups[] = $proxy->getOwnerId() . '/calendar-proxy-write';
100
-			}
101
-		}
102
-
103
-		return $groups;
104
-	}
105
-
106
-	/**
107
-	 * Updates the list of group members for a group principal.
108
-	 *
109
-	 * The principals should be passed as a list of uri's.
110
-	 *
111
-	 * @param string $principal
112
-	 * @param string[] $members
113
-	 * @throws Exception
114
-	 */
115
-	public function setGroupMemberSet($principal, array $members) {
116
-		[$principalUri, $target] = \Sabre\Uri\split($principal);
117
-
118
-		if ($target !== 'calendar-proxy-write' && $target !== 'calendar-proxy-read') {
119
-			throw new Exception('Setting members of the group is not supported yet');
120
-		}
121
-
122
-		$masterPrincipalArray = $this->getPrincipalByPath($principalUri);
123
-		if (!$masterPrincipalArray) {
124
-			throw new Exception('Principal not found');
125
-		}
126
-
127
-		$permission = ProxyMapper::PERMISSION_READ;
128
-		if ($target === 'calendar-proxy-write') {
129
-			$permission |= ProxyMapper::PERMISSION_WRITE;
130
-		}
131
-
132
-		[$prefix, $owner] = \Sabre\Uri\split($principalUri);
133
-		$proxies = $this->proxyMapper->getProxiesOf($principalUri);
134
-
135
-		foreach ($members as $member) {
136
-			[$prefix, $name] = \Sabre\Uri\split($member);
137
-
138
-			if ($prefix !== $this->principalPrefix) {
139
-				throw new Exception('Invalid member group prefix: ' . $prefix);
140
-			}
141
-
142
-			$principalArray = $this->getPrincipalByPath($member);
143
-			if (!$principalArray) {
144
-				throw new Exception('Principal not found');
145
-			}
146
-
147
-			$found = false;
148
-			foreach ($proxies as $proxy) {
149
-				if ($proxy->getProxyId() === $member) {
150
-					$found = true;
151
-					$proxy->setPermissions($proxy->getPermissions() | $permission);
152
-					$this->proxyMapper->update($proxy);
153
-
154
-					$proxies = array_filter($proxies, function (Proxy $p) use ($proxy) {
155
-						return $p->getId() !== $proxy->getId();
156
-					});
157
-					break;
158
-				}
159
-			}
160
-
161
-			if ($found === false) {
162
-				$proxy = new Proxy();
163
-				$proxy->setOwnerId($principalUri);
164
-				$proxy->setProxyId($member);
165
-				$proxy->setPermissions($permission);
166
-				$this->proxyMapper->insert($proxy);
167
-			}
168
-		}
169
-
170
-		// Delete all remaining proxies
171
-		foreach ($proxies as $proxy) {
172
-			// Write and Read Proxies have individual requests,
173
-			// so only delete proxies of this permission
174
-			if ($proxy->getPermissions() === $permission) {
175
-				$this->proxyMapper->delete($proxy);
176
-			}
177
-		}
178
-	}
179
-
180
-	/**
181
-	 * @param string $principalUri
182
-	 * @return bool
183
-	 */
184
-	private function isProxyPrincipal(string $principalUri):bool {
185
-		[$realPrincipalUri, $proxy] = \Sabre\Uri\split($principalUri);
186
-		[$prefix, $userId] = \Sabre\Uri\split($realPrincipalUri);
187
-
188
-		if (!isset($prefix) || !isset($userId)) {
189
-			return false;
190
-		}
191
-		if ($prefix !== $this->principalPrefix) {
192
-			return false;
193
-		}
194
-
195
-		return $proxy === 'calendar-proxy-read'
196
-			|| $proxy === 'calendar-proxy-write';
197
-	}
198
-
199
-	/**
200
-	 * @param string $principalUri
201
-	 * @return bool
202
-	 */
203
-	private function isReadProxyPrincipal(string $principalUri):bool {
204
-		[, $proxy] = \Sabre\Uri\split($principalUri);
205
-		return $proxy === 'calendar-proxy-read';
206
-	}
207
-
208
-	/**
209
-	 * @param string $principalUri
210
-	 * @return bool
211
-	 */
212
-	private function isWriteProxyPrincipal(string $principalUri):bool {
213
-		[, $proxy] = \Sabre\Uri\split($principalUri);
214
-		return $proxy === 'calendar-proxy-write';
215
-	}
216
-
217
-	/**
218
-	 * @param string $principalUri
219
-	 * @return string
220
-	 */
221
-	private function getPrincipalUriFromProxyPrincipal(string $principalUri):string {
222
-		[$realPrincipalUri, ] = \Sabre\Uri\split($principalUri);
223
-		return $realPrincipalUri;
224
-	}
39
+    /**
40
+     * Returns the list of members for a group-principal
41
+     *
42
+     * @param string $principal
43
+     * @return string[]
44
+     * @throws Exception
45
+     */
46
+    public function getGroupMemberSet($principal) {
47
+        $members = [];
48
+
49
+        if ($this->isProxyPrincipal($principal)) {
50
+            $realPrincipal = $this->getPrincipalUriFromProxyPrincipal($principal);
51
+            $principalArray = $this->getPrincipalByPath($realPrincipal);
52
+            if (!$principalArray) {
53
+                throw new Exception('Principal not found');
54
+            }
55
+
56
+            $proxies = $this->proxyMapper->getProxiesOf($principalArray['uri']);
57
+            foreach ($proxies as $proxy) {
58
+                if ($this->isReadProxyPrincipal($principal) && $proxy->getPermissions() === ProxyMapper::PERMISSION_READ) {
59
+                    $members[] = $proxy->getProxyId();
60
+                }
61
+
62
+                if ($this->isWriteProxyPrincipal($principal) && $proxy->getPermissions() === (ProxyMapper::PERMISSION_READ | ProxyMapper::PERMISSION_WRITE)) {
63
+                    $members[] = $proxy->getProxyId();
64
+                }
65
+            }
66
+        }
67
+
68
+        return $members;
69
+    }
70
+
71
+    /**
72
+     * Returns the list of groups a principal is a member of
73
+     *
74
+     * @param string $principal
75
+     * @param bool $needGroups
76
+     * @return array
77
+     * @throws Exception
78
+     */
79
+    public function getGroupMembership($principal, $needGroups = false) {
80
+        [$prefix, $name] = \Sabre\Uri\split($principal);
81
+
82
+        if ($prefix !== $this->principalPrefix) {
83
+            return [];
84
+        }
85
+
86
+        $principalArray = $this->getPrincipalByPath($principal);
87
+        if (!$principalArray) {
88
+            throw new Exception('Principal not found');
89
+        }
90
+
91
+        $groups = [];
92
+        $proxies = $this->proxyMapper->getProxiesFor($principal);
93
+        foreach ($proxies as $proxy) {
94
+            if ($proxy->getPermissions() === ProxyMapper::PERMISSION_READ) {
95
+                $groups[] = $proxy->getOwnerId() . '/calendar-proxy-read';
96
+            }
97
+
98
+            if ($proxy->getPermissions() === (ProxyMapper::PERMISSION_READ | ProxyMapper::PERMISSION_WRITE)) {
99
+                $groups[] = $proxy->getOwnerId() . '/calendar-proxy-write';
100
+            }
101
+        }
102
+
103
+        return $groups;
104
+    }
105
+
106
+    /**
107
+     * Updates the list of group members for a group principal.
108
+     *
109
+     * The principals should be passed as a list of uri's.
110
+     *
111
+     * @param string $principal
112
+     * @param string[] $members
113
+     * @throws Exception
114
+     */
115
+    public function setGroupMemberSet($principal, array $members) {
116
+        [$principalUri, $target] = \Sabre\Uri\split($principal);
117
+
118
+        if ($target !== 'calendar-proxy-write' && $target !== 'calendar-proxy-read') {
119
+            throw new Exception('Setting members of the group is not supported yet');
120
+        }
121
+
122
+        $masterPrincipalArray = $this->getPrincipalByPath($principalUri);
123
+        if (!$masterPrincipalArray) {
124
+            throw new Exception('Principal not found');
125
+        }
126
+
127
+        $permission = ProxyMapper::PERMISSION_READ;
128
+        if ($target === 'calendar-proxy-write') {
129
+            $permission |= ProxyMapper::PERMISSION_WRITE;
130
+        }
131
+
132
+        [$prefix, $owner] = \Sabre\Uri\split($principalUri);
133
+        $proxies = $this->proxyMapper->getProxiesOf($principalUri);
134
+
135
+        foreach ($members as $member) {
136
+            [$prefix, $name] = \Sabre\Uri\split($member);
137
+
138
+            if ($prefix !== $this->principalPrefix) {
139
+                throw new Exception('Invalid member group prefix: ' . $prefix);
140
+            }
141
+
142
+            $principalArray = $this->getPrincipalByPath($member);
143
+            if (!$principalArray) {
144
+                throw new Exception('Principal not found');
145
+            }
146
+
147
+            $found = false;
148
+            foreach ($proxies as $proxy) {
149
+                if ($proxy->getProxyId() === $member) {
150
+                    $found = true;
151
+                    $proxy->setPermissions($proxy->getPermissions() | $permission);
152
+                    $this->proxyMapper->update($proxy);
153
+
154
+                    $proxies = array_filter($proxies, function (Proxy $p) use ($proxy) {
155
+                        return $p->getId() !== $proxy->getId();
156
+                    });
157
+                    break;
158
+                }
159
+            }
160
+
161
+            if ($found === false) {
162
+                $proxy = new Proxy();
163
+                $proxy->setOwnerId($principalUri);
164
+                $proxy->setProxyId($member);
165
+                $proxy->setPermissions($permission);
166
+                $this->proxyMapper->insert($proxy);
167
+            }
168
+        }
169
+
170
+        // Delete all remaining proxies
171
+        foreach ($proxies as $proxy) {
172
+            // Write and Read Proxies have individual requests,
173
+            // so only delete proxies of this permission
174
+            if ($proxy->getPermissions() === $permission) {
175
+                $this->proxyMapper->delete($proxy);
176
+            }
177
+        }
178
+    }
179
+
180
+    /**
181
+     * @param string $principalUri
182
+     * @return bool
183
+     */
184
+    private function isProxyPrincipal(string $principalUri):bool {
185
+        [$realPrincipalUri, $proxy] = \Sabre\Uri\split($principalUri);
186
+        [$prefix, $userId] = \Sabre\Uri\split($realPrincipalUri);
187
+
188
+        if (!isset($prefix) || !isset($userId)) {
189
+            return false;
190
+        }
191
+        if ($prefix !== $this->principalPrefix) {
192
+            return false;
193
+        }
194
+
195
+        return $proxy === 'calendar-proxy-read'
196
+            || $proxy === 'calendar-proxy-write';
197
+    }
198
+
199
+    /**
200
+     * @param string $principalUri
201
+     * @return bool
202
+     */
203
+    private function isReadProxyPrincipal(string $principalUri):bool {
204
+        [, $proxy] = \Sabre\Uri\split($principalUri);
205
+        return $proxy === 'calendar-proxy-read';
206
+    }
207
+
208
+    /**
209
+     * @param string $principalUri
210
+     * @return bool
211
+     */
212
+    private function isWriteProxyPrincipal(string $principalUri):bool {
213
+        [, $proxy] = \Sabre\Uri\split($principalUri);
214
+        return $proxy === 'calendar-proxy-write';
215
+    }
216
+
217
+    /**
218
+     * @param string $principalUri
219
+     * @return string
220
+     */
221
+    private function getPrincipalUriFromProxyPrincipal(string $principalUri):string {
222
+        [$realPrincipalUri, ] = \Sabre\Uri\split($principalUri);
223
+        return $realPrincipalUri;
224
+    }
225 225
 }
Please login to merge, or discard this patch.
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -92,11 +92,11 @@  discard block
 block discarded – undo
92 92
 		$proxies = $this->proxyMapper->getProxiesFor($principal);
93 93
 		foreach ($proxies as $proxy) {
94 94
 			if ($proxy->getPermissions() === ProxyMapper::PERMISSION_READ) {
95
-				$groups[] = $proxy->getOwnerId() . '/calendar-proxy-read';
95
+				$groups[] = $proxy->getOwnerId().'/calendar-proxy-read';
96 96
 			}
97 97
 
98 98
 			if ($proxy->getPermissions() === (ProxyMapper::PERMISSION_READ | ProxyMapper::PERMISSION_WRITE)) {
99
-				$groups[] = $proxy->getOwnerId() . '/calendar-proxy-write';
99
+				$groups[] = $proxy->getOwnerId().'/calendar-proxy-write';
100 100
 			}
101 101
 		}
102 102
 
@@ -136,7 +136,7 @@  discard block
 block discarded – undo
136 136
 			[$prefix, $name] = \Sabre\Uri\split($member);
137 137
 
138 138
 			if ($prefix !== $this->principalPrefix) {
139
-				throw new Exception('Invalid member group prefix: ' . $prefix);
139
+				throw new Exception('Invalid member group prefix: '.$prefix);
140 140
 			}
141 141
 
142 142
 			$principalArray = $this->getPrincipalByPath($member);
@@ -151,7 +151,7 @@  discard block
 block discarded – undo
151 151
 					$proxy->setPermissions($proxy->getPermissions() | $permission);
152 152
 					$this->proxyMapper->update($proxy);
153 153
 
154
-					$proxies = array_filter($proxies, function (Proxy $p) use ($proxy) {
154
+					$proxies = array_filter($proxies, function(Proxy $p) use ($proxy) {
155 155
 						return $p->getId() !== $proxy->getId();
156 156
 					});
157 157
 					break;
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/FilesPlugin.php 2 patches
Indentation   +462 added lines, -462 removed lines patch added patch discarded remove patch
@@ -53,466 +53,466 @@
 block discarded – undo
53 53
 
54 54
 class FilesPlugin extends ServerPlugin {
55 55
 
56
-	// namespace
57
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
58
-	public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
59
-	public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
60
-	public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
61
-	public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
62
-	public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
63
-	public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
64
-	public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
65
-	public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
66
-	public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
67
-	public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
68
-	public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
69
-	public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
70
-	public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
71
-	public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
72
-	public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
73
-	public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
74
-	public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
75
-	public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
76
-	public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
77
-	public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
78
-	public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
79
-
80
-	/**
81
-	 * Reference to main server object
82
-	 *
83
-	 * @var \Sabre\DAV\Server
84
-	 */
85
-	private $server;
86
-
87
-	/**
88
-	 * @var Tree
89
-	 */
90
-	private $tree;
91
-
92
-	/**
93
-	 * Whether this is public webdav.
94
-	 * If true, some returned information will be stripped off.
95
-	 *
96
-	 * @var bool
97
-	 */
98
-	private $isPublic;
99
-
100
-	/**
101
-	 * @var bool
102
-	 */
103
-	private $downloadAttachment;
104
-
105
-	/**
106
-	 * @var IConfig
107
-	 */
108
-	private $config;
109
-
110
-	/**
111
-	 * @var IRequest
112
-	 */
113
-	private $request;
114
-
115
-	/**
116
-	 * @var IPreview
117
-	 */
118
-	private $previewManager;
119
-
120
-	/**
121
-	 * @param Tree $tree
122
-	 * @param IConfig $config
123
-	 * @param IRequest $request
124
-	 * @param IPreview $previewManager
125
-	 * @param bool $isPublic
126
-	 * @param bool $downloadAttachment
127
-	 */
128
-	public function __construct(Tree $tree,
129
-								IConfig $config,
130
-								IRequest $request,
131
-								IPreview $previewManager,
132
-								$isPublic = false,
133
-								$downloadAttachment = true) {
134
-		$this->tree = $tree;
135
-		$this->config = $config;
136
-		$this->request = $request;
137
-		$this->isPublic = $isPublic;
138
-		$this->downloadAttachment = $downloadAttachment;
139
-		$this->previewManager = $previewManager;
140
-	}
141
-
142
-	/**
143
-	 * This initializes the plugin.
144
-	 *
145
-	 * This function is called by \Sabre\DAV\Server, after
146
-	 * addPlugin is called.
147
-	 *
148
-	 * This method should set up the required event subscriptions.
149
-	 *
150
-	 * @param \Sabre\DAV\Server $server
151
-	 * @return void
152
-	 */
153
-	public function initialize(\Sabre\DAV\Server $server) {
154
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
155
-		$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
156
-		$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
157
-		$server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
158
-		$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
159
-		$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
160
-		$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
161
-		$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
162
-		$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
163
-		$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
164
-		$server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
165
-		$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
166
-		$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
167
-		$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
168
-		$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
169
-		$server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME;
170
-		$server->protectedProperties[] = self::SHARE_NOTE;
171
-
172
-		// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
173
-		$allowedProperties = ['{DAV:}getetag'];
174
-		$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
175
-
176
-		$this->server = $server;
177
-		$this->server->on('propFind', [$this, 'handleGetProperties']);
178
-		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
179
-		$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
180
-		$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
181
-		$this->server->on('afterMethod:GET', [$this,'httpGet']);
182
-		$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
183
-		$this->server->on('afterResponse', function ($request, ResponseInterface $response) {
184
-			$body = $response->getBody();
185
-			if (is_resource($body)) {
186
-				fclose($body);
187
-			}
188
-		});
189
-		$this->server->on('beforeMove', [$this, 'checkMove']);
190
-	}
191
-
192
-	/**
193
-	 * Plugin that checks if a move can actually be performed.
194
-	 *
195
-	 * @param string $source source path
196
-	 * @param string $destination destination path
197
-	 * @throws Forbidden
198
-	 * @throws NotFound
199
-	 */
200
-	public function checkMove($source, $destination) {
201
-		$sourceNode = $this->tree->getNodeForPath($source);
202
-		if (!$sourceNode instanceof Node) {
203
-			return;
204
-		}
205
-		[$sourceDir,] = \Sabre\Uri\split($source);
206
-		[$destinationDir,] = \Sabre\Uri\split($destination);
207
-
208
-		if ($sourceDir !== $destinationDir) {
209
-			$sourceNodeFileInfo = $sourceNode->getFileInfo();
210
-			if ($sourceNodeFileInfo === null) {
211
-				throw new NotFound($source . ' does not exist');
212
-			}
213
-
214
-			if (!$sourceNodeFileInfo->isDeletable()) {
215
-				throw new Forbidden($source . " cannot be deleted");
216
-			}
217
-		}
218
-	}
219
-
220
-	/**
221
-	 * This sets a cookie to be able to recognize the start of the download
222
-	 * the content must not be longer than 32 characters and must only contain
223
-	 * alphanumeric characters
224
-	 *
225
-	 * @param RequestInterface $request
226
-	 * @param ResponseInterface $response
227
-	 */
228
-	public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
229
-		$queryParams = $request->getQueryParameters();
230
-
231
-		/**
232
-		 * this sets a cookie to be able to recognize the start of the download
233
-		 * the content must not be longer than 32 characters and must only contain
234
-		 * alphanumeric characters
235
-		 */
236
-		if (isset($queryParams['downloadStartSecret'])) {
237
-			$token = $queryParams['downloadStartSecret'];
238
-			if (!isset($token[32])
239
-				&& preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
240
-				// FIXME: use $response->setHeader() instead
241
-				setcookie('ocDownloadStarted', $token, time() + 20, '/');
242
-			}
243
-		}
244
-	}
245
-
246
-	/**
247
-	 * Add headers to file download
248
-	 *
249
-	 * @param RequestInterface $request
250
-	 * @param ResponseInterface $response
251
-	 */
252
-	public function httpGet(RequestInterface $request, ResponseInterface $response) {
253
-		// Only handle valid files
254
-		$node = $this->tree->getNodeForPath($request->getPath());
255
-		if (!($node instanceof IFile)) {
256
-			return;
257
-		}
258
-
259
-		// adds a 'Content-Disposition: attachment' header in case no disposition
260
-		// header has been set before
261
-		if ($this->downloadAttachment &&
262
-			$response->getHeader('Content-Disposition') === null) {
263
-			$filename = $node->getName();
264
-			if ($this->request->isUserAgent(
265
-				[
266
-					Request::USER_AGENT_IE,
267
-					Request::USER_AGENT_ANDROID_MOBILE_CHROME,
268
-					Request::USER_AGENT_FREEBOX,
269
-				])) {
270
-				$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
271
-			} else {
272
-				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
273
-													 . '; filename="' . rawurlencode($filename) . '"');
274
-			}
275
-		}
276
-
277
-		if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
278
-			//Add OC-Checksum header
279
-			$checksum = $node->getChecksum();
280
-			if ($checksum !== null && $checksum !== '') {
281
-				$response->addHeader('OC-Checksum', $checksum);
282
-			}
283
-		}
284
-	}
285
-
286
-	/**
287
-	 * Adds all ownCloud-specific properties
288
-	 *
289
-	 * @param PropFind $propFind
290
-	 * @param \Sabre\DAV\INode $node
291
-	 * @return void
292
-	 */
293
-	public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
294
-		$httpRequest = $this->server->httpRequest;
295
-
296
-		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
297
-			/**
298
-			 * This was disabled, because it made dir listing throw an exception,
299
-			 * so users were unable to navigate into folders where one subitem
300
-			 * is blocked by the files_accesscontrol app, see:
301
-			 * https://github.com/nextcloud/files_accesscontrol/issues/65
302
-			 * if (!$node->getFileInfo()->isReadable()) {
303
-			 *     // avoid detecting files through this means
304
-			 *     throw new NotFound();
305
-			 * }
306
-			 */
307
-
308
-			$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
309
-				return $node->getFileId();
310
-			});
311
-
312
-			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
313
-				return $node->getInternalFileId();
314
-			});
315
-
316
-			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
317
-				$perms = $node->getDavPermissions();
318
-				if ($this->isPublic) {
319
-					// remove mount information
320
-					$perms = str_replace(['S', 'M'], '', $perms);
321
-				}
322
-				return $perms;
323
-			});
324
-
325
-			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
326
-				return $node->getSharePermissions(
327
-					$httpRequest->getRawServerValue('PHP_AUTH_USER')
328
-				);
329
-			});
330
-
331
-			$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
332
-				$ncPermissions = $node->getSharePermissions(
333
-					$httpRequest->getRawServerValue('PHP_AUTH_USER')
334
-				);
335
-				$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
336
-				return json_encode($ocmPermissions);
337
-			});
338
-
339
-			$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node) {
340
-				return $node->getETag();
341
-			});
342
-
343
-			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node) {
344
-				$owner = $node->getOwner();
345
-				if (!$owner) {
346
-					return null;
347
-				} else {
348
-					return $owner->getUID();
349
-				}
350
-			});
351
-			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node) {
352
-				$owner = $node->getOwner();
353
-				if (!$owner) {
354
-					return null;
355
-				} else {
356
-					return $owner->getDisplayName();
357
-				}
358
-			});
359
-
360
-			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
361
-				return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
362
-			});
363
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
364
-				return $node->getSize();
365
-			});
366
-			$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
367
-				return $node->getFileInfo()->getMountPoint()->getMountType();
368
-			});
369
-
370
-			$propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest) {
371
-				return $node->getNoteFromShare(
372
-					$httpRequest->getRawServerValue('PHP_AUTH_USER')
373
-				);
374
-			});
375
-		}
376
-
377
-		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
378
-			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
379
-				return $this->config->getSystemValue('data-fingerprint', '');
380
-			});
381
-		}
382
-
383
-		if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
384
-			$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
385
-				try {
386
-					$directDownloadUrl = $node->getDirectDownload();
387
-					if (isset($directDownloadUrl['url'])) {
388
-						return $directDownloadUrl['url'];
389
-					}
390
-				} catch (StorageNotAvailableException $e) {
391
-					return false;
392
-				} catch (ForbiddenException $e) {
393
-					return false;
394
-				}
395
-				return false;
396
-			});
397
-
398
-			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
399
-				$checksum = $node->getChecksum();
400
-				if ($checksum === null || $checksum === '') {
401
-					return null;
402
-				}
403
-
404
-				return new ChecksumList($checksum);
405
-			});
406
-
407
-			$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
408
-				return $node->getFileInfo()->getCreationTime();
409
-			});
410
-
411
-			$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
412
-				return $node->getFileInfo()->getUploadTime();
413
-			});
414
-		}
415
-
416
-		if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) {
417
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
418
-				return $node->getSize();
419
-			});
420
-
421
-			$propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
422
-				return $node->getFileInfo()->isEncrypted() ? '1' : '0';
423
-			});
424
-		}
425
-	}
426
-
427
-	/**
428
-	 * translate Nextcloud permissions to OCM Permissions
429
-	 *
430
-	 * @param $ncPermissions
431
-	 * @return array
432
-	 */
433
-	protected function ncPermissions2ocmPermissions($ncPermissions) {
434
-		$ocmPermissions = [];
435
-
436
-		if ($ncPermissions & Constants::PERMISSION_SHARE) {
437
-			$ocmPermissions[] = 'share';
438
-		}
439
-
440
-		if ($ncPermissions & Constants::PERMISSION_READ) {
441
-			$ocmPermissions[] = 'read';
442
-		}
443
-
444
-		if (($ncPermissions & Constants::PERMISSION_CREATE) ||
445
-			($ncPermissions & Constants::PERMISSION_UPDATE)) {
446
-			$ocmPermissions[] = 'write';
447
-		}
448
-
449
-		return $ocmPermissions;
450
-	}
451
-
452
-	/**
453
-	 * Update ownCloud-specific properties
454
-	 *
455
-	 * @param string $path
456
-	 * @param PropPatch $propPatch
457
-	 *
458
-	 * @return void
459
-	 */
460
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
461
-		$node = $this->tree->getNodeForPath($path);
462
-		if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
463
-			return;
464
-		}
465
-
466
-		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
467
-			if (empty($time)) {
468
-				return false;
469
-			}
470
-			$node->touch($time);
471
-			return true;
472
-		});
473
-		$propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
474
-			if (empty($etag)) {
475
-				return false;
476
-			}
477
-			if ($node->setEtag($etag) !== -1) {
478
-				return true;
479
-			}
480
-			return false;
481
-		});
482
-		$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
483
-			if (empty($time)) {
484
-				return false;
485
-			}
486
-			$node->setCreationTime((int) $time);
487
-			return true;
488
-		});
489
-	}
490
-
491
-	/**
492
-	 * @param string $filePath
493
-	 * @param \Sabre\DAV\INode $node
494
-	 * @throws \Sabre\DAV\Exception\BadRequest
495
-	 */
496
-	public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
497
-		// chunked upload handling
498
-		if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
499
-			[$path, $name] = \Sabre\Uri\split($filePath);
500
-			$info = \OC_FileChunking::decodeName($name);
501
-			if (!empty($info)) {
502
-				$filePath = $path . '/' . $info['name'];
503
-			}
504
-		}
505
-
506
-		// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
507
-		if (!$this->server->tree->nodeExists($filePath)) {
508
-			return;
509
-		}
510
-		$node = $this->server->tree->getNodeForPath($filePath);
511
-		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
512
-			$fileId = $node->getFileId();
513
-			if (!is_null($fileId)) {
514
-				$this->server->httpResponse->setHeader('OC-FileId', $fileId);
515
-			}
516
-		}
517
-	}
56
+    // namespace
57
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
58
+    public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
59
+    public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
60
+    public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
61
+    public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
62
+    public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
63
+    public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
64
+    public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
65
+    public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
66
+    public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
67
+    public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
68
+    public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
69
+    public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
70
+    public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
71
+    public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
72
+    public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
73
+    public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
74
+    public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
75
+    public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
76
+    public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
77
+    public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
78
+    public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
79
+
80
+    /**
81
+     * Reference to main server object
82
+     *
83
+     * @var \Sabre\DAV\Server
84
+     */
85
+    private $server;
86
+
87
+    /**
88
+     * @var Tree
89
+     */
90
+    private $tree;
91
+
92
+    /**
93
+     * Whether this is public webdav.
94
+     * If true, some returned information will be stripped off.
95
+     *
96
+     * @var bool
97
+     */
98
+    private $isPublic;
99
+
100
+    /**
101
+     * @var bool
102
+     */
103
+    private $downloadAttachment;
104
+
105
+    /**
106
+     * @var IConfig
107
+     */
108
+    private $config;
109
+
110
+    /**
111
+     * @var IRequest
112
+     */
113
+    private $request;
114
+
115
+    /**
116
+     * @var IPreview
117
+     */
118
+    private $previewManager;
119
+
120
+    /**
121
+     * @param Tree $tree
122
+     * @param IConfig $config
123
+     * @param IRequest $request
124
+     * @param IPreview $previewManager
125
+     * @param bool $isPublic
126
+     * @param bool $downloadAttachment
127
+     */
128
+    public function __construct(Tree $tree,
129
+                                IConfig $config,
130
+                                IRequest $request,
131
+                                IPreview $previewManager,
132
+                                $isPublic = false,
133
+                                $downloadAttachment = true) {
134
+        $this->tree = $tree;
135
+        $this->config = $config;
136
+        $this->request = $request;
137
+        $this->isPublic = $isPublic;
138
+        $this->downloadAttachment = $downloadAttachment;
139
+        $this->previewManager = $previewManager;
140
+    }
141
+
142
+    /**
143
+     * This initializes the plugin.
144
+     *
145
+     * This function is called by \Sabre\DAV\Server, after
146
+     * addPlugin is called.
147
+     *
148
+     * This method should set up the required event subscriptions.
149
+     *
150
+     * @param \Sabre\DAV\Server $server
151
+     * @return void
152
+     */
153
+    public function initialize(\Sabre\DAV\Server $server) {
154
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
155
+        $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
156
+        $server->protectedProperties[] = self::FILEID_PROPERTYNAME;
157
+        $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
158
+        $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
159
+        $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
160
+        $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
161
+        $server->protectedProperties[] = self::SIZE_PROPERTYNAME;
162
+        $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
163
+        $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
164
+        $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
165
+        $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
166
+        $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
167
+        $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
168
+        $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
169
+        $server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME;
170
+        $server->protectedProperties[] = self::SHARE_NOTE;
171
+
172
+        // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
173
+        $allowedProperties = ['{DAV:}getetag'];
174
+        $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
175
+
176
+        $this->server = $server;
177
+        $this->server->on('propFind', [$this, 'handleGetProperties']);
178
+        $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
179
+        $this->server->on('afterBind', [$this, 'sendFileIdHeader']);
180
+        $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
181
+        $this->server->on('afterMethod:GET', [$this,'httpGet']);
182
+        $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
183
+        $this->server->on('afterResponse', function ($request, ResponseInterface $response) {
184
+            $body = $response->getBody();
185
+            if (is_resource($body)) {
186
+                fclose($body);
187
+            }
188
+        });
189
+        $this->server->on('beforeMove', [$this, 'checkMove']);
190
+    }
191
+
192
+    /**
193
+     * Plugin that checks if a move can actually be performed.
194
+     *
195
+     * @param string $source source path
196
+     * @param string $destination destination path
197
+     * @throws Forbidden
198
+     * @throws NotFound
199
+     */
200
+    public function checkMove($source, $destination) {
201
+        $sourceNode = $this->tree->getNodeForPath($source);
202
+        if (!$sourceNode instanceof Node) {
203
+            return;
204
+        }
205
+        [$sourceDir,] = \Sabre\Uri\split($source);
206
+        [$destinationDir,] = \Sabre\Uri\split($destination);
207
+
208
+        if ($sourceDir !== $destinationDir) {
209
+            $sourceNodeFileInfo = $sourceNode->getFileInfo();
210
+            if ($sourceNodeFileInfo === null) {
211
+                throw new NotFound($source . ' does not exist');
212
+            }
213
+
214
+            if (!$sourceNodeFileInfo->isDeletable()) {
215
+                throw new Forbidden($source . " cannot be deleted");
216
+            }
217
+        }
218
+    }
219
+
220
+    /**
221
+     * This sets a cookie to be able to recognize the start of the download
222
+     * the content must not be longer than 32 characters and must only contain
223
+     * alphanumeric characters
224
+     *
225
+     * @param RequestInterface $request
226
+     * @param ResponseInterface $response
227
+     */
228
+    public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
229
+        $queryParams = $request->getQueryParameters();
230
+
231
+        /**
232
+         * this sets a cookie to be able to recognize the start of the download
233
+         * the content must not be longer than 32 characters and must only contain
234
+         * alphanumeric characters
235
+         */
236
+        if (isset($queryParams['downloadStartSecret'])) {
237
+            $token = $queryParams['downloadStartSecret'];
238
+            if (!isset($token[32])
239
+                && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
240
+                // FIXME: use $response->setHeader() instead
241
+                setcookie('ocDownloadStarted', $token, time() + 20, '/');
242
+            }
243
+        }
244
+    }
245
+
246
+    /**
247
+     * Add headers to file download
248
+     *
249
+     * @param RequestInterface $request
250
+     * @param ResponseInterface $response
251
+     */
252
+    public function httpGet(RequestInterface $request, ResponseInterface $response) {
253
+        // Only handle valid files
254
+        $node = $this->tree->getNodeForPath($request->getPath());
255
+        if (!($node instanceof IFile)) {
256
+            return;
257
+        }
258
+
259
+        // adds a 'Content-Disposition: attachment' header in case no disposition
260
+        // header has been set before
261
+        if ($this->downloadAttachment &&
262
+            $response->getHeader('Content-Disposition') === null) {
263
+            $filename = $node->getName();
264
+            if ($this->request->isUserAgent(
265
+                [
266
+                    Request::USER_AGENT_IE,
267
+                    Request::USER_AGENT_ANDROID_MOBILE_CHROME,
268
+                    Request::USER_AGENT_FREEBOX,
269
+                ])) {
270
+                $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
271
+            } else {
272
+                $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
273
+                                                        . '; filename="' . rawurlencode($filename) . '"');
274
+            }
275
+        }
276
+
277
+        if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
278
+            //Add OC-Checksum header
279
+            $checksum = $node->getChecksum();
280
+            if ($checksum !== null && $checksum !== '') {
281
+                $response->addHeader('OC-Checksum', $checksum);
282
+            }
283
+        }
284
+    }
285
+
286
+    /**
287
+     * Adds all ownCloud-specific properties
288
+     *
289
+     * @param PropFind $propFind
290
+     * @param \Sabre\DAV\INode $node
291
+     * @return void
292
+     */
293
+    public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
294
+        $httpRequest = $this->server->httpRequest;
295
+
296
+        if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
297
+            /**
298
+             * This was disabled, because it made dir listing throw an exception,
299
+             * so users were unable to navigate into folders where one subitem
300
+             * is blocked by the files_accesscontrol app, see:
301
+             * https://github.com/nextcloud/files_accesscontrol/issues/65
302
+             * if (!$node->getFileInfo()->isReadable()) {
303
+             *     // avoid detecting files through this means
304
+             *     throw new NotFound();
305
+             * }
306
+             */
307
+
308
+            $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
309
+                return $node->getFileId();
310
+            });
311
+
312
+            $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
313
+                return $node->getInternalFileId();
314
+            });
315
+
316
+            $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
317
+                $perms = $node->getDavPermissions();
318
+                if ($this->isPublic) {
319
+                    // remove mount information
320
+                    $perms = str_replace(['S', 'M'], '', $perms);
321
+                }
322
+                return $perms;
323
+            });
324
+
325
+            $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
326
+                return $node->getSharePermissions(
327
+                    $httpRequest->getRawServerValue('PHP_AUTH_USER')
328
+                );
329
+            });
330
+
331
+            $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
332
+                $ncPermissions = $node->getSharePermissions(
333
+                    $httpRequest->getRawServerValue('PHP_AUTH_USER')
334
+                );
335
+                $ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
336
+                return json_encode($ocmPermissions);
337
+            });
338
+
339
+            $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node) {
340
+                return $node->getETag();
341
+            });
342
+
343
+            $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node) {
344
+                $owner = $node->getOwner();
345
+                if (!$owner) {
346
+                    return null;
347
+                } else {
348
+                    return $owner->getUID();
349
+                }
350
+            });
351
+            $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node) {
352
+                $owner = $node->getOwner();
353
+                if (!$owner) {
354
+                    return null;
355
+                } else {
356
+                    return $owner->getDisplayName();
357
+                }
358
+            });
359
+
360
+            $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
361
+                return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
362
+            });
363
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
364
+                return $node->getSize();
365
+            });
366
+            $propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
367
+                return $node->getFileInfo()->getMountPoint()->getMountType();
368
+            });
369
+
370
+            $propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest) {
371
+                return $node->getNoteFromShare(
372
+                    $httpRequest->getRawServerValue('PHP_AUTH_USER')
373
+                );
374
+            });
375
+        }
376
+
377
+        if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
378
+            $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
379
+                return $this->config->getSystemValue('data-fingerprint', '');
380
+            });
381
+        }
382
+
383
+        if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
384
+            $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
385
+                try {
386
+                    $directDownloadUrl = $node->getDirectDownload();
387
+                    if (isset($directDownloadUrl['url'])) {
388
+                        return $directDownloadUrl['url'];
389
+                    }
390
+                } catch (StorageNotAvailableException $e) {
391
+                    return false;
392
+                } catch (ForbiddenException $e) {
393
+                    return false;
394
+                }
395
+                return false;
396
+            });
397
+
398
+            $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
399
+                $checksum = $node->getChecksum();
400
+                if ($checksum === null || $checksum === '') {
401
+                    return null;
402
+                }
403
+
404
+                return new ChecksumList($checksum);
405
+            });
406
+
407
+            $propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
408
+                return $node->getFileInfo()->getCreationTime();
409
+            });
410
+
411
+            $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
412
+                return $node->getFileInfo()->getUploadTime();
413
+            });
414
+        }
415
+
416
+        if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) {
417
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
418
+                return $node->getSize();
419
+            });
420
+
421
+            $propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
422
+                return $node->getFileInfo()->isEncrypted() ? '1' : '0';
423
+            });
424
+        }
425
+    }
426
+
427
+    /**
428
+     * translate Nextcloud permissions to OCM Permissions
429
+     *
430
+     * @param $ncPermissions
431
+     * @return array
432
+     */
433
+    protected function ncPermissions2ocmPermissions($ncPermissions) {
434
+        $ocmPermissions = [];
435
+
436
+        if ($ncPermissions & Constants::PERMISSION_SHARE) {
437
+            $ocmPermissions[] = 'share';
438
+        }
439
+
440
+        if ($ncPermissions & Constants::PERMISSION_READ) {
441
+            $ocmPermissions[] = 'read';
442
+        }
443
+
444
+        if (($ncPermissions & Constants::PERMISSION_CREATE) ||
445
+            ($ncPermissions & Constants::PERMISSION_UPDATE)) {
446
+            $ocmPermissions[] = 'write';
447
+        }
448
+
449
+        return $ocmPermissions;
450
+    }
451
+
452
+    /**
453
+     * Update ownCloud-specific properties
454
+     *
455
+     * @param string $path
456
+     * @param PropPatch $propPatch
457
+     *
458
+     * @return void
459
+     */
460
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
461
+        $node = $this->tree->getNodeForPath($path);
462
+        if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
463
+            return;
464
+        }
465
+
466
+        $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
467
+            if (empty($time)) {
468
+                return false;
469
+            }
470
+            $node->touch($time);
471
+            return true;
472
+        });
473
+        $propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
474
+            if (empty($etag)) {
475
+                return false;
476
+            }
477
+            if ($node->setEtag($etag) !== -1) {
478
+                return true;
479
+            }
480
+            return false;
481
+        });
482
+        $propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
483
+            if (empty($time)) {
484
+                return false;
485
+            }
486
+            $node->setCreationTime((int) $time);
487
+            return true;
488
+        });
489
+    }
490
+
491
+    /**
492
+     * @param string $filePath
493
+     * @param \Sabre\DAV\INode $node
494
+     * @throws \Sabre\DAV\Exception\BadRequest
495
+     */
496
+    public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
497
+        // chunked upload handling
498
+        if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
499
+            [$path, $name] = \Sabre\Uri\split($filePath);
500
+            $info = \OC_FileChunking::decodeName($name);
501
+            if (!empty($info)) {
502
+                $filePath = $path . '/' . $info['name'];
503
+            }
504
+        }
505
+
506
+        // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
507
+        if (!$this->server->tree->nodeExists($filePath)) {
508
+            return;
509
+        }
510
+        $node = $this->server->tree->getNodeForPath($filePath);
511
+        if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
512
+            $fileId = $node->getFileId();
513
+            if (!is_null($fileId)) {
514
+                $this->server->httpResponse->setHeader('OC-FileId', $fileId);
515
+            }
516
+        }
517
+    }
518 518
 }
Please login to merge, or discard this patch.
Spacing   +32 added lines, -32 removed lines patch added patch discarded remove patch
@@ -178,9 +178,9 @@  discard block
 block discarded – undo
178 178
 		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
179 179
 		$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
180 180
 		$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
181
-		$this->server->on('afterMethod:GET', [$this,'httpGet']);
181
+		$this->server->on('afterMethod:GET', [$this, 'httpGet']);
182 182
 		$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
183
-		$this->server->on('afterResponse', function ($request, ResponseInterface $response) {
183
+		$this->server->on('afterResponse', function($request, ResponseInterface $response) {
184 184
 			$body = $response->getBody();
185 185
 			if (is_resource($body)) {
186 186
 				fclose($body);
@@ -202,17 +202,17 @@  discard block
 block discarded – undo
202 202
 		if (!$sourceNode instanceof Node) {
203 203
 			return;
204 204
 		}
205
-		[$sourceDir,] = \Sabre\Uri\split($source);
206
-		[$destinationDir,] = \Sabre\Uri\split($destination);
205
+		[$sourceDir, ] = \Sabre\Uri\split($source);
206
+		[$destinationDir, ] = \Sabre\Uri\split($destination);
207 207
 
208 208
 		if ($sourceDir !== $destinationDir) {
209 209
 			$sourceNodeFileInfo = $sourceNode->getFileInfo();
210 210
 			if ($sourceNodeFileInfo === null) {
211
-				throw new NotFound($source . ' does not exist');
211
+				throw new NotFound($source.' does not exist');
212 212
 			}
213 213
 
214 214
 			if (!$sourceNodeFileInfo->isDeletable()) {
215
-				throw new Forbidden($source . " cannot be deleted");
215
+				throw new Forbidden($source." cannot be deleted");
216 216
 			}
217 217
 		}
218 218
 	}
@@ -267,10 +267,10 @@  discard block
 block discarded – undo
267 267
 					Request::USER_AGENT_ANDROID_MOBILE_CHROME,
268 268
 					Request::USER_AGENT_FREEBOX,
269 269
 				])) {
270
-				$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
270
+				$response->addHeader('Content-Disposition', 'attachment; filename="'.rawurlencode($filename).'"');
271 271
 			} else {
272
-				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
273
-													 . '; filename="' . rawurlencode($filename) . '"');
272
+				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\''.rawurlencode($filename)
273
+													 . '; filename="'.rawurlencode($filename).'"');
274 274
 			}
275 275
 		}
276 276
 
@@ -305,15 +305,15 @@  discard block
 block discarded – undo
305 305
 			 * }
306 306
 			 */
307 307
 
308
-			$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
308
+			$propFind->handle(self::FILEID_PROPERTYNAME, function() use ($node) {
309 309
 				return $node->getFileId();
310 310
 			});
311 311
 
312
-			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
312
+			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) {
313 313
 				return $node->getInternalFileId();
314 314
 			});
315 315
 
316
-			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
316
+			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) {
317 317
 				$perms = $node->getDavPermissions();
318 318
 				if ($this->isPublic) {
319 319
 					// remove mount information
@@ -322,13 +322,13 @@  discard block
 block discarded – undo
322 322
 				return $perms;
323 323
 			});
324 324
 
325
-			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
325
+			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function() use ($node, $httpRequest) {
326 326
 				return $node->getSharePermissions(
327 327
 					$httpRequest->getRawServerValue('PHP_AUTH_USER')
328 328
 				);
329 329
 			});
330 330
 
331
-			$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
331
+			$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function() use ($node, $httpRequest) {
332 332
 				$ncPermissions = $node->getSharePermissions(
333 333
 					$httpRequest->getRawServerValue('PHP_AUTH_USER')
334 334
 				);
@@ -336,11 +336,11 @@  discard block
 block discarded – undo
336 336
 				return json_encode($ocmPermissions);
337 337
 			});
338 338
 
339
-			$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node) {
339
+			$propFind->handle(self::GETETAG_PROPERTYNAME, function() use ($node) {
340 340
 				return $node->getETag();
341 341
 			});
342 342
 
343
-			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node) {
343
+			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) {
344 344
 				$owner = $node->getOwner();
345 345
 				if (!$owner) {
346 346
 					return null;
@@ -348,7 +348,7 @@  discard block
 block discarded – undo
348 348
 					return $owner->getUID();
349 349
 				}
350 350
 			});
351
-			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node) {
351
+			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) {
352 352
 				$owner = $node->getOwner();
353 353
 				if (!$owner) {
354 354
 					return null;
@@ -357,17 +357,17 @@  discard block
 block discarded – undo
357 357
 				}
358 358
 			});
359 359
 
360
-			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
360
+			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function() use ($node) {
361 361
 				return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
362 362
 			});
363
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
363
+			$propFind->handle(self::SIZE_PROPERTYNAME, function() use ($node) {
364 364
 				return $node->getSize();
365 365
 			});
366
-			$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
366
+			$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function() use ($node) {
367 367
 				return $node->getFileInfo()->getMountPoint()->getMountType();
368 368
 			});
369 369
 
370
-			$propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest) {
370
+			$propFind->handle(self::SHARE_NOTE, function() use ($node, $httpRequest) {
371 371
 				return $node->getNoteFromShare(
372 372
 					$httpRequest->getRawServerValue('PHP_AUTH_USER')
373 373
 				);
@@ -375,13 +375,13 @@  discard block
 block discarded – undo
375 375
 		}
376 376
 
377 377
 		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
378
-			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
378
+			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) {
379 379
 				return $this->config->getSystemValue('data-fingerprint', '');
380 380
 			});
381 381
 		}
382 382
 
383 383
 		if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
384
-			$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
384
+			$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function() use ($node) {
385 385
 				try {
386 386
 					$directDownloadUrl = $node->getDirectDownload();
387 387
 					if (isset($directDownloadUrl['url'])) {
@@ -395,7 +395,7 @@  discard block
 block discarded – undo
395 395
 				return false;
396 396
 			});
397 397
 
398
-			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
398
+			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function() use ($node) {
399 399
 				$checksum = $node->getChecksum();
400 400
 				if ($checksum === null || $checksum === '') {
401 401
 					return null;
@@ -404,21 +404,21 @@  discard block
 block discarded – undo
404 404
 				return new ChecksumList($checksum);
405 405
 			});
406 406
 
407
-			$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
407
+			$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function() use ($node) {
408 408
 				return $node->getFileInfo()->getCreationTime();
409 409
 			});
410 410
 
411
-			$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
411
+			$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function() use ($node) {
412 412
 				return $node->getFileInfo()->getUploadTime();
413 413
 			});
414 414
 		}
415 415
 
416 416
 		if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) {
417
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
417
+			$propFind->handle(self::SIZE_PROPERTYNAME, function() use ($node) {
418 418
 				return $node->getSize();
419 419
 			});
420 420
 
421
-			$propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
421
+			$propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function() use ($node) {
422 422
 				return $node->getFileInfo()->isEncrypted() ? '1' : '0';
423 423
 			});
424 424
 		}
@@ -463,14 +463,14 @@  discard block
 block discarded – undo
463 463
 			return;
464 464
 		}
465 465
 
466
-		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
466
+		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function($time) use ($node) {
467 467
 			if (empty($time)) {
468 468
 				return false;
469 469
 			}
470 470
 			$node->touch($time);
471 471
 			return true;
472 472
 		});
473
-		$propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
473
+		$propPatch->handle(self::GETETAG_PROPERTYNAME, function($etag) use ($node) {
474 474
 			if (empty($etag)) {
475 475
 				return false;
476 476
 			}
@@ -479,7 +479,7 @@  discard block
 block discarded – undo
479 479
 			}
480 480
 			return false;
481 481
 		});
482
-		$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
482
+		$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function($time) use ($node) {
483 483
 			if (empty($time)) {
484 484
 				return false;
485 485
 			}
@@ -499,7 +499,7 @@  discard block
 block discarded – undo
499 499
 			[$path, $name] = \Sabre\Uri\split($filePath);
500 500
 			$info = \OC_FileChunking::decodeName($name);
501 501
 			if (!empty($info)) {
502
-				$filePath = $path . '/' . $info['name'];
502
+				$filePath = $path.'/'.$info['name'];
503 503
 			}
504 504
 		}
505 505
 
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/TagsPlugin.php 2 patches
Indentation   +217 added lines, -217 removed lines patch added patch discarded remove patch
@@ -54,245 +54,245 @@
 block discarded – undo
54 54
 
55 55
 class TagsPlugin extends \Sabre\DAV\ServerPlugin {
56 56
 
57
-	// namespace
58
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
59
-	public const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
60
-	public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
61
-	public const TAG_FAVORITE = '_$!<Favorite>!$_';
57
+    // namespace
58
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
59
+    public const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
60
+    public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
61
+    public const TAG_FAVORITE = '_$!<Favorite>!$_';
62 62
 
63
-	/**
64
-	 * Reference to main server object
65
-	 *
66
-	 * @var \Sabre\DAV\Server
67
-	 */
68
-	private $server;
63
+    /**
64
+     * Reference to main server object
65
+     *
66
+     * @var \Sabre\DAV\Server
67
+     */
68
+    private $server;
69 69
 
70
-	/**
71
-	 * @var \OCP\ITagManager
72
-	 */
73
-	private $tagManager;
70
+    /**
71
+     * @var \OCP\ITagManager
72
+     */
73
+    private $tagManager;
74 74
 
75
-	/**
76
-	 * @var \OCP\ITags
77
-	 */
78
-	private $tagger;
75
+    /**
76
+     * @var \OCP\ITags
77
+     */
78
+    private $tagger;
79 79
 
80
-	/**
81
-	 * Array of file id to tags array
82
-	 * The null value means the cache wasn't initialized.
83
-	 *
84
-	 * @var array
85
-	 */
86
-	private $cachedTags;
80
+    /**
81
+     * Array of file id to tags array
82
+     * The null value means the cache wasn't initialized.
83
+     *
84
+     * @var array
85
+     */
86
+    private $cachedTags;
87 87
 
88
-	/**
89
-	 * @var \Sabre\DAV\Tree
90
-	 */
91
-	private $tree;
88
+    /**
89
+     * @var \Sabre\DAV\Tree
90
+     */
91
+    private $tree;
92 92
 
93
-	/**
94
-	 * @param \Sabre\DAV\Tree $tree tree
95
-	 * @param \OCP\ITagManager $tagManager tag manager
96
-	 */
97
-	public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) {
98
-		$this->tree = $tree;
99
-		$this->tagManager = $tagManager;
100
-		$this->tagger = null;
101
-		$this->cachedTags = [];
102
-	}
93
+    /**
94
+     * @param \Sabre\DAV\Tree $tree tree
95
+     * @param \OCP\ITagManager $tagManager tag manager
96
+     */
97
+    public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) {
98
+        $this->tree = $tree;
99
+        $this->tagManager = $tagManager;
100
+        $this->tagger = null;
101
+        $this->cachedTags = [];
102
+    }
103 103
 
104
-	/**
105
-	 * This initializes the plugin.
106
-	 *
107
-	 * This function is called by \Sabre\DAV\Server, after
108
-	 * addPlugin is called.
109
-	 *
110
-	 * This method should set up the required event subscriptions.
111
-	 *
112
-	 * @param \Sabre\DAV\Server $server
113
-	 * @return void
114
-	 */
115
-	public function initialize(\Sabre\DAV\Server $server) {
116
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
117
-		$server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class;
104
+    /**
105
+     * This initializes the plugin.
106
+     *
107
+     * This function is called by \Sabre\DAV\Server, after
108
+     * addPlugin is called.
109
+     *
110
+     * This method should set up the required event subscriptions.
111
+     *
112
+     * @param \Sabre\DAV\Server $server
113
+     * @return void
114
+     */
115
+    public function initialize(\Sabre\DAV\Server $server) {
116
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
117
+        $server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class;
118 118
 
119
-		$this->server = $server;
120
-		$this->server->on('propFind', [$this, 'handleGetProperties']);
121
-		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
122
-	}
119
+        $this->server = $server;
120
+        $this->server->on('propFind', [$this, 'handleGetProperties']);
121
+        $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
122
+    }
123 123
 
124
-	/**
125
-	 * Returns the tagger
126
-	 *
127
-	 * @return \OCP\ITags tagger
128
-	 */
129
-	private function getTagger() {
130
-		if (!$this->tagger) {
131
-			$this->tagger = $this->tagManager->load('files');
132
-		}
133
-		return $this->tagger;
134
-	}
124
+    /**
125
+     * Returns the tagger
126
+     *
127
+     * @return \OCP\ITags tagger
128
+     */
129
+    private function getTagger() {
130
+        if (!$this->tagger) {
131
+            $this->tagger = $this->tagManager->load('files');
132
+        }
133
+        return $this->tagger;
134
+    }
135 135
 
136
-	/**
137
-	 * Returns tags and favorites.
138
-	 *
139
-	 * @param integer $fileId file id
140
-	 * @return array list($tags, $favorite) with $tags as tag array
141
-	 * and $favorite is a boolean whether the file was favorited
142
-	 */
143
-	private function getTagsAndFav($fileId) {
144
-		$isFav = false;
145
-		$tags = $this->getTags($fileId);
146
-		if ($tags) {
147
-			$favPos = array_search(self::TAG_FAVORITE, $tags);
148
-			if ($favPos !== false) {
149
-				$isFav = true;
150
-				unset($tags[$favPos]);
151
-			}
152
-		}
153
-		return [$tags, $isFav];
154
-	}
136
+    /**
137
+     * Returns tags and favorites.
138
+     *
139
+     * @param integer $fileId file id
140
+     * @return array list($tags, $favorite) with $tags as tag array
141
+     * and $favorite is a boolean whether the file was favorited
142
+     */
143
+    private function getTagsAndFav($fileId) {
144
+        $isFav = false;
145
+        $tags = $this->getTags($fileId);
146
+        if ($tags) {
147
+            $favPos = array_search(self::TAG_FAVORITE, $tags);
148
+            if ($favPos !== false) {
149
+                $isFav = true;
150
+                unset($tags[$favPos]);
151
+            }
152
+        }
153
+        return [$tags, $isFav];
154
+    }
155 155
 
156
-	/**
157
-	 * Returns tags for the given file id
158
-	 *
159
-	 * @param integer $fileId file id
160
-	 * @return array list of tags for that file
161
-	 */
162
-	private function getTags($fileId) {
163
-		if (isset($this->cachedTags[$fileId])) {
164
-			return $this->cachedTags[$fileId];
165
-		} else {
166
-			$tags = $this->getTagger()->getTagsForObjects([$fileId]);
167
-			if ($tags !== false) {
168
-				if (empty($tags)) {
169
-					return [];
170
-				}
171
-				return current($tags);
172
-			}
173
-		}
174
-		return null;
175
-	}
156
+    /**
157
+     * Returns tags for the given file id
158
+     *
159
+     * @param integer $fileId file id
160
+     * @return array list of tags for that file
161
+     */
162
+    private function getTags($fileId) {
163
+        if (isset($this->cachedTags[$fileId])) {
164
+            return $this->cachedTags[$fileId];
165
+        } else {
166
+            $tags = $this->getTagger()->getTagsForObjects([$fileId]);
167
+            if ($tags !== false) {
168
+                if (empty($tags)) {
169
+                    return [];
170
+                }
171
+                return current($tags);
172
+            }
173
+        }
174
+        return null;
175
+    }
176 176
 
177
-	/**
178
-	 * Updates the tags of the given file id
179
-	 *
180
-	 * @param int $fileId
181
-	 * @param array $tags array of tag strings
182
-	 */
183
-	private function updateTags($fileId, $tags) {
184
-		$tagger = $this->getTagger();
185
-		$currentTags = $this->getTags($fileId);
177
+    /**
178
+     * Updates the tags of the given file id
179
+     *
180
+     * @param int $fileId
181
+     * @param array $tags array of tag strings
182
+     */
183
+    private function updateTags($fileId, $tags) {
184
+        $tagger = $this->getTagger();
185
+        $currentTags = $this->getTags($fileId);
186 186
 
187
-		$newTags = array_diff($tags, $currentTags);
188
-		foreach ($newTags as $tag) {
189
-			if ($tag === self::TAG_FAVORITE) {
190
-				continue;
191
-			}
192
-			$tagger->tagAs($fileId, $tag);
193
-		}
194
-		$deletedTags = array_diff($currentTags, $tags);
195
-		foreach ($deletedTags as $tag) {
196
-			if ($tag === self::TAG_FAVORITE) {
197
-				continue;
198
-			}
199
-			$tagger->unTag($fileId, $tag);
200
-		}
201
-	}
187
+        $newTags = array_diff($tags, $currentTags);
188
+        foreach ($newTags as $tag) {
189
+            if ($tag === self::TAG_FAVORITE) {
190
+                continue;
191
+            }
192
+            $tagger->tagAs($fileId, $tag);
193
+        }
194
+        $deletedTags = array_diff($currentTags, $tags);
195
+        foreach ($deletedTags as $tag) {
196
+            if ($tag === self::TAG_FAVORITE) {
197
+                continue;
198
+            }
199
+            $tagger->unTag($fileId, $tag);
200
+        }
201
+    }
202 202
 
203
-	/**
204
-	 * Adds tags and favorites properties to the response,
205
-	 * if requested.
206
-	 *
207
-	 * @param PropFind $propFind
208
-	 * @param \Sabre\DAV\INode $node
209
-	 * @return void
210
-	 */
211
-	public function handleGetProperties(
212
-		PropFind $propFind,
213
-		\Sabre\DAV\INode $node
214
-	) {
215
-		if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
216
-			return;
217
-		}
203
+    /**
204
+     * Adds tags and favorites properties to the response,
205
+     * if requested.
206
+     *
207
+     * @param PropFind $propFind
208
+     * @param \Sabre\DAV\INode $node
209
+     * @return void
210
+     */
211
+    public function handleGetProperties(
212
+        PropFind $propFind,
213
+        \Sabre\DAV\INode $node
214
+    ) {
215
+        if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
216
+            return;
217
+        }
218 218
 
219
-		// need prefetch ?
220
-		if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
221
-			&& $propFind->getDepth() !== 0
222
-			&& (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
223
-			|| !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
224
-		)) {
225
-			// note: pre-fetching only supported for depth <= 1
226
-			$folderContent = $node->getChildren();
227
-			$fileIds[] = (int)$node->getId();
228
-			foreach ($folderContent as $info) {
229
-				$fileIds[] = (int)$info->getId();
230
-			}
231
-			$tags = $this->getTagger()->getTagsForObjects($fileIds);
232
-			if ($tags === false) {
233
-				// the tags API returns false on error...
234
-				$tags = [];
235
-			}
219
+        // need prefetch ?
220
+        if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
221
+            && $propFind->getDepth() !== 0
222
+            && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
223
+            || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
224
+        )) {
225
+            // note: pre-fetching only supported for depth <= 1
226
+            $folderContent = $node->getChildren();
227
+            $fileIds[] = (int)$node->getId();
228
+            foreach ($folderContent as $info) {
229
+                $fileIds[] = (int)$info->getId();
230
+            }
231
+            $tags = $this->getTagger()->getTagsForObjects($fileIds);
232
+            if ($tags === false) {
233
+                // the tags API returns false on error...
234
+                $tags = [];
235
+            }
236 236
 
237
-			$this->cachedTags = $this->cachedTags + $tags;
238
-			$emptyFileIds = array_diff($fileIds, array_keys($tags));
239
-			// also cache the ones that were not found
240
-			foreach ($emptyFileIds as $fileId) {
241
-				$this->cachedTags[$fileId] = [];
242
-			}
243
-		}
237
+            $this->cachedTags = $this->cachedTags + $tags;
238
+            $emptyFileIds = array_diff($fileIds, array_keys($tags));
239
+            // also cache the ones that were not found
240
+            foreach ($emptyFileIds as $fileId) {
241
+                $this->cachedTags[$fileId] = [];
242
+            }
243
+        }
244 244
 
245
-		$isFav = null;
245
+        $isFav = null;
246 246
 
247
-		$propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
248
-			[$tags, $isFav] = $this->getTagsAndFav($node->getId());
249
-			return new TagList($tags);
250
-		});
247
+        $propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
248
+            [$tags, $isFav] = $this->getTagsAndFav($node->getId());
249
+            return new TagList($tags);
250
+        });
251 251
 
252
-		$propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
253
-			if (is_null($isFav)) {
254
-				[, $isFav] = $this->getTagsAndFav($node->getId());
255
-			}
256
-			if ($isFav) {
257
-				return 1;
258
-			} else {
259
-				return 0;
260
-			}
261
-		});
262
-	}
252
+        $propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
253
+            if (is_null($isFav)) {
254
+                [, $isFav] = $this->getTagsAndFav($node->getId());
255
+            }
256
+            if ($isFav) {
257
+                return 1;
258
+            } else {
259
+                return 0;
260
+            }
261
+        });
262
+    }
263 263
 
264
-	/**
265
-	 * Updates tags and favorites properties, if applicable.
266
-	 *
267
-	 * @param string $path
268
-	 * @param PropPatch $propPatch
269
-	 *
270
-	 * @return void
271
-	 */
272
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
273
-		$node = $this->tree->getNodeForPath($path);
274
-		if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
275
-			return;
276
-		}
264
+    /**
265
+     * Updates tags and favorites properties, if applicable.
266
+     *
267
+     * @param string $path
268
+     * @param PropPatch $propPatch
269
+     *
270
+     * @return void
271
+     */
272
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
273
+        $node = $this->tree->getNodeForPath($path);
274
+        if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
275
+            return;
276
+        }
277 277
 
278
-		$propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
279
-			$this->updateTags($node->getId(), $tagList->getTags());
280
-			return true;
281
-		});
278
+        $propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
279
+            $this->updateTags($node->getId(), $tagList->getTags());
280
+            return true;
281
+        });
282 282
 
283
-		$propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) {
284
-			if ((int)$favState === 1 || $favState === 'true') {
285
-				$this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
286
-			} else {
287
-				$this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
288
-			}
283
+        $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) {
284
+            if ((int)$favState === 1 || $favState === 'true') {
285
+                $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
286
+            } else {
287
+                $this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
288
+            }
289 289
 
290
-			if (is_null($favState)) {
291
-				// confirm deletion
292
-				return 204;
293
-			}
290
+            if (is_null($favState)) {
291
+                // confirm deletion
292
+                return 204;
293
+            }
294 294
 
295
-			return 200;
296
-		});
297
-	}
295
+            return 200;
296
+        });
297
+    }
298 298
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -224,9 +224,9 @@  discard block
 block discarded – undo
224 224
 		)) {
225 225
 			// note: pre-fetching only supported for depth <= 1
226 226
 			$folderContent = $node->getChildren();
227
-			$fileIds[] = (int)$node->getId();
227
+			$fileIds[] = (int) $node->getId();
228 228
 			foreach ($folderContent as $info) {
229
-				$fileIds[] = (int)$info->getId();
229
+				$fileIds[] = (int) $info->getId();
230 230
 			}
231 231
 			$tags = $this->getTagger()->getTagsForObjects($fileIds);
232 232
 			if ($tags === false) {
@@ -244,12 +244,12 @@  discard block
 block discarded – undo
244 244
 
245 245
 		$isFav = null;
246 246
 
247
-		$propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
247
+		$propFind->handle(self::TAGS_PROPERTYNAME, function() use (&$isFav, $node) {
248 248
 			[$tags, $isFav] = $this->getTagsAndFav($node->getId());
249 249
 			return new TagList($tags);
250 250
 		});
251 251
 
252
-		$propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
252
+		$propFind->handle(self::FAVORITE_PROPERTYNAME, function() use ($isFav, $node) {
253 253
 			if (is_null($isFav)) {
254 254
 				[, $isFav] = $this->getTagsAndFav($node->getId());
255 255
 			}
@@ -275,13 +275,13 @@  discard block
 block discarded – undo
275 275
 			return;
276 276
 		}
277 277
 
278
-		$propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
278
+		$propPatch->handle(self::TAGS_PROPERTYNAME, function($tagList) use ($node) {
279 279
 			$this->updateTags($node->getId(), $tagList->getTags());
280 280
 			return true;
281 281
 		});
282 282
 
283
-		$propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) {
284
-			if ((int)$favState === 1 || $favState === 'true') {
283
+		$propPatch->handle(self::FAVORITE_PROPERTYNAME, function($favState) use ($node) {
284
+			if ((int) $favState === 1 || $favState === 'true') {
285 285
 				$this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
286 286
 			} else {
287 287
 				$this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/QuotaPlugin.php 1 patch
Indentation   +167 added lines, -167 removed lines patch added patch discarded remove patch
@@ -44,171 +44,171 @@
 block discarded – undo
44 44
  */
45 45
 class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
46 46
 
47
-	/** @var \OC\Files\View */
48
-	private $view;
49
-
50
-	/**
51
-	 * Reference to main server object
52
-	 *
53
-	 * @var \Sabre\DAV\Server
54
-	 */
55
-	private $server;
56
-
57
-	/**
58
-	 * @param \OC\Files\View $view
59
-	 */
60
-	public function __construct($view) {
61
-		$this->view = $view;
62
-	}
63
-
64
-	/**
65
-	 * This initializes the plugin.
66
-	 *
67
-	 * This function is called by \Sabre\DAV\Server, after
68
-	 * addPlugin is called.
69
-	 *
70
-	 * This method should set up the requires event subscriptions.
71
-	 *
72
-	 * @param \Sabre\DAV\Server $server
73
-	 * @return void
74
-	 */
75
-	public function initialize(\Sabre\DAV\Server $server) {
76
-		$this->server = $server;
77
-
78
-		$server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
79
-		$server->on('beforeCreateFile', [$this, 'beforeCreateFile'], 10);
80
-		$server->on('beforeMove', [$this, 'beforeMove'], 10);
81
-	}
82
-
83
-	/**
84
-	 * Check quota before creating file
85
-	 *
86
-	 * @param string $uri target file URI
87
-	 * @param resource $data data
88
-	 * @param INode $parent Sabre Node
89
-	 * @param bool $modified modified
90
-	 */
91
-	public function beforeCreateFile($uri, $data, INode $parent, $modified) {
92
-		if (!$parent instanceof Node) {
93
-			return;
94
-		}
95
-
96
-		return $this->checkQuota($parent->getPath() . '/' . basename($uri));
97
-	}
98
-
99
-	/**
100
-	 * Check quota before writing content
101
-	 *
102
-	 * @param string $uri target file URI
103
-	 * @param INode $node Sabre Node
104
-	 * @param resource $data data
105
-	 * @param bool $modified modified
106
-	 */
107
-	public function beforeWriteContent($uri, INode $node, $data, $modified) {
108
-		if (!$node instanceof Node) {
109
-			return;
110
-		}
111
-
112
-		return $this->checkQuota($node->getPath());
113
-	}
114
-
115
-	/**
116
-	 * Check if we're moving a Futurefile in which case we need to check
117
-	 * the quota on the target destination.
118
-	 *
119
-	 * @param string $source source path
120
-	 * @param string $destination destination path
121
-	 */
122
-	public function beforeMove($source, $destination) {
123
-		$sourceNode = $this->server->tree->getNodeForPath($source);
124
-		if (!$sourceNode instanceof FutureFile) {
125
-			return;
126
-		}
127
-
128
-		// get target node for proper path conversion
129
-		if ($this->server->tree->nodeExists($destination)) {
130
-			$destinationNode = $this->server->tree->getNodeForPath($destination);
131
-			$path = $destinationNode->getPath();
132
-		} else {
133
-			$parentNode = $this->server->tree->getNodeForPath(dirname($destination));
134
-			$path = $parentNode->getPath();
135
-		}
136
-
137
-		return $this->checkQuota($path, $sourceNode->getSize());
138
-	}
139
-
140
-
141
-	/**
142
-	 * This method is called before any HTTP method and validates there is enough free space to store the file
143
-	 *
144
-	 * @param string $path relative to the users home
145
-	 * @param int $length
146
-	 * @throws InsufficientStorage
147
-	 * @return bool
148
-	 */
149
-	public function checkQuota($path, $length = null) {
150
-		if ($length === null) {
151
-			$length = $this->getLength();
152
-		}
153
-
154
-		if ($length) {
155
-			[$parentPath, $newName] = \Sabre\Uri\split($path);
156
-			if (is_null($parentPath)) {
157
-				$parentPath = '';
158
-			}
159
-			$req = $this->server->httpRequest;
160
-			if ($req->getHeader('OC-Chunked')) {
161
-				$info = \OC_FileChunking::decodeName($newName);
162
-				$chunkHandler = $this->getFileChunking($info);
163
-				// subtract the already uploaded size to see whether
164
-				// there is still enough space for the remaining chunks
165
-				$length -= $chunkHandler->getCurrentSize();
166
-				// use target file name for free space check in case of shared files
167
-				$path = rtrim($parentPath, '/') . '/' . $info['name'];
168
-			}
169
-			$freeSpace = $this->getFreeSpace($path);
170
-			if ($freeSpace >= 0 && $length > $freeSpace) {
171
-				if (isset($chunkHandler)) {
172
-					$chunkHandler->cleanup();
173
-				}
174
-				throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available");
175
-			}
176
-		}
177
-		return true;
178
-	}
179
-
180
-	public function getFileChunking($info) {
181
-		// FIXME: need a factory for better mocking support
182
-		return new \OC_FileChunking($info);
183
-	}
184
-
185
-	public function getLength() {
186
-		$req = $this->server->httpRequest;
187
-		$length = $req->getHeader('X-Expected-Entity-Length');
188
-		if (!is_numeric($length)) {
189
-			$length = $req->getHeader('Content-Length');
190
-			$length = is_numeric($length) ? $length : null;
191
-		}
192
-
193
-		$ocLength = $req->getHeader('OC-Total-Length');
194
-		if (is_numeric($length) && is_numeric($ocLength)) {
195
-			return max($length, $ocLength);
196
-		}
197
-
198
-		return $length;
199
-	}
200
-
201
-	/**
202
-	 * @param string $uri
203
-	 * @return mixed
204
-	 * @throws ServiceUnavailable
205
-	 */
206
-	public function getFreeSpace($uri) {
207
-		try {
208
-			$freeSpace = $this->view->free_space(ltrim($uri, '/'));
209
-			return $freeSpace;
210
-		} catch (StorageNotAvailableException $e) {
211
-			throw new ServiceUnavailable($e->getMessage());
212
-		}
213
-	}
47
+    /** @var \OC\Files\View */
48
+    private $view;
49
+
50
+    /**
51
+     * Reference to main server object
52
+     *
53
+     * @var \Sabre\DAV\Server
54
+     */
55
+    private $server;
56
+
57
+    /**
58
+     * @param \OC\Files\View $view
59
+     */
60
+    public function __construct($view) {
61
+        $this->view = $view;
62
+    }
63
+
64
+    /**
65
+     * This initializes the plugin.
66
+     *
67
+     * This function is called by \Sabre\DAV\Server, after
68
+     * addPlugin is called.
69
+     *
70
+     * This method should set up the requires event subscriptions.
71
+     *
72
+     * @param \Sabre\DAV\Server $server
73
+     * @return void
74
+     */
75
+    public function initialize(\Sabre\DAV\Server $server) {
76
+        $this->server = $server;
77
+
78
+        $server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
79
+        $server->on('beforeCreateFile', [$this, 'beforeCreateFile'], 10);
80
+        $server->on('beforeMove', [$this, 'beforeMove'], 10);
81
+    }
82
+
83
+    /**
84
+     * Check quota before creating file
85
+     *
86
+     * @param string $uri target file URI
87
+     * @param resource $data data
88
+     * @param INode $parent Sabre Node
89
+     * @param bool $modified modified
90
+     */
91
+    public function beforeCreateFile($uri, $data, INode $parent, $modified) {
92
+        if (!$parent instanceof Node) {
93
+            return;
94
+        }
95
+
96
+        return $this->checkQuota($parent->getPath() . '/' . basename($uri));
97
+    }
98
+
99
+    /**
100
+     * Check quota before writing content
101
+     *
102
+     * @param string $uri target file URI
103
+     * @param INode $node Sabre Node
104
+     * @param resource $data data
105
+     * @param bool $modified modified
106
+     */
107
+    public function beforeWriteContent($uri, INode $node, $data, $modified) {
108
+        if (!$node instanceof Node) {
109
+            return;
110
+        }
111
+
112
+        return $this->checkQuota($node->getPath());
113
+    }
114
+
115
+    /**
116
+     * Check if we're moving a Futurefile in which case we need to check
117
+     * the quota on the target destination.
118
+     *
119
+     * @param string $source source path
120
+     * @param string $destination destination path
121
+     */
122
+    public function beforeMove($source, $destination) {
123
+        $sourceNode = $this->server->tree->getNodeForPath($source);
124
+        if (!$sourceNode instanceof FutureFile) {
125
+            return;
126
+        }
127
+
128
+        // get target node for proper path conversion
129
+        if ($this->server->tree->nodeExists($destination)) {
130
+            $destinationNode = $this->server->tree->getNodeForPath($destination);
131
+            $path = $destinationNode->getPath();
132
+        } else {
133
+            $parentNode = $this->server->tree->getNodeForPath(dirname($destination));
134
+            $path = $parentNode->getPath();
135
+        }
136
+
137
+        return $this->checkQuota($path, $sourceNode->getSize());
138
+    }
139
+
140
+
141
+    /**
142
+     * This method is called before any HTTP method and validates there is enough free space to store the file
143
+     *
144
+     * @param string $path relative to the users home
145
+     * @param int $length
146
+     * @throws InsufficientStorage
147
+     * @return bool
148
+     */
149
+    public function checkQuota($path, $length = null) {
150
+        if ($length === null) {
151
+            $length = $this->getLength();
152
+        }
153
+
154
+        if ($length) {
155
+            [$parentPath, $newName] = \Sabre\Uri\split($path);
156
+            if (is_null($parentPath)) {
157
+                $parentPath = '';
158
+            }
159
+            $req = $this->server->httpRequest;
160
+            if ($req->getHeader('OC-Chunked')) {
161
+                $info = \OC_FileChunking::decodeName($newName);
162
+                $chunkHandler = $this->getFileChunking($info);
163
+                // subtract the already uploaded size to see whether
164
+                // there is still enough space for the remaining chunks
165
+                $length -= $chunkHandler->getCurrentSize();
166
+                // use target file name for free space check in case of shared files
167
+                $path = rtrim($parentPath, '/') . '/' . $info['name'];
168
+            }
169
+            $freeSpace = $this->getFreeSpace($path);
170
+            if ($freeSpace >= 0 && $length > $freeSpace) {
171
+                if (isset($chunkHandler)) {
172
+                    $chunkHandler->cleanup();
173
+                }
174
+                throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available");
175
+            }
176
+        }
177
+        return true;
178
+    }
179
+
180
+    public function getFileChunking($info) {
181
+        // FIXME: need a factory for better mocking support
182
+        return new \OC_FileChunking($info);
183
+    }
184
+
185
+    public function getLength() {
186
+        $req = $this->server->httpRequest;
187
+        $length = $req->getHeader('X-Expected-Entity-Length');
188
+        if (!is_numeric($length)) {
189
+            $length = $req->getHeader('Content-Length');
190
+            $length = is_numeric($length) ? $length : null;
191
+        }
192
+
193
+        $ocLength = $req->getHeader('OC-Total-Length');
194
+        if (is_numeric($length) && is_numeric($ocLength)) {
195
+            return max($length, $ocLength);
196
+        }
197
+
198
+        return $length;
199
+    }
200
+
201
+    /**
202
+     * @param string $uri
203
+     * @return mixed
204
+     * @throws ServiceUnavailable
205
+     */
206
+    public function getFreeSpace($uri) {
207
+        try {
208
+            $freeSpace = $this->view->free_space(ltrim($uri, '/'));
209
+            return $freeSpace;
210
+        } catch (StorageNotAvailableException $e) {
211
+            throw new ServiceUnavailable($e->getMessage());
212
+        }
213
+    }
214 214
 }
Please login to merge, or discard this patch.