Passed
Push — master ( 927130...9d89f8 )
by Roeland
35:01 queued 22:12
created

CardDavBackend::getUID()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Georg Ehrke <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author John Molakvoæ (skjnldsv) <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Stefan Weil <[email protected]>
15
 * @author Thomas Citharel <[email protected]>
16
 * @author Thomas Müller <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OCA\DAV\CardDAV;
35
36
use OCA\DAV\Connector\Sabre\Principal;
37
use OCP\DB\QueryBuilder\IQueryBuilder;
38
use OCA\DAV\DAV\Sharing\Backend;
39
use OCA\DAV\DAV\Sharing\IShareable;
40
use OCP\IDBConnection;
41
use OCP\IGroupManager;
42
use OCP\IUser;
43
use OCP\IUserManager;
44
use PDO;
45
use Sabre\CardDAV\Backend\BackendInterface;
46
use Sabre\CardDAV\Backend\SyncSupport;
47
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...
48
use Sabre\DAV\Exception\BadRequest;
49
use Sabre\VObject\Component\VCard;
50
use Sabre\VObject\Reader;
51
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
52
use Symfony\Component\EventDispatcher\GenericEvent;
53
54
class CardDavBackend implements BackendInterface, SyncSupport {
55
56
	const PERSONAL_ADDRESSBOOK_URI = 'contacts';
57
	const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
58
59
	/** @var Principal */
60
	private $principalBackend;
61
62
	/** @var string */
63
	private $dbCardsTable = 'cards';
64
65
	/** @var string */
66
	private $dbCardsPropertiesTable = 'cards_properties';
67
68
	/** @var IDBConnection */
69
	private $db;
70
71
	/** @var Backend */
72
	private $sharingBackend;
73
74
	/** @var array properties to index */
75
	public static $indexProperties = array(
76
			'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
77
			'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD');
78
79
	/**
80
	 * @var string[] Map of uid => display name
81
	 */
82
	protected $userDisplayNames;
83
84
	/** @var IUserManager */
85
	private $userManager;
86
87
	/** @var EventDispatcherInterface */
88
	private $dispatcher;
89
90
	/**
91
	 * CardDavBackend constructor.
92
	 *
93
	 * @param IDBConnection $db
94
	 * @param Principal $principalBackend
95
	 * @param IUserManager $userManager
96
	 * @param IGroupManager $groupManager
97
	 * @param EventDispatcherInterface $dispatcher
98
	 */
99
	public function __construct(IDBConnection $db,
100
								Principal $principalBackend,
101
								IUserManager $userManager,
102
								IGroupManager $groupManager,
103
								EventDispatcherInterface $dispatcher) {
104
		$this->db = $db;
105
		$this->principalBackend = $principalBackend;
106
		$this->userManager = $userManager;
107
		$this->dispatcher = $dispatcher;
108
		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
109
	}
110
111
	/**
112
	 * Return the number of address books for a principal
113
	 *
114
	 * @param $principalUri
115
	 * @return int
116
	 */
117
	public function getAddressBooksForUserCount($principalUri) {
118
		$principalUri = $this->convertPrincipal($principalUri, true);
119
		$query = $this->db->getQueryBuilder();
120
		$query->select($query->createFunction('COUNT(*)'))
121
			->from('addressbooks')
122
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
123
124
		return (int)$query->execute()->fetchColumn();
125
	}
126
127
	/**
128
	 * Returns the list of address books for a specific user.
129
	 *
130
	 * Every addressbook should have the following properties:
131
	 *   id - an arbitrary unique id
132
	 *   uri - the 'basename' part of the url
133
	 *   principaluri - Same as the passed parameter
134
	 *
135
	 * Any additional clark-notation property may be passed besides this. Some
136
	 * common ones are :
137
	 *   {DAV:}displayname
138
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
139
	 *   {http://calendarserver.org/ns/}getctag
140
	 *
141
	 * @param string $principalUri
142
	 * @return array
143
	 */
144
	function getAddressBooksForUser($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
145
		$principalUriOriginal = $principalUri;
146
		$principalUri = $this->convertPrincipal($principalUri, true);
147
		$query = $this->db->getQueryBuilder();
148
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
149
			->from('addressbooks')
150
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
151
152
		$addressBooks = [];
153
154
		$result = $query->execute();
155
		while($row = $result->fetch()) {
156
			$addressBooks[$row['id']] = [
157
				'id'  => $row['id'],
158
				'uri' => $row['uri'],
159
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
160
				'{DAV:}displayname' => $row['displayname'],
161
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
162
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
163
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
164
			];
165
166
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
167
		}
168
		$result->closeCursor();
169
170
		// query for shared calendars
171
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
172
		$principals = array_map(function($principal) {
173
			return urldecode($principal);
174
		}, $principals);
175
		$principals[]= $principalUri;
176
177
		$query = $this->db->getQueryBuilder();
178
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
179
			->from('dav_shares', 's')
180
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
181
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
182
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
183
			->setParameter('type', 'addressbook')
184
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
185
			->execute();
186
187
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
188
		while($row = $result->fetch()) {
189
			if ($row['principaluri'] === $principalUri) {
190
				continue;
191
			}
192
193
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
194
			if (isset($addressBooks[$row['id']])) {
195
				if ($readOnly) {
196
					// New share can not have more permissions then the old one.
197
					continue;
198
				}
199
				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
200
					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
201
					// Old share is already read-write, no more permissions can be gained
202
					continue;
203
				}
204
			}
205
206
			list(, $name) = \Sabre\Uri\split($row['principaluri']);
207
			$uri = $row['uri'] . '_shared_by_' . $name;
208
			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
209
210
			$addressBooks[$row['id']] = [
211
				'id'  => $row['id'],
212
				'uri' => $uri,
213
				'principaluri' => $principalUriOriginal,
214
				'{DAV:}displayname' => $displayName,
215
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
216
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
217
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
218
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
219
				$readOnlyPropertyName => $readOnly,
220
			];
221
222
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
223
		}
224
		$result->closeCursor();
225
226
		return array_values($addressBooks);
227
	}
228
229
	public function getUsersOwnAddressBooks($principalUri) {
230
		$principalUri = $this->convertPrincipal($principalUri, true);
231
		$query = $this->db->getQueryBuilder();
232
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
233
			  ->from('addressbooks')
234
			  ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
235
236
		$addressBooks = [];
237
238
		$result = $query->execute();
239
		while($row = $result->fetch()) {
240
			$addressBooks[$row['id']] = [
241
				'id'  => $row['id'],
242
				'uri' => $row['uri'],
243
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
244
				'{DAV:}displayname' => $row['displayname'],
245
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
246
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
247
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
248
			];
249
250
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
251
		}
252
		$result->closeCursor();
253
254
		return array_values($addressBooks);
255
	}
256
257
	private function getUserDisplayName($uid) {
258
		if (!isset($this->userDisplayNames[$uid])) {
259
			$user = $this->userManager->get($uid);
260
261
			if ($user instanceof IUser) {
262
				$this->userDisplayNames[$uid] = $user->getDisplayName();
263
			} else {
264
				$this->userDisplayNames[$uid] = $uid;
265
			}
266
		}
267
268
		return $this->userDisplayNames[$uid];
269
	}
270
271
	/**
272
	 * @param int $addressBookId
273
	 */
274
	public function getAddressBookById($addressBookId) {
275
		$query = $this->db->getQueryBuilder();
276
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
277
			->from('addressbooks')
278
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
279
			->execute();
280
281
		$row = $result->fetch();
282
		$result->closeCursor();
283
		if ($row === false) {
284
			return null;
285
		}
286
287
		$addressBook = [
288
			'id'  => $row['id'],
289
			'uri' => $row['uri'],
290
			'principaluri' => $row['principaluri'],
291
			'{DAV:}displayname' => $row['displayname'],
292
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
293
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
294
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
295
		];
296
297
		$this->addOwnerPrincipal($addressBook);
298
299
		return $addressBook;
300
	}
301
302
	/**
303
	 * @param $addressBookUri
304
	 * @return array|null
305
	 */
306
	public function getAddressBooksByUri($principal, $addressBookUri) {
307
		$query = $this->db->getQueryBuilder();
308
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
309
			->from('addressbooks')
310
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
311
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
312
			->setMaxResults(1)
313
			->execute();
314
315
		$row = $result->fetch();
316
		$result->closeCursor();
317
		if ($row === false) {
318
			return null;
319
		}
320
321
		$addressBook = [
322
			'id'  => $row['id'],
323
			'uri' => $row['uri'],
324
			'principaluri' => $row['principaluri'],
325
			'{DAV:}displayname' => $row['displayname'],
326
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
327
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
328
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
329
		];
330
331
		$this->addOwnerPrincipal($addressBook);
332
333
		return $addressBook;
334
	}
335
336
	/**
337
	 * Updates properties for an address book.
338
	 *
339
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
340
	 * To do the actual updates, you must tell this object which properties
341
	 * you're going to process with the handle() method.
342
	 *
343
	 * Calling the handle method is like telling the PropPatch object "I
344
	 * promise I can handle updating this property".
345
	 *
346
	 * Read the PropPatch documentation for more info and examples.
347
	 *
348
	 * @param string $addressBookId
349
	 * @param \Sabre\DAV\PropPatch $propPatch
350
	 * @return void
351
	 */
352
	function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
353
		$supportedProperties = [
354
			'{DAV:}displayname',
355
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
356
		];
357
358
		/**
359
		 * @suppress SqlInjectionChecker
360
		 */
361
		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
362
363
			$updates = [];
364
			foreach($mutations as $property=>$newValue) {
365
366
				switch($property) {
367
					case '{DAV:}displayname' :
368
						$updates['displayname'] = $newValue;
369
						break;
370
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
371
						$updates['description'] = $newValue;
372
						break;
373
				}
374
			}
375
			$query = $this->db->getQueryBuilder();
376
			$query->update('addressbooks');
377
378
			foreach($updates as $key=>$value) {
379
				$query->set($key, $query->createNamedParameter($value));
380
			}
381
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
382
			->execute();
383
384
			$this->addChange($addressBookId, "", 2);
385
386
			return true;
387
388
		});
389
	}
390
391
	/**
392
	 * Creates a new address book
393
	 *
394
	 * @param string $principalUri
395
	 * @param string $url Just the 'basename' of the url.
396
	 * @param array $properties
397
	 * @return int
398
	 * @throws BadRequest
399
	 */
400
	function createAddressBook($principalUri, $url, array $properties) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
401
		$values = [
402
			'displayname' => null,
403
			'description' => null,
404
			'principaluri' => $principalUri,
405
			'uri' => $url,
406
			'synctoken' => 1
407
		];
408
409
		foreach($properties as $property=>$newValue) {
410
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
424
		// Fallback to make sure the displayname is set. Some clients may refuse
425
		// to work with addressbooks not having a displayname.
426
		if(is_null($values['displayname'])) {
427
			$values['displayname'] = $url;
428
		}
429
430
		$query = $this->db->getQueryBuilder();
431
		$query->insert('addressbooks')
432
			->values([
433
				'uri' => $query->createParameter('uri'),
434
				'displayname' => $query->createParameter('displayname'),
435
				'description' => $query->createParameter('description'),
436
				'principaluri' => $query->createParameter('principaluri'),
437
				'synctoken' => $query->createParameter('synctoken'),
438
			])
439
			->setParameters($values)
440
			->execute();
441
442
		return $query->getLastInsertId();
443
	}
444
445
	/**
446
	 * Deletes an entire addressbook and all its contents
447
	 *
448
	 * @param mixed $addressBookId
449
	 * @return void
450
	 */
451
	function deleteAddressBook($addressBookId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
452
		$query = $this->db->getQueryBuilder();
453
		$query->delete('cards')
454
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
455
			->setParameter('addressbookid', $addressBookId)
456
			->execute();
457
458
		$query->delete('addressbookchanges')
459
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
460
			->setParameter('addressbookid', $addressBookId)
461
			->execute();
462
463
		$query->delete('addressbooks')
464
			->where($query->expr()->eq('id', $query->createParameter('id')))
465
			->setParameter('id', $addressBookId)
466
			->execute();
467
468
		$this->sharingBackend->deleteAllShares($addressBookId);
469
470
		$query->delete($this->dbCardsPropertiesTable)
471
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
472
			->execute();
473
474
	}
475
476
	/**
477
	 * Returns all cards for a specific addressbook id.
478
	 *
479
	 * This method should return the following properties for each card:
480
	 *   * carddata - raw vcard data
481
	 *   * uri - Some unique url
482
	 *   * lastmodified - A unix timestamp
483
	 *
484
	 * It's recommended to also return the following properties:
485
	 *   * etag - A unique etag. This must change every time the card changes.
486
	 *   * size - The size of the card in bytes.
487
	 *
488
	 * If these last two properties are provided, less time will be spent
489
	 * calculating them. If they are specified, you can also ommit carddata.
490
	 * This may speed up certain requests, especially with large cards.
491
	 *
492
	 * @param mixed $addressBookId
493
	 * @return array
494
	 */
495
	function getCards($addressBookId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
496
		$query = $this->db->getQueryBuilder();
497
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
498
			->from('cards')
499
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
500
501
		$cards = [];
502
503
		$result = $query->execute();
504
		while($row = $result->fetch()) {
505
			$row['etag'] = '"' . $row['etag'] . '"';
506
			$row['carddata'] = $this->readBlob($row['carddata']);
507
			$cards[] = $row;
508
		}
509
		$result->closeCursor();
510
511
		return $cards;
512
	}
513
514
	/**
515
	 * Returns a specific card.
516
	 *
517
	 * The same set of properties must be returned as with getCards. The only
518
	 * exception is that 'carddata' is absolutely required.
519
	 *
520
	 * If the card does not exist, you must return false.
521
	 *
522
	 * @param mixed $addressBookId
523
	 * @param string $cardUri
524
	 * @return array
525
	 */
526
	function getCard($addressBookId, $cardUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
527
		$query = $this->db->getQueryBuilder();
528
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
529
			->from('cards')
530
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
531
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
532
			->setMaxResults(1);
533
534
		$result = $query->execute();
535
		$row = $result->fetch();
536
		if (!$row) {
537
			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...
538
		}
539
		$row['etag'] = '"' . $row['etag'] . '"';
540
		$row['carddata'] = $this->readBlob($row['carddata']);
541
542
		return $row;
543
	}
544
545
	/**
546
	 * Returns a list of cards.
547
	 *
548
	 * This method should work identical to getCard, but instead return all the
549
	 * cards in the list as an array.
550
	 *
551
	 * If the backend supports this, it may allow for some speed-ups.
552
	 *
553
	 * @param mixed $addressBookId
554
	 * @param string[] $uris
555
	 * @return array
556
	 */
557
	function getMultipleCards($addressBookId, array $uris) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
558
		if (empty($uris)) {
559
			return [];
560
		}
561
562
		$chunks = array_chunk($uris, 100);
563
		$cards = [];
564
565
		$query = $this->db->getQueryBuilder();
566
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
567
			->from('cards')
568
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
569
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
570
571
		foreach ($chunks as $uris) {
572
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
573
			$result = $query->execute();
574
575
			while ($row = $result->fetch()) {
576
				$row['etag'] = '"' . $row['etag'] . '"';
577
				$row['carddata'] = $this->readBlob($row['carddata']);
578
				$cards[] = $row;
579
			}
580
			$result->closeCursor();
581
		}
582
		return $cards;
583
	}
584
585
	/**
586
	 * Creates a new card.
587
	 *
588
	 * The addressbook id will be passed as the first argument. This is the
589
	 * same id as it is returned from the getAddressBooksForUser method.
590
	 *
591
	 * The cardUri is a base uri, and doesn't include the full path. The
592
	 * cardData argument is the vcard body, and is passed as a string.
593
	 *
594
	 * It is possible to return an ETag from this method. This ETag is for the
595
	 * newly created resource, and must be enclosed with double quotes (that
596
	 * is, the string itself must contain the double quotes).
597
	 *
598
	 * You should only return the ETag if you store the carddata as-is. If a
599
	 * subsequent GET request on the same card does not have the same body,
600
	 * byte-by-byte and you did return an ETag here, clients tend to get
601
	 * confused.
602
	 *
603
	 * If you don't return an ETag, you can just return null.
604
	 *
605
	 * @param mixed $addressBookId
606
	 * @param string $cardUri
607
	 * @param string $cardData
608
	 * @return string
609
	 */
610
	function createCard($addressBookId, $cardUri, $cardData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
611
		$etag = md5($cardData);
612
		$uid = $this->getUID($cardData);
613
614
		$query = $this->db->getQueryBuilder();
615
		$query->insert('cards')
616
			->values([
617
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
618
				'uri' => $query->createNamedParameter($cardUri),
619
				'lastmodified' => $query->createNamedParameter(time()),
620
				'addressbookid' => $query->createNamedParameter($addressBookId),
621
				'size' => $query->createNamedParameter(strlen($cardData)),
622
				'etag' => $query->createNamedParameter($etag),
623
				'uid' => $query->createNamedParameter($uid),
624
			])
625
			->execute();
626
627
		$this->addChange($addressBookId, $cardUri, 1);
628
		$this->updateProperties($addressBookId, $cardUri, $cardData);
629
630
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
631
			new GenericEvent(null, [
632
				'addressBookId' => $addressBookId,
633
				'cardUri' => $cardUri,
634
				'cardData' => $cardData]));
635
636
		return '"' . $etag . '"';
637
	}
638
639
	/**
640
	 * Updates a card.
641
	 *
642
	 * The addressbook id will be passed as the first argument. This is the
643
	 * same id as it is returned from the getAddressBooksForUser method.
644
	 *
645
	 * The cardUri is a base uri, and doesn't include the full path. The
646
	 * cardData argument is the vcard body, and is passed as a string.
647
	 *
648
	 * It is possible to return an ETag from this method. This ETag should
649
	 * match that of the updated resource, and must be enclosed with double
650
	 * quotes (that is: the string itself must contain the actual quotes).
651
	 *
652
	 * You should only return the ETag if you store the carddata as-is. If a
653
	 * subsequent GET request on the same card does not have the same body,
654
	 * byte-by-byte and you did return an ETag here, clients tend to get
655
	 * confused.
656
	 *
657
	 * If you don't return an ETag, you can just return null.
658
	 *
659
	 * @param mixed $addressBookId
660
	 * @param string $cardUri
661
	 * @param string $cardData
662
	 * @return string
663
	 */
664
	function updateCard($addressBookId, $cardUri, $cardData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
665
666
		$uid = $this->getUID($cardData);
667
		$etag = md5($cardData);
668
		$query = $this->db->getQueryBuilder();
669
		$query->update('cards')
670
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
671
			->set('lastmodified', $query->createNamedParameter(time()))
672
			->set('size', $query->createNamedParameter(strlen($cardData)))
673
			->set('etag', $query->createNamedParameter($etag))
674
			->set('uid', $query->createNamedParameter($uid))
675
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
676
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
677
			->execute();
678
679
		$this->addChange($addressBookId, $cardUri, 2);
680
		$this->updateProperties($addressBookId, $cardUri, $cardData);
681
682
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
683
			new GenericEvent(null, [
684
				'addressBookId' => $addressBookId,
685
				'cardUri' => $cardUri,
686
				'cardData' => $cardData]));
687
688
		return '"' . $etag . '"';
689
	}
690
691
	/**
692
	 * Deletes a card
693
	 *
694
	 * @param mixed $addressBookId
695
	 * @param string $cardUri
696
	 * @return bool
697
	 */
698
	function deleteCard($addressBookId, $cardUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
699
		try {
700
			$cardId = $this->getCardId($addressBookId, $cardUri);
701
		} catch (\InvalidArgumentException $e) {
702
			$cardId = null;
703
		}
704
		$query = $this->db->getQueryBuilder();
705
		$ret = $query->delete('cards')
706
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
707
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
708
			->execute();
709
710
		$this->addChange($addressBookId, $cardUri, 3);
711
712
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
713
			new GenericEvent(null, [
714
				'addressBookId' => $addressBookId,
715
				'cardUri' => $cardUri]));
716
717
		if ($ret === 1) {
718
			if ($cardId !== null) {
719
				$this->purgeProperties($addressBookId, $cardId);
720
			}
721
			return true;
722
		}
723
724
		return false;
725
	}
726
727
	/**
728
	 * The getChanges method returns all the changes that have happened, since
729
	 * the specified syncToken in the specified address book.
730
	 *
731
	 * This function should return an array, such as the following:
732
	 *
733
	 * [
734
	 *   'syncToken' => 'The current synctoken',
735
	 *   'added'   => [
736
	 *      'new.txt',
737
	 *   ],
738
	 *   'modified'   => [
739
	 *      'modified.txt',
740
	 *   ],
741
	 *   'deleted' => [
742
	 *      'foo.php.bak',
743
	 *      'old.txt'
744
	 *   ]
745
	 * ];
746
	 *
747
	 * The returned syncToken property should reflect the *current* syncToken
748
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
749
	 * property. This is needed here too, to ensure the operation is atomic.
750
	 *
751
	 * If the $syncToken argument is specified as null, this is an initial
752
	 * sync, and all members should be reported.
753
	 *
754
	 * The modified property is an array of nodenames that have changed since
755
	 * the last token.
756
	 *
757
	 * The deleted property is an array with nodenames, that have been deleted
758
	 * from collection.
759
	 *
760
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
761
	 * 1, you only have to report changes that happened only directly in
762
	 * immediate descendants. If it's 2, it should also include changes from
763
	 * the nodes below the child collections. (grandchildren)
764
	 *
765
	 * The $limit argument allows a client to specify how many results should
766
	 * be returned at most. If the limit is not specified, it should be treated
767
	 * as infinite.
768
	 *
769
	 * If the limit (infinite or not) is higher than you're willing to return,
770
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
771
	 *
772
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
773
	 * return null.
774
	 *
775
	 * The limit is 'suggestive'. You are free to ignore it.
776
	 *
777
	 * @param string $addressBookId
778
	 * @param string $syncToken
779
	 * @param int $syncLevel
780
	 * @param int $limit
781
	 * @return array
782
	 */
783
	function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
784
		// Current synctoken
785
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
786
		$stmt->execute([ $addressBookId ]);
787
		$currentToken = $stmt->fetchColumn(0);
788
789
		if (is_null($currentToken)) return null;
0 ignored issues
show
introduced by
The condition is_null($currentToken) is always false.
Loading history...
790
791
		$result = [
792
			'syncToken' => $currentToken,
793
			'added'     => [],
794
			'modified'  => [],
795
			'deleted'   => [],
796
		];
797
798
		if ($syncToken) {
799
800
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
801
			if ($limit>0) {
802
				$query .= " `LIMIT` " . (int)$limit;
803
			}
804
805
			// Fetching all changes
806
			$stmt = $this->db->prepare($query);
807
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
808
809
			$changes = [];
810
811
			// This loop ensures that any duplicates are overwritten, only the
812
			// last change on a node is relevant.
813
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
814
815
				$changes[$row['uri']] = $row['operation'];
816
817
			}
818
819
			foreach($changes as $uri => $operation) {
820
821
				switch($operation) {
822
					case 1:
823
						$result['added'][] = $uri;
824
						break;
825
					case 2:
826
						$result['modified'][] = $uri;
827
						break;
828
					case 3:
829
						$result['deleted'][] = $uri;
830
						break;
831
				}
832
833
			}
834
		} else {
835
			// No synctoken supplied, this is the initial sync.
836
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
837
			$stmt = $this->db->prepare($query);
838
			$stmt->execute([$addressBookId]);
839
840
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
841
		}
842
		return $result;
843
	}
844
845
	/**
846
	 * Adds a change record to the addressbookchanges table.
847
	 *
848
	 * @param mixed $addressBookId
849
	 * @param string $objectUri
850
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
851
	 * @return void
852
	 */
853
	protected function addChange($addressBookId, $objectUri, $operation) {
854
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
855
		$stmt = $this->db->prepare($sql);
856
		$stmt->execute([
857
			$objectUri,
858
			$addressBookId,
859
			$operation,
860
			$addressBookId
861
		]);
862
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
863
		$stmt->execute([
864
			$addressBookId
865
		]);
866
	}
867
868
	private function readBlob($cardData) {
869
		if (is_resource($cardData)) {
870
			return stream_get_contents($cardData);
871
		}
872
873
		return $cardData;
874
	}
875
876
	/**
877
	 * @param IShareable $shareable
878
	 * @param string[] $add
879
	 * @param string[] $remove
880
	 */
881
	public function updateShares(IShareable $shareable, $add, $remove) {
882
		$this->sharingBackend->updateShares($shareable, $add, $remove);
883
	}
884
885
	/**
886
	 * search contact
887
	 *
888
	 * @param int $addressBookId
889
	 * @param string $pattern which should match within the $searchProperties
890
	 * @param array $searchProperties defines the properties within the query pattern should match
891
	 * @return array an array of contacts which are arrays of key-value-pairs
892
	 */
893
	public function search($addressBookId, $pattern, $searchProperties) {
894
		$query = $this->db->getQueryBuilder();
895
		$query2 = $this->db->getQueryBuilder();
896
897
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
898
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
899
		$or = $query2->expr()->orX();
900
		foreach ($searchProperties as $property) {
901
			$or->add($query2->expr()->eq('cp.name', $query->createNamedParameter($property)));
902
		}
903
		$query2->andWhere($or);
904
905
		// No need for like when the pattern is empty
906
		if ('' !== $pattern) {
907
			$query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
908
		}
909
910
		$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
911
			->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
912
913
		$result = $query->execute();
914
		$cards = $result->fetchAll();
915
916
		$result->closeCursor();
917
918
		return array_map(function($array) {
919
			$array['carddata'] = $this->readBlob($array['carddata']);
920
			return $array;
921
		}, $cards);
922
	}
923
924
	/**
925
	 * @param int $bookId
926
	 * @param string $name
927
	 * @return array
928
	 */
929
	public function collectCardProperties($bookId, $name) {
930
		$query = $this->db->getQueryBuilder();
931
		$result = $query->selectDistinct('value')
932
			->from($this->dbCardsPropertiesTable)
933
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
934
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
935
			->execute();
936
937
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
938
		$result->closeCursor();
939
940
		return $all;
941
	}
942
943
	/**
944
	 * get URI from a given contact
945
	 *
946
	 * @param int $id
947
	 * @return string
948
	 */
949
	public function getCardUri($id) {
950
		$query = $this->db->getQueryBuilder();
951
		$query->select('uri')->from($this->dbCardsTable)
952
				->where($query->expr()->eq('id', $query->createParameter('id')))
953
				->setParameter('id', $id);
954
955
		$result = $query->execute();
956
		$uri = $result->fetch();
957
		$result->closeCursor();
958
959
		if (!isset($uri['uri'])) {
960
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
961
		}
962
963
		return $uri['uri'];
964
	}
965
966
	/**
967
	 * return contact with the given URI
968
	 *
969
	 * @param int $addressBookId
970
	 * @param string $uri
971
	 * @returns array
972
	 */
973
	public function getContact($addressBookId, $uri) {
974
		$result = [];
975
		$query = $this->db->getQueryBuilder();
976
		$query->select('*')->from($this->dbCardsTable)
977
				->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
978
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
979
		$queryResult = $query->execute();
980
		$contact = $queryResult->fetch();
981
		$queryResult->closeCursor();
982
983
		if (is_array($contact)) {
984
			$result = $contact;
985
		}
986
987
		return $result;
988
	}
989
990
	/**
991
	 * Returns the list of people whom this address book is shared with.
992
	 *
993
	 * Every element in this array should have the following properties:
994
	 *   * href - Often a mailto: address
995
	 *   * commonName - Optional, for example a first + last name
996
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
997
	 *   * readOnly - boolean
998
	 *   * summary - Optional, a description for the share
999
	 *
1000
	 * @return array
1001
	 */
1002
	public function getShares($addressBookId) {
1003
		return $this->sharingBackend->getShares($addressBookId);
1004
	}
1005
1006
	/**
1007
	 * update properties table
1008
	 *
1009
	 * @param int $addressBookId
1010
	 * @param string $cardUri
1011
	 * @param string $vCardSerialized
1012
	 */
1013
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1014
		$cardId = $this->getCardId($addressBookId, $cardUri);
1015
		$vCard = $this->readCard($vCardSerialized);
1016
1017
		$this->purgeProperties($addressBookId, $cardId);
1018
1019
		$query = $this->db->getQueryBuilder();
1020
		$query->insert($this->dbCardsPropertiesTable)
1021
			->values(
1022
				[
1023
					'addressbookid' => $query->createNamedParameter($addressBookId),
1024
					'cardid' => $query->createNamedParameter($cardId),
1025
					'name' => $query->createParameter('name'),
1026
					'value' => $query->createParameter('value'),
1027
					'preferred' => $query->createParameter('preferred')
1028
				]
1029
			);
1030
1031
		foreach ($vCard->children() as $property) {
1032
			if(!in_array($property->name, self::$indexProperties)) {
1033
				continue;
1034
			}
1035
			$preferred = 0;
1036
			foreach($property->parameters as $parameter) {
1037
				if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1038
					$preferred = 1;
1039
					break;
1040
				}
1041
			}
1042
			$query->setParameter('name', $property->name);
1043
			$query->setParameter('value', substr($property->getValue(), 0, 254));
1044
			$query->setParameter('preferred', $preferred);
1045
			$query->execute();
1046
		}
1047
	}
1048
1049
	/**
1050
	 * read vCard data into a vCard object
1051
	 *
1052
	 * @param string $cardData
1053
	 * @return VCard
1054
	 */
1055
	protected function readCard($cardData) {
1056
		return  Reader::read($cardData);
1057
	}
1058
1059
	/**
1060
	 * delete all properties from a given card
1061
	 *
1062
	 * @param int $addressBookId
1063
	 * @param int $cardId
1064
	 */
1065
	protected function purgeProperties($addressBookId, $cardId) {
1066
		$query = $this->db->getQueryBuilder();
1067
		$query->delete($this->dbCardsPropertiesTable)
1068
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1069
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1070
		$query->execute();
1071
	}
1072
1073
	/**
1074
	 * get ID from a given contact
1075
	 *
1076
	 * @param int $addressBookId
1077
	 * @param string $uri
1078
	 * @return int
1079
	 */
1080
	protected function getCardId($addressBookId, $uri) {
1081
		$query = $this->db->getQueryBuilder();
1082
		$query->select('id')->from($this->dbCardsTable)
1083
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1084
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1085
1086
		$result = $query->execute();
1087
		$cardIds = $result->fetch();
1088
		$result->closeCursor();
1089
1090
		if (!isset($cardIds['id'])) {
1091
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1092
		}
1093
1094
		return (int)$cardIds['id'];
1095
	}
1096
1097
	/**
1098
	 * For shared address books the sharee is set in the ACL of the address book
1099
	 * @param $addressBookId
1100
	 * @param $acl
1101
	 * @return array
1102
	 */
1103
	public function applyShareAcl($addressBookId, $acl) {
1104
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1105
	}
1106
1107
	private function convertPrincipal($principalUri, $toV2) {
1108
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1109
			list(, $name) = \Sabre\Uri\split($principalUri);
1110
			if ($toV2 === true) {
1111
				return "principals/users/$name";
1112
			}
1113
			return "principals/$name";
1114
		}
1115
		return $principalUri;
1116
	}
1117
1118
	private function addOwnerPrincipal(&$addressbookInfo) {
1119
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1120
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1121
		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1122
			$uri = $addressbookInfo[$ownerPrincipalKey];
1123
		} else {
1124
			$uri = $addressbookInfo['principaluri'];
1125
		}
1126
1127
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1128
		if (isset($principalInformation['{DAV:}displayname'])) {
1129
			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1130
		}
1131
	}
1132
1133
	/**
1134
	 * Extract UID from vcard
1135
	 *
1136
	 * @param string $cardData the vcard raw data
1137
	 * @return string the uid
1138
	 * @throws BadRequest if no UID is available
1139
	 */
1140
	private function getUID($cardData) {
1141
		if ($cardData != '') {
1142
			$vCard = Reader::read($cardData);
1143
			if ($vCard->UID) {
1144
				$uid = $vCard->UID->getValue();
1145
				return $uid;
1146
			}
1147
			// should already be handled, but just in case
1148
			throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1149
		}
1150
		// should already be handled, but just in case
1151
		throw new BadRequest('vCard can not be empty');
1152
	}
1153
}
1154