Passed
Push — master ( 7c4b7e...ed5661 )
by Roeland
11:08 queued 12s
created

CardDavBackend::search()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 37
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 25
c 1
b 0
f 0
nc 6
nop 4
dl 0
loc 37
rs 8.8977
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arne Hamann <[email protected]>
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bjoern Schiessle <[email protected]>
8
 * @author Björn Schießle <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Georg Ehrke <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author John Molakvoæ (skjnldsv) <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Stefan Weil <[email protected]>
18
 * @author Thomas Citharel <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 *
21
 * @license AGPL-3.0
22
 *
23
 * This code is free software: you can redistribute it and/or modify
24
 * it under the terms of the GNU Affero General Public License, version 3,
25
 * as published by the Free Software Foundation.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
 * GNU Affero General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
33
 * along with this program. If not, see <http://www.gnu.org/licenses/>
34
 *
35
 */
36
37
namespace OCA\DAV\CardDAV;
38
39
use OCA\DAV\Connector\Sabre\Principal;
40
use OCA\DAV\DAV\Sharing\Backend;
41
use OCA\DAV\DAV\Sharing\IShareable;
42
use OCP\DB\QueryBuilder\IQueryBuilder;
43
use OCP\IDBConnection;
44
use OCP\IGroupManager;
45
use OCP\IUser;
46
use OCP\IUserManager;
47
use PDO;
48
use Sabre\CardDAV\Backend\BackendInterface;
49
use Sabre\CardDAV\Backend\SyncSupport;
50
use Sabre\CardDAV\Plugin;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, OCA\DAV\CardDAV\Plugin. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
51
use Sabre\DAV\Exception\BadRequest;
52
use Sabre\VObject\Component\VCard;
53
use Sabre\VObject\Reader;
54
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
55
use Symfony\Component\EventDispatcher\GenericEvent;
56
57
class CardDavBackend implements BackendInterface, SyncSupport {
58
	public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
59
	public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
60
61
	/** @var Principal */
62
	private $principalBackend;
63
64
	/** @var string */
65
	private $dbCardsTable = 'cards';
66
67
	/** @var string */
68
	private $dbCardsPropertiesTable = 'cards_properties';
69
70
	/** @var IDBConnection */
71
	private $db;
72
73
	/** @var Backend */
74
	private $sharingBackend;
75
76
	/** @var array properties to index */
77
	public static $indexProperties = [
78
		'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
79
		'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'];
80
81
	/**
82
	 * @var string[] Map of uid => display name
83
	 */
84
	protected $userDisplayNames;
85
86
	/** @var IUserManager */
87
	private $userManager;
88
89
	/** @var EventDispatcherInterface */
90
	private $dispatcher;
91
92
	/**
93
	 * CardDavBackend constructor.
94
	 *
95
	 * @param IDBConnection $db
96
	 * @param Principal $principalBackend
97
	 * @param IUserManager $userManager
98
	 * @param IGroupManager $groupManager
99
	 * @param EventDispatcherInterface $dispatcher
100
	 */
101
	public function __construct(IDBConnection $db,
102
								Principal $principalBackend,
103
								IUserManager $userManager,
104
								IGroupManager $groupManager,
105
								EventDispatcherInterface $dispatcher) {
106
		$this->db = $db;
107
		$this->principalBackend = $principalBackend;
108
		$this->userManager = $userManager;
109
		$this->dispatcher = $dispatcher;
110
		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
111
	}
112
113
	/**
114
	 * Return the number of address books for a principal
115
	 *
116
	 * @param $principalUri
117
	 * @return int
118
	 */
119
	public function getAddressBooksForUserCount($principalUri) {
120
		$principalUri = $this->convertPrincipal($principalUri, true);
121
		$query = $this->db->getQueryBuilder();
122
		$query->select($query->func()->count('*'))
123
			->from('addressbooks')
124
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
125
126
		return (int)$query->execute()->fetchColumn();
127
	}
128
129
	/**
130
	 * Returns the list of address books for a specific user.
131
	 *
132
	 * Every addressbook should have the following properties:
133
	 *   id - an arbitrary unique id
134
	 *   uri - the 'basename' part of the url
135
	 *   principaluri - Same as the passed parameter
136
	 *
137
	 * Any additional clark-notation property may be passed besides this. Some
138
	 * common ones are :
139
	 *   {DAV:}displayname
140
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
141
	 *   {http://calendarserver.org/ns/}getctag
142
	 *
143
	 * @param string $principalUri
144
	 * @return array
145
	 */
146
	public function getAddressBooksForUser($principalUri) {
147
		$principalUriOriginal = $principalUri;
148
		$principalUri = $this->convertPrincipal($principalUri, true);
149
		$query = $this->db->getQueryBuilder();
150
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
151
			->from('addressbooks')
152
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
153
154
		$addressBooks = [];
155
156
		$result = $query->execute();
157
		while ($row = $result->fetch()) {
158
			$addressBooks[$row['id']] = [
159
				'id'  => $row['id'],
160
				'uri' => $row['uri'],
161
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
162
				'{DAV:}displayname' => $row['displayname'],
163
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
164
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
165
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
166
			];
167
168
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
169
		}
170
		$result->closeCursor();
171
172
		// query for shared addressbooks
173
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
174
		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
175
176
		$principals = array_map(function ($principal) {
177
			return urldecode($principal);
178
		}, $principals);
179
		$principals[]= $principalUri;
180
181
		$query = $this->db->getQueryBuilder();
182
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
183
			->from('dav_shares', 's')
184
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
185
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
186
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
187
			->setParameter('type', 'addressbook')
188
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
189
			->execute();
190
191
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
192
		while ($row = $result->fetch()) {
193
			if ($row['principaluri'] === $principalUri) {
194
				continue;
195
			}
196
197
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
198
			if (isset($addressBooks[$row['id']])) {
199
				if ($readOnly) {
200
					// New share can not have more permissions then the old one.
201
					continue;
202
				}
203
				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
204
					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
205
					// Old share is already read-write, no more permissions can be gained
206
					continue;
207
				}
208
			}
209
210
			list(, $name) = \Sabre\Uri\split($row['principaluri']);
211
			$uri = $row['uri'] . '_shared_by_' . $name;
212
			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
213
214
			$addressBooks[$row['id']] = [
215
				'id'  => $row['id'],
216
				'uri' => $uri,
217
				'principaluri' => $principalUriOriginal,
218
				'{DAV:}displayname' => $displayName,
219
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
220
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
221
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
222
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
223
				$readOnlyPropertyName => $readOnly,
224
			];
225
226
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
227
		}
228
		$result->closeCursor();
229
230
		return array_values($addressBooks);
231
	}
232
233
	public function getUsersOwnAddressBooks($principalUri) {
234
		$principalUri = $this->convertPrincipal($principalUri, true);
235
		$query = $this->db->getQueryBuilder();
236
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
237
			  ->from('addressbooks')
238
			  ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
239
240
		$addressBooks = [];
241
242
		$result = $query->execute();
243
		while ($row = $result->fetch()) {
244
			$addressBooks[$row['id']] = [
245
				'id'  => $row['id'],
246
				'uri' => $row['uri'],
247
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
248
				'{DAV:}displayname' => $row['displayname'],
249
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
250
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
251
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
252
			];
253
254
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
255
		}
256
		$result->closeCursor();
257
258
		return array_values($addressBooks);
259
	}
260
261
	private function getUserDisplayName($uid) {
262
		if (!isset($this->userDisplayNames[$uid])) {
263
			$user = $this->userManager->get($uid);
264
265
			if ($user instanceof IUser) {
266
				$this->userDisplayNames[$uid] = $user->getDisplayName();
267
			} else {
268
				$this->userDisplayNames[$uid] = $uid;
269
			}
270
		}
271
272
		return $this->userDisplayNames[$uid];
273
	}
274
275
	/**
276
	 * @param int $addressBookId
277
	 */
278
	public function getAddressBookById($addressBookId) {
279
		$query = $this->db->getQueryBuilder();
280
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
281
			->from('addressbooks')
282
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
283
			->execute();
284
285
		$row = $result->fetch();
286
		$result->closeCursor();
287
		if ($row === false) {
288
			return null;
289
		}
290
291
		$addressBook = [
292
			'id'  => $row['id'],
293
			'uri' => $row['uri'],
294
			'principaluri' => $row['principaluri'],
295
			'{DAV:}displayname' => $row['displayname'],
296
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
297
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
298
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
299
		];
300
301
		$this->addOwnerPrincipal($addressBook);
302
303
		return $addressBook;
304
	}
305
306
	/**
307
	 * @param $addressBookUri
308
	 * @return array|null
309
	 */
310
	public function getAddressBooksByUri($principal, $addressBookUri) {
311
		$query = $this->db->getQueryBuilder();
312
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
313
			->from('addressbooks')
314
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
315
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
316
			->setMaxResults(1)
317
			->execute();
318
319
		$row = $result->fetch();
320
		$result->closeCursor();
321
		if ($row === false) {
322
			return null;
323
		}
324
325
		$addressBook = [
326
			'id'  => $row['id'],
327
			'uri' => $row['uri'],
328
			'principaluri' => $row['principaluri'],
329
			'{DAV:}displayname' => $row['displayname'],
330
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
331
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
332
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
333
		];
334
335
		$this->addOwnerPrincipal($addressBook);
336
337
		return $addressBook;
338
	}
339
340
	/**
341
	 * Updates properties for an address book.
342
	 *
343
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
344
	 * To do the actual updates, you must tell this object which properties
345
	 * you're going to process with the handle() method.
346
	 *
347
	 * Calling the handle method is like telling the PropPatch object "I
348
	 * promise I can handle updating this property".
349
	 *
350
	 * Read the PropPatch documentation for more info and examples.
351
	 *
352
	 * @param string $addressBookId
353
	 * @param \Sabre\DAV\PropPatch $propPatch
354
	 * @return void
355
	 */
356
	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
357
		$supportedProperties = [
358
			'{DAV:}displayname',
359
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
360
		];
361
362
		/**
363
		 * @suppress SqlInjectionChecker
364
		 */
365
		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
366
			$updates = [];
367
			foreach ($mutations as $property=>$newValue) {
368
				switch ($property) {
369
					case '{DAV:}displayname':
370
						$updates['displayname'] = $newValue;
371
						break;
372
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
373
						$updates['description'] = $newValue;
374
						break;
375
				}
376
			}
377
			$query = $this->db->getQueryBuilder();
378
			$query->update('addressbooks');
379
380
			foreach ($updates as $key=>$value) {
381
				$query->set($key, $query->createNamedParameter($value));
382
			}
383
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
384
			->execute();
385
386
			$this->addChange($addressBookId, "", 2);
387
388
			return true;
389
		});
390
	}
391
392
	/**
393
	 * Creates a new address book
394
	 *
395
	 * @param string $principalUri
396
	 * @param string $url Just the 'basename' of the url.
397
	 * @param array $properties
398
	 * @return int
399
	 * @throws BadRequest
400
	 */
401
	public function createAddressBook($principalUri, $url, array $properties) {
402
		$values = [
403
			'displayname' => null,
404
			'description' => null,
405
			'principaluri' => $principalUri,
406
			'uri' => $url,
407
			'synctoken' => 1
408
		];
409
410
		foreach ($properties as $property=>$newValue) {
411
			switch ($property) {
412
				case '{DAV:}displayname':
413
					$values['displayname'] = $newValue;
414
					break;
415
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
416
					$values['description'] = $newValue;
417
					break;
418
				default:
419
					throw new BadRequest('Unknown property: ' . $property);
420
			}
421
		}
422
423
		// Fallback to make sure the displayname is set. Some clients may refuse
424
		// to work with addressbooks not having a displayname.
425
		if (is_null($values['displayname'])) {
426
			$values['displayname'] = $url;
427
		}
428
429
		$query = $this->db->getQueryBuilder();
430
		$query->insert('addressbooks')
431
			->values([
432
				'uri' => $query->createParameter('uri'),
433
				'displayname' => $query->createParameter('displayname'),
434
				'description' => $query->createParameter('description'),
435
				'principaluri' => $query->createParameter('principaluri'),
436
				'synctoken' => $query->createParameter('synctoken'),
437
			])
438
			->setParameters($values)
439
			->execute();
440
441
		return $query->getLastInsertId();
442
	}
443
444
	/**
445
	 * Deletes an entire addressbook and all its contents
446
	 *
447
	 * @param mixed $addressBookId
448
	 * @return void
449
	 */
450
	public function deleteAddressBook($addressBookId) {
451
		$query = $this->db->getQueryBuilder();
452
		$query->delete($this->dbCardsTable)
453
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
454
			->setParameter('addressbookid', $addressBookId)
455
			->execute();
456
457
		$query->delete('addressbookchanges')
458
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
459
			->setParameter('addressbookid', $addressBookId)
460
			->execute();
461
462
		$query->delete('addressbooks')
463
			->where($query->expr()->eq('id', $query->createParameter('id')))
464
			->setParameter('id', $addressBookId)
465
			->execute();
466
467
		$this->sharingBackend->deleteAllShares($addressBookId);
468
469
		$query->delete($this->dbCardsPropertiesTable)
470
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
471
			->execute();
472
	}
473
474
	/**
475
	 * Returns all cards for a specific addressbook id.
476
	 *
477
	 * This method should return the following properties for each card:
478
	 *   * carddata - raw vcard data
479
	 *   * uri - Some unique url
480
	 *   * lastmodified - A unix timestamp
481
	 *
482
	 * It's recommended to also return the following properties:
483
	 *   * etag - A unique etag. This must change every time the card changes.
484
	 *   * size - The size of the card in bytes.
485
	 *
486
	 * If these last two properties are provided, less time will be spent
487
	 * calculating them. If they are specified, you can also ommit carddata.
488
	 * This may speed up certain requests, especially with large cards.
489
	 *
490
	 * @param mixed $addressBookId
491
	 * @return array
492
	 */
493
	public function getCards($addressBookId) {
494
		$query = $this->db->getQueryBuilder();
495
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
496
			->from($this->dbCardsTable)
497
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
498
499
		$cards = [];
500
501
		$result = $query->execute();
502
		while ($row = $result->fetch()) {
503
			$row['etag'] = '"' . $row['etag'] . '"';
504
505
			$modified = false;
506
			$row['carddata'] = $this->readBlob($row['carddata'], $modified);
507
			if ($modified) {
508
				$row['size'] = strlen($row['carddata']);
509
			}
510
511
			$cards[] = $row;
512
		}
513
		$result->closeCursor();
514
515
		return $cards;
516
	}
517
518
	/**
519
	 * Returns a specific card.
520
	 *
521
	 * The same set of properties must be returned as with getCards. The only
522
	 * exception is that 'carddata' is absolutely required.
523
	 *
524
	 * If the card does not exist, you must return false.
525
	 *
526
	 * @param mixed $addressBookId
527
	 * @param string $cardUri
528
	 * @return array
529
	 */
530
	public function getCard($addressBookId, $cardUri) {
531
		$query = $this->db->getQueryBuilder();
532
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
533
			->from($this->dbCardsTable)
534
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
535
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
536
			->setMaxResults(1);
537
538
		$result = $query->execute();
539
		$row = $result->fetch();
540
		if (!$row) {
541
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
542
		}
543
		$row['etag'] = '"' . $row['etag'] . '"';
544
545
		$modified = false;
546
		$row['carddata'] = $this->readBlob($row['carddata'], $modified);
547
		if ($modified) {
548
			$row['size'] = strlen($row['carddata']);
549
		}
550
551
		return $row;
552
	}
553
554
	/**
555
	 * Returns a list of cards.
556
	 *
557
	 * This method should work identical to getCard, but instead return all the
558
	 * cards in the list as an array.
559
	 *
560
	 * If the backend supports this, it may allow for some speed-ups.
561
	 *
562
	 * @param mixed $addressBookId
563
	 * @param string[] $uris
564
	 * @return array
565
	 */
566
	public function getMultipleCards($addressBookId, array $uris) {
567
		if (empty($uris)) {
568
			return [];
569
		}
570
571
		$chunks = array_chunk($uris, 100);
572
		$cards = [];
573
574
		$query = $this->db->getQueryBuilder();
575
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
576
			->from($this->dbCardsTable)
577
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
578
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
579
580
		foreach ($chunks as $uris) {
581
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
582
			$result = $query->execute();
583
584
			while ($row = $result->fetch()) {
585
				$row['etag'] = '"' . $row['etag'] . '"';
586
587
				$modified = false;
588
				$row['carddata'] = $this->readBlob($row['carddata'], $modified);
589
				if ($modified) {
590
					$row['size'] = strlen($row['carddata']);
591
				}
592
593
				$cards[] = $row;
594
			}
595
			$result->closeCursor();
596
		}
597
		return $cards;
598
	}
599
600
	/**
601
	 * Creates a new card.
602
	 *
603
	 * The addressbook id will be passed as the first argument. This is the
604
	 * same id as it is returned from the getAddressBooksForUser method.
605
	 *
606
	 * The cardUri is a base uri, and doesn't include the full path. The
607
	 * cardData argument is the vcard body, and is passed as a string.
608
	 *
609
	 * It is possible to return an ETag from this method. This ETag is for the
610
	 * newly created resource, and must be enclosed with double quotes (that
611
	 * is, the string itself must contain the double quotes).
612
	 *
613
	 * You should only return the ETag if you store the carddata as-is. If a
614
	 * subsequent GET request on the same card does not have the same body,
615
	 * byte-by-byte and you did return an ETag here, clients tend to get
616
	 * confused.
617
	 *
618
	 * If you don't return an ETag, you can just return null.
619
	 *
620
	 * @param mixed $addressBookId
621
	 * @param string $cardUri
622
	 * @param string $cardData
623
	 * @return string
624
	 */
625
	public function createCard($addressBookId, $cardUri, $cardData) {
626
		$etag = md5($cardData);
627
		$uid = $this->getUID($cardData);
628
629
		$q = $this->db->getQueryBuilder();
630
		$q->select('uid')
631
			->from($this->dbCardsTable)
632
			->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
633
			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
634
			->setMaxResults(1);
635
		$result = $q->execute();
636
		$count = (bool) $result->fetchColumn();
637
		$result->closeCursor();
638
		if ($count) {
639
			throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
640
		}
641
642
		$query = $this->db->getQueryBuilder();
643
		$query->insert('cards')
644
			->values([
645
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
646
				'uri' => $query->createNamedParameter($cardUri),
647
				'lastmodified' => $query->createNamedParameter(time()),
648
				'addressbookid' => $query->createNamedParameter($addressBookId),
649
				'size' => $query->createNamedParameter(strlen($cardData)),
650
				'etag' => $query->createNamedParameter($etag),
651
				'uid' => $query->createNamedParameter($uid),
652
			])
653
			->execute();
654
655
		$this->addChange($addressBookId, $cardUri, 1);
656
		$this->updateProperties($addressBookId, $cardUri, $cardData);
657
658
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CardDAV\CardDavBackend::createCard' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

658
		$this->dispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CardDAV\CardDavBackend::createCard',
Loading history...
659
			new GenericEvent(null, [
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ardData' => $cardData)). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

659
		$this->dispatcher->/** @scrutinizer ignore-call */ 
660
                     dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
660
				'addressBookId' => $addressBookId,
661
				'cardUri' => $cardUri,
662
				'cardData' => $cardData]));
663
664
		return '"' . $etag . '"';
665
	}
666
667
	/**
668
	 * Updates a card.
669
	 *
670
	 * The addressbook id will be passed as the first argument. This is the
671
	 * same id as it is returned from the getAddressBooksForUser method.
672
	 *
673
	 * The cardUri is a base uri, and doesn't include the full path. The
674
	 * cardData argument is the vcard body, and is passed as a string.
675
	 *
676
	 * It is possible to return an ETag from this method. This ETag should
677
	 * match that of the updated resource, and must be enclosed with double
678
	 * quotes (that is: the string itself must contain the actual quotes).
679
	 *
680
	 * You should only return the ETag if you store the carddata as-is. If a
681
	 * subsequent GET request on the same card does not have the same body,
682
	 * byte-by-byte and you did return an ETag here, clients tend to get
683
	 * confused.
684
	 *
685
	 * If you don't return an ETag, you can just return null.
686
	 *
687
	 * @param mixed $addressBookId
688
	 * @param string $cardUri
689
	 * @param string $cardData
690
	 * @return string
691
	 */
692
	public function updateCard($addressBookId, $cardUri, $cardData) {
693
		$uid = $this->getUID($cardData);
694
		$etag = md5($cardData);
695
		$query = $this->db->getQueryBuilder();
696
		$query->update($this->dbCardsTable)
697
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
698
			->set('lastmodified', $query->createNamedParameter(time()))
699
			->set('size', $query->createNamedParameter(strlen($cardData)))
700
			->set('etag', $query->createNamedParameter($etag))
701
			->set('uid', $query->createNamedParameter($uid))
702
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
703
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
704
			->execute();
705
706
		$this->addChange($addressBookId, $cardUri, 2);
707
		$this->updateProperties($addressBookId, $cardUri, $cardData);
708
709
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CardDAV\CardDavBackend::updateCard' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

709
		$this->dispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CardDAV\CardDavBackend::updateCard',
Loading history...
710
			new GenericEvent(null, [
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ardData' => $cardData)). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

710
		$this->dispatcher->/** @scrutinizer ignore-call */ 
711
                     dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
711
				'addressBookId' => $addressBookId,
712
				'cardUri' => $cardUri,
713
				'cardData' => $cardData]));
714
715
		return '"' . $etag . '"';
716
	}
717
718
	/**
719
	 * Deletes a card
720
	 *
721
	 * @param mixed $addressBookId
722
	 * @param string $cardUri
723
	 * @return bool
724
	 */
725
	public function deleteCard($addressBookId, $cardUri) {
726
		try {
727
			$cardId = $this->getCardId($addressBookId, $cardUri);
728
		} catch (\InvalidArgumentException $e) {
729
			$cardId = null;
730
		}
731
		$query = $this->db->getQueryBuilder();
732
		$ret = $query->delete($this->dbCardsTable)
733
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
734
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
735
			->execute();
736
737
		$this->addChange($addressBookId, $cardUri, 3);
738
739
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CardDAV\CardDavBackend::deleteCard' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

739
		$this->dispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
Loading history...
740
			new GenericEvent(null, [
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...'cardUri' => $cardUri)). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

740
		$this->dispatcher->/** @scrutinizer ignore-call */ 
741
                     dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
741
				'addressBookId' => $addressBookId,
742
				'cardUri' => $cardUri]));
743
744
		if ($ret === 1) {
745
			if ($cardId !== null) {
746
				$this->purgeProperties($addressBookId, $cardId);
747
			}
748
			return true;
749
		}
750
751
		return false;
752
	}
753
754
	/**
755
	 * The getChanges method returns all the changes that have happened, since
756
	 * the specified syncToken in the specified address book.
757
	 *
758
	 * This function should return an array, such as the following:
759
	 *
760
	 * [
761
	 *   'syncToken' => 'The current synctoken',
762
	 *   'added'   => [
763
	 *      'new.txt',
764
	 *   ],
765
	 *   'modified'   => [
766
	 *      'modified.txt',
767
	 *   ],
768
	 *   'deleted' => [
769
	 *      'foo.php.bak',
770
	 *      'old.txt'
771
	 *   ]
772
	 * ];
773
	 *
774
	 * The returned syncToken property should reflect the *current* syncToken
775
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
776
	 * property. This is needed here too, to ensure the operation is atomic.
777
	 *
778
	 * If the $syncToken argument is specified as null, this is an initial
779
	 * sync, and all members should be reported.
780
	 *
781
	 * The modified property is an array of nodenames that have changed since
782
	 * the last token.
783
	 *
784
	 * The deleted property is an array with nodenames, that have been deleted
785
	 * from collection.
786
	 *
787
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
788
	 * 1, you only have to report changes that happened only directly in
789
	 * immediate descendants. If it's 2, it should also include changes from
790
	 * the nodes below the child collections. (grandchildren)
791
	 *
792
	 * The $limit argument allows a client to specify how many results should
793
	 * be returned at most. If the limit is not specified, it should be treated
794
	 * as infinite.
795
	 *
796
	 * If the limit (infinite or not) is higher than you're willing to return,
797
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
798
	 *
799
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
800
	 * return null.
801
	 *
802
	 * The limit is 'suggestive'. You are free to ignore it.
803
	 *
804
	 * @param string $addressBookId
805
	 * @param string $syncToken
806
	 * @param int $syncLevel
807
	 * @param int $limit
808
	 * @return array
809
	 */
810
	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
811
		// Current synctoken
812
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
813
		$stmt->execute([ $addressBookId ]);
814
		$currentToken = $stmt->fetchColumn(0);
815
816
		if (is_null($currentToken)) {
817
			return null;
818
		}
819
820
		$result = [
821
			'syncToken' => $currentToken,
822
			'added'     => [],
823
			'modified'  => [],
824
			'deleted'   => [],
825
		];
826
827
		if ($syncToken) {
828
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
829
			if ($limit>0) {
830
				$query .= " LIMIT " . (int)$limit;
831
			}
832
833
			// Fetching all changes
834
			$stmt = $this->db->prepare($query);
835
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
836
837
			$changes = [];
838
839
			// This loop ensures that any duplicates are overwritten, only the
840
			// last change on a node is relevant.
841
			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
842
				$changes[$row['uri']] = $row['operation'];
843
			}
844
845
			foreach ($changes as $uri => $operation) {
846
				switch ($operation) {
847
					case 1:
848
						$result['added'][] = $uri;
849
						break;
850
					case 2:
851
						$result['modified'][] = $uri;
852
						break;
853
					case 3:
854
						$result['deleted'][] = $uri;
855
						break;
856
				}
857
			}
858
		} else {
859
			// No synctoken supplied, this is the initial sync.
860
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
861
			$stmt = $this->db->prepare($query);
862
			$stmt->execute([$addressBookId]);
863
864
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
865
		}
866
		return $result;
867
	}
868
869
	/**
870
	 * Adds a change record to the addressbookchanges table.
871
	 *
872
	 * @param mixed $addressBookId
873
	 * @param string $objectUri
874
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
875
	 * @return void
876
	 */
877
	protected function addChange($addressBookId, $objectUri, $operation) {
878
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
879
		$stmt = $this->db->prepare($sql);
880
		$stmt->execute([
881
			$objectUri,
882
			$addressBookId,
883
			$operation,
884
			$addressBookId
885
		]);
886
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
887
		$stmt->execute([
888
			$addressBookId
889
		]);
890
	}
891
892
	/**
893
	 * @param resource|string $cardData
894
	 * @param bool $modified
895
	 * @return string
896
	 */
897
	private function readBlob($cardData, &$modified=false) {
898
		if (is_resource($cardData)) {
899
			$cardData = stream_get_contents($cardData);
900
		}
901
902
		$cardDataArray = explode("\r\n", $cardData);
903
904
		$cardDataFiltered = [];
905
		$removingPhoto = false;
906
		foreach ($cardDataArray as $line) {
907
			if (strpos($line, 'PHOTO:data:') === 0
908
				&& strpos($line, 'PHOTO:data:image/') !== 0) {
909
				// Filter out PHOTO data of non-images
910
				$removingPhoto = true;
911
				$modified = true;
912
				continue;
913
			}
914
915
			if ($removingPhoto) {
916
				if (strpos($line, ' ') === 0) {
917
					continue;
918
				}
919
				// No leading space means this is a new property
920
				$removingPhoto = false;
921
			}
922
923
			$cardDataFiltered[] = $line;
924
		}
925
926
		return implode("\r\n", $cardDataFiltered);
927
	}
928
929
	/**
930
	 * @param IShareable $shareable
931
	 * @param string[] $add
932
	 * @param string[] $remove
933
	 */
934
	public function updateShares(IShareable $shareable, $add, $remove) {
935
		$this->sharingBackend->updateShares($shareable, $add, $remove);
936
	}
937
938
	/**
939
	 * search contact
940
	 *
941
	 * @param int $addressBookId
942
	 * @param string $pattern which should match within the $searchProperties
943
	 * @param array $searchProperties defines the properties within the query pattern should match
944
	 * @param array $options = array() to define the search behavior
945
	 * 	- 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
946
	 * @return array an array of contacts which are arrays of key-value-pairs
947
	 */
948
	public function search($addressBookId, $pattern, $searchProperties, $options = []) {
949
		$query = $this->db->getQueryBuilder();
950
		$query2 = $this->db->getQueryBuilder();
951
952
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
953
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
954
		$or = $query2->expr()->orX();
955
		foreach ($searchProperties as $property) {
956
			$or->add($query2->expr()->eq('cp.name', $query->createNamedParameter($property)));
957
		}
958
		$query2->andWhere($or);
959
960
		// No need for like when the pattern is empty
961
		if ('' !== $pattern) {
962
			if (\array_key_exists('escape_like_param', $options) && $options['escape_like_param'] === false) {
963
				$query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter($pattern)));
964
			} else {
965
				$query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
966
			}
967
		}
968
969
		$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
970
			->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
971
972
		$result = $query->execute();
973
		$cards = $result->fetchAll();
974
975
		$result->closeCursor();
976
977
		return array_map(function ($array) {
978
			$modified = false;
979
			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
980
			if ($modified) {
981
				$array['size'] = strlen($array['carddata']);
982
			}
983
			return $array;
984
		}, $cards);
985
	}
986
987
	/**
988
	 * @param int $bookId
989
	 * @param string $name
990
	 * @return array
991
	 */
992
	public function collectCardProperties($bookId, $name) {
993
		$query = $this->db->getQueryBuilder();
994
		$result = $query->selectDistinct('value')
995
			->from($this->dbCardsPropertiesTable)
996
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
997
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
998
			->execute();
999
1000
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
1001
		$result->closeCursor();
1002
1003
		return $all;
1004
	}
1005
1006
	/**
1007
	 * get URI from a given contact
1008
	 *
1009
	 * @param int $id
1010
	 * @return string
1011
	 */
1012
	public function getCardUri($id) {
1013
		$query = $this->db->getQueryBuilder();
1014
		$query->select('uri')->from($this->dbCardsTable)
1015
				->where($query->expr()->eq('id', $query->createParameter('id')))
1016
				->setParameter('id', $id);
1017
1018
		$result = $query->execute();
1019
		$uri = $result->fetch();
1020
		$result->closeCursor();
1021
1022
		if (!isset($uri['uri'])) {
1023
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1024
		}
1025
1026
		return $uri['uri'];
1027
	}
1028
1029
	/**
1030
	 * return contact with the given URI
1031
	 *
1032
	 * @param int $addressBookId
1033
	 * @param string $uri
1034
	 * @returns array
1035
	 */
1036
	public function getContact($addressBookId, $uri) {
1037
		$result = [];
1038
		$query = $this->db->getQueryBuilder();
1039
		$query->select('*')->from($this->dbCardsTable)
1040
				->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1041
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1042
		$queryResult = $query->execute();
1043
		$contact = $queryResult->fetch();
1044
		$queryResult->closeCursor();
1045
1046
		if (is_array($contact)) {
1047
			$modified = false;
1048
			$contact['etag'] = '"' . $contact['etag'] . '"';
1049
			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1050
			if ($modified) {
1051
				$contact['size'] = strlen($contact['carddata']);
1052
			}
1053
1054
			$result = $contact;
1055
		}
1056
1057
		return $result;
1058
	}
1059
1060
	/**
1061
	 * Returns the list of people whom this address book is shared with.
1062
	 *
1063
	 * Every element in this array should have the following properties:
1064
	 *   * href - Often a mailto: address
1065
	 *   * commonName - Optional, for example a first + last name
1066
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1067
	 *   * readOnly - boolean
1068
	 *   * summary - Optional, a description for the share
1069
	 *
1070
	 * @return array
1071
	 */
1072
	public function getShares($addressBookId) {
1073
		return $this->sharingBackend->getShares($addressBookId);
1074
	}
1075
1076
	/**
1077
	 * update properties table
1078
	 *
1079
	 * @param int $addressBookId
1080
	 * @param string $cardUri
1081
	 * @param string $vCardSerialized
1082
	 */
1083
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1084
		$cardId = $this->getCardId($addressBookId, $cardUri);
1085
		$vCard = $this->readCard($vCardSerialized);
1086
1087
		$this->purgeProperties($addressBookId, $cardId);
1088
1089
		$query = $this->db->getQueryBuilder();
1090
		$query->insert($this->dbCardsPropertiesTable)
1091
			->values(
1092
				[
1093
					'addressbookid' => $query->createNamedParameter($addressBookId),
1094
					'cardid' => $query->createNamedParameter($cardId),
1095
					'name' => $query->createParameter('name'),
1096
					'value' => $query->createParameter('value'),
1097
					'preferred' => $query->createParameter('preferred')
1098
				]
1099
			);
1100
1101
		foreach ($vCard->children() as $property) {
1102
			if (!in_array($property->name, self::$indexProperties)) {
1103
				continue;
1104
			}
1105
			$preferred = 0;
1106
			foreach ($property->parameters as $parameter) {
1107
				if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1108
					$preferred = 1;
1109
					break;
1110
				}
1111
			}
1112
			$query->setParameter('name', $property->name);
1113
			$query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1114
			$query->setParameter('preferred', $preferred);
1115
			$query->execute();
1116
		}
1117
	}
1118
1119
	/**
1120
	 * read vCard data into a vCard object
1121
	 *
1122
	 * @param string $cardData
1123
	 * @return VCard
1124
	 */
1125
	protected function readCard($cardData) {
1126
		return  Reader::read($cardData);
1127
	}
1128
1129
	/**
1130
	 * delete all properties from a given card
1131
	 *
1132
	 * @param int $addressBookId
1133
	 * @param int $cardId
1134
	 */
1135
	protected function purgeProperties($addressBookId, $cardId) {
1136
		$query = $this->db->getQueryBuilder();
1137
		$query->delete($this->dbCardsPropertiesTable)
1138
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1139
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1140
		$query->execute();
1141
	}
1142
1143
	/**
1144
	 * get ID from a given contact
1145
	 *
1146
	 * @param int $addressBookId
1147
	 * @param string $uri
1148
	 * @return int
1149
	 */
1150
	protected function getCardId($addressBookId, $uri) {
1151
		$query = $this->db->getQueryBuilder();
1152
		$query->select('id')->from($this->dbCardsTable)
1153
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1154
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1155
1156
		$result = $query->execute();
1157
		$cardIds = $result->fetch();
1158
		$result->closeCursor();
1159
1160
		if (!isset($cardIds['id'])) {
1161
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1162
		}
1163
1164
		return (int)$cardIds['id'];
1165
	}
1166
1167
	/**
1168
	 * For shared address books the sharee is set in the ACL of the address book
1169
	 * @param $addressBookId
1170
	 * @param $acl
1171
	 * @return array
1172
	 */
1173
	public function applyShareAcl($addressBookId, $acl) {
1174
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1175
	}
1176
1177
	private function convertPrincipal($principalUri, $toV2) {
1178
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1179
			list(, $name) = \Sabre\Uri\split($principalUri);
1180
			if ($toV2 === true) {
1181
				return "principals/users/$name";
1182
			}
1183
			return "principals/$name";
1184
		}
1185
		return $principalUri;
1186
	}
1187
1188
	private function addOwnerPrincipal(&$addressbookInfo) {
1189
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1190
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1191
		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1192
			$uri = $addressbookInfo[$ownerPrincipalKey];
1193
		} else {
1194
			$uri = $addressbookInfo['principaluri'];
1195
		}
1196
1197
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1198
		if (isset($principalInformation['{DAV:}displayname'])) {
1199
			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1200
		}
1201
	}
1202
1203
	/**
1204
	 * Extract UID from vcard
1205
	 *
1206
	 * @param string $cardData the vcard raw data
1207
	 * @return string the uid
1208
	 * @throws BadRequest if no UID is available
1209
	 */
1210
	private function getUID($cardData) {
1211
		if ($cardData != '') {
1212
			$vCard = Reader::read($cardData);
1213
			if ($vCard->UID) {
1214
				$uid = $vCard->UID->getValue();
1215
				return $uid;
1216
			}
1217
			// should already be handled, but just in case
1218
			throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1219
		}
1220
		// should already be handled, but just in case
1221
		throw new BadRequest('vCard can not be empty');
1222
	}
1223
}
1224