Completed
Push — master ( d842b2...8a1d3c )
by Lukas
17:12
created

CardDavBackend::getAddressBookById()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 20
nc 3
nop 1
dl 0
loc 27
rs 8.8571
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 Stefan Weil <[email protected]>
11
 * @author Thomas Müller <[email protected]>
12
 *
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
namespace OCA\DAV\CardDAV;
30
31
use OCA\DAV\Connector\Sabre\Principal;
32
use OCP\DB\QueryBuilder\IQueryBuilder;
33
use OCA\DAV\DAV\Sharing\Backend;
34
use OCA\DAV\DAV\Sharing\IShareable;
35
use OCP\IDBConnection;
36
use OCP\IUser;
37
use OCP\IUserManager;
38
use PDO;
39
use Sabre\CardDAV\Backend\BackendInterface;
40
use Sabre\CardDAV\Backend\SyncSupport;
41
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.

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...
42
use Sabre\DAV\Exception\BadRequest;
43
use Sabre\HTTP\URLUtil;
44
use Sabre\VObject\Component\VCard;
45
use Sabre\VObject\Reader;
46
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
47
use Symfony\Component\EventDispatcher\GenericEvent;
48
49
class CardDavBackend implements BackendInterface, SyncSupport {
50
51
	const PERSONAL_ADDRESSBOOK_URI = 'contacts';
52
	const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
53
54
	/** @var Principal */
55
	private $principalBackend;
56
57
	/** @var string */
58
	private $dbCardsTable = 'cards';
59
60
	/** @var string */
61
	private $dbCardsPropertiesTable = 'cards_properties';
62
63
	/** @var IDBConnection */
64
	private $db;
65
66
	/** @var Backend */
67
	private $sharingBackend;
68
69
	/** @var array properties to index */
70
	public static $indexProperties = array(
71
			'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
72
			'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD');
73
74
	/**
75
	 * @var string[] Map of uid => display name
76
	 */
77
	protected $userDisplayNames;
78
79
	/** @var IUserManager */
80
	private $userManager;
81
82
	/** @var EventDispatcherInterface */
83
	private $dispatcher;
84
85
	/**
86
	 * CardDavBackend constructor.
87
	 *
88
	 * @param IDBConnection $db
89
	 * @param Principal $principalBackend
90
	 * @param IUserManager $userManager
91
	 * @param EventDispatcherInterface $dispatcher
92
	 */
93
	public function __construct(IDBConnection $db,
94
								Principal $principalBackend,
95
								IUserManager $userManager,
96
								EventDispatcherInterface $dispatcher = null) {
97
		$this->db = $db;
98
		$this->principalBackend = $principalBackend;
99
		$this->userManager = $userManager;
100
		$this->dispatcher = $dispatcher;
101
		$this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook');
102
	}
103
104
	/**
105
	 * Return the number of address books for a principal
106
	 *
107
	 * @param $principalUri
108
	 * @return int
109
	 */
110
	public function getAddressBooksForUserCount($principalUri) {
111
		$principalUri = $this->convertPrincipal($principalUri, true);
112
		$query = $this->db->getQueryBuilder();
113
		$query->select($query->createFunction('COUNT(*)'))
114
			->from('addressbooks')
115
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
116
117
		return (int)$query->execute()->fetchColumn();
118
	}
119
120
	/**
121
	 * Returns the list of address books for a specific user.
122
	 *
123
	 * Every addressbook should have the following properties:
124
	 *   id - an arbitrary unique id
125
	 *   uri - the 'basename' part of the url
126
	 *   principaluri - Same as the passed parameter
127
	 *
128
	 * Any additional clark-notation property may be passed besides this. Some
129
	 * common ones are :
130
	 *   {DAV:}displayname
131
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
132
	 *   {http://calendarserver.org/ns/}getctag
133
	 *
134
	 * @param string $principalUri
135
	 * @return array
136
	 */
137
	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...
138
		$principalUriOriginal = $principalUri;
139
		$principalUri = $this->convertPrincipal($principalUri, true);
140
		$query = $this->db->getQueryBuilder();
141
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
142
			->from('addressbooks')
143
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
144
145
		$addressBooks = [];
146
147
		$result = $query->execute();
148 View Code Duplication
		while($row = $result->fetch()) {
149
			$addressBooks[$row['id']] = [
150
				'id'  => $row['id'],
151
				'uri' => $row['uri'],
152
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
153
				'{DAV:}displayname' => $row['displayname'],
154
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
155
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
156
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
157
			];
158
159
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
160
		}
161
		$result->closeCursor();
162
163
		// query for shared calendars
164
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
165
		$principals[]= $principalUri;
166
167
		$query = $this->db->getQueryBuilder();
168
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
169
			->from('dav_shares', 's')
170
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
171
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
172
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
173
			->setParameter('type', 'addressbook')
174
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
175
			->execute();
176
177
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
178
		while($row = $result->fetch()) {
179
			if ($row['principaluri'] === $principalUri) {
180
				continue;
181
			}
182
183
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
184 View Code Duplication
			if (isset($addressBooks[$row['id']])) {
185
				if ($readOnly) {
186
					// New share can not have more permissions then the old one.
187
					continue;
188
				}
189
				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
190
					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
191
					// Old share is already read-write, no more permissions can be gained
192
					continue;
193
				}
194
			}
195
196
			list(, $name) = URLUtil::splitPath($row['principaluri']);
197
			$uri = $row['uri'] . '_shared_by_' . $name;
198
			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
199
200
			$addressBooks[$row['id']] = [
201
				'id'  => $row['id'],
202
				'uri' => $uri,
203
				'principaluri' => $principalUriOriginal,
204
				'{DAV:}displayname' => $displayName,
205
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
206
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
207
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
208
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
209
				$readOnlyPropertyName => $readOnly,
210
			];
211
212
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
213
		}
214
		$result->closeCursor();
215
216
		return array_values($addressBooks);
217
	}
218
219
	public function getUsersOwnAddressBooks($principalUri) {
220
		$principalUri = $this->convertPrincipal($principalUri, true);
221
		$query = $this->db->getQueryBuilder();
222
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
223
			  ->from('addressbooks')
224
			  ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
225
226
		$addressBooks = [];
227
228
		$result = $query->execute();
229 View Code Duplication
		while($row = $result->fetch()) {
230
			$addressBooks[$row['id']] = [
231
				'id'  => $row['id'],
232
				'uri' => $row['uri'],
233
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
234
				'{DAV:}displayname' => $row['displayname'],
235
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
236
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
237
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
238
			];
239
240
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
241
		}
242
		$result->closeCursor();
243
244
		return array_values($addressBooks);
245
	}
246
247 View Code Duplication
	private function getUserDisplayName($uid) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
248
		if (!isset($this->userDisplayNames[$uid])) {
249
			$user = $this->userManager->get($uid);
250
251
			if ($user instanceof IUser) {
252
				$this->userDisplayNames[$uid] = $user->getDisplayName();
253
			} else {
254
				$this->userDisplayNames[$uid] = $uid;
255
			}
256
		}
257
258
		return $this->userDisplayNames[$uid];
259
	}
260
261
	/**
262
	 * @param int $addressBookId
263
	 */
264
	public function getAddressBookById($addressBookId) {
265
		$query = $this->db->getQueryBuilder();
266
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
267
			->from('addressbooks')
268
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
269
			->execute();
270
271
		$row = $result->fetch();
272
		$result->closeCursor();
273
		if ($row === false) {
274
			return null;
275
		}
276
277
		$addressBook = [
278
			'id'  => $row['id'],
279
			'uri' => $row['uri'],
280
			'principaluri' => $row['principaluri'],
281
			'{DAV:}displayname' => $row['displayname'],
282
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
283
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
284
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
285
		];
286
287
		$this->addOwnerPrincipal($addressBook);
288
289
		return $addressBook;
290
	}
291
292
	/**
293
	 * @param $addressBookUri
294
	 * @return array|null
295
	 */
296
	public function getAddressBooksByUri($principal, $addressBookUri) {
297
		$query = $this->db->getQueryBuilder();
298
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
299
			->from('addressbooks')
300
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
301
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
302
			->setMaxResults(1)
303
			->execute();
304
305
		$row = $result->fetch();
306
		$result->closeCursor();
307
		if ($row === false) {
308
			return null;
309
		}
310
311
		$addressBook = [
312
			'id'  => $row['id'],
313
			'uri' => $row['uri'],
314
			'principaluri' => $row['principaluri'],
315
			'{DAV:}displayname' => $row['displayname'],
316
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
317
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
318
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
319
		];
320
321
		$this->addOwnerPrincipal($addressBook);
322
323
		return $addressBook;
324
	}
325
326
	/**
327
	 * Updates properties for an address book.
328
	 *
329
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
330
	 * To do the actual updates, you must tell this object which properties
331
	 * you're going to process with the handle() method.
332
	 *
333
	 * Calling the handle method is like telling the PropPatch object "I
334
	 * promise I can handle updating this property".
335
	 *
336
	 * Read the PropPatch documentation for more info and examples.
337
	 *
338
	 * @param string $addressBookId
339
	 * @param \Sabre\DAV\PropPatch $propPatch
340
	 * @return void
341
	 */
342
	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...
343
		$supportedProperties = [
344
			'{DAV:}displayname',
345
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
346
		];
347
348
		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
349
350
			$updates = [];
351 View Code Duplication
			foreach($mutations as $property=>$newValue) {
352
353
				switch($property) {
354
					case '{DAV:}displayname' :
355
						$updates['displayname'] = $newValue;
356
						break;
357
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
358
						$updates['description'] = $newValue;
359
						break;
360
				}
361
			}
362
			$query = $this->db->getQueryBuilder();
363
			$query->update('addressbooks');
364
365
			foreach($updates as $key=>$value) {
366
				$query->set($key, $query->createNamedParameter($value));
367
			}
368
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
369
			->execute();
370
371
			$this->addChange($addressBookId, "", 2);
372
373
			return true;
374
375
		});
376
	}
377
378
	/**
379
	 * Creates a new address book
380
	 *
381
	 * @param string $principalUri
382
	 * @param string $url Just the 'basename' of the url.
383
	 * @param array $properties
384
	 * @return int
385
	 * @throws BadRequest
386
	 */
387
	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...
388
		$values = [
389
			'displayname' => null,
390
			'description' => null,
391
			'principaluri' => $principalUri,
392
			'uri' => $url,
393
			'synctoken' => 1
394
		];
395
396 View Code Duplication
		foreach($properties as $property=>$newValue) {
397
398
			switch($property) {
399
				case '{DAV:}displayname' :
400
					$values['displayname'] = $newValue;
401
					break;
402
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
403
					$values['description'] = $newValue;
404
					break;
405
				default :
406
					throw new BadRequest('Unknown property: ' . $property);
407
			}
408
409
		}
410
411
		// Fallback to make sure the displayname is set. Some clients may refuse
412
		// to work with addressbooks not having a displayname.
413
		if(is_null($values['displayname'])) {
414
			$values['displayname'] = $url;
415
		}
416
417
		$query = $this->db->getQueryBuilder();
418
		$query->insert('addressbooks')
419
			->values([
420
				'uri' => $query->createParameter('uri'),
421
				'displayname' => $query->createParameter('displayname'),
422
				'description' => $query->createParameter('description'),
423
				'principaluri' => $query->createParameter('principaluri'),
424
				'synctoken' => $query->createParameter('synctoken'),
425
			])
426
			->setParameters($values)
427
			->execute();
428
429
		return $query->getLastInsertId();
430
	}
431
432
	/**
433
	 * Deletes an entire addressbook and all its contents
434
	 *
435
	 * @param mixed $addressBookId
436
	 * @return void
437
	 */
438
	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...
439
		$query = $this->db->getQueryBuilder();
440
		$query->delete('cards')
441
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
442
			->setParameter('addressbookid', $addressBookId)
443
			->execute();
444
445
		$query->delete('addressbookchanges')
446
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
447
			->setParameter('addressbookid', $addressBookId)
448
			->execute();
449
450
		$query->delete('addressbooks')
451
			->where($query->expr()->eq('id', $query->createParameter('id')))
452
			->setParameter('id', $addressBookId)
453
			->execute();
454
455
		$this->sharingBackend->deleteAllShares($addressBookId);
456
457
		$query->delete($this->dbCardsPropertiesTable)
458
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
459
			->execute();
460
461
	}
462
463
	/**
464
	 * Returns all cards for a specific addressbook id.
465
	 *
466
	 * This method should return the following properties for each card:
467
	 *   * carddata - raw vcard data
468
	 *   * uri - Some unique url
469
	 *   * lastmodified - A unix timestamp
470
	 *
471
	 * It's recommended to also return the following properties:
472
	 *   * etag - A unique etag. This must change every time the card changes.
473
	 *   * size - The size of the card in bytes.
474
	 *
475
	 * If these last two properties are provided, less time will be spent
476
	 * calculating them. If they are specified, you can also ommit carddata.
477
	 * This may speed up certain requests, especially with large cards.
478
	 *
479
	 * @param mixed $addressBookId
480
	 * @return array
481
	 */
482
	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...
483
		$query = $this->db->getQueryBuilder();
484
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
485
			->from('cards')
486
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
487
488
		$cards = [];
489
490
		$result = $query->execute();
491 View Code Duplication
		while($row = $result->fetch()) {
492
			$row['etag'] = '"' . $row['etag'] . '"';
493
			$row['carddata'] = $this->readBlob($row['carddata']);
494
			$cards[] = $row;
495
		}
496
		$result->closeCursor();
497
498
		return $cards;
499
	}
500
501
	/**
502
	 * Returns a specific card.
503
	 *
504
	 * The same set of properties must be returned as with getCards. The only
505
	 * exception is that 'carddata' is absolutely required.
506
	 *
507
	 * If the card does not exist, you must return false.
508
	 *
509
	 * @param mixed $addressBookId
510
	 * @param string $cardUri
511
	 * @return array
512
	 */
513
	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...
514
		$query = $this->db->getQueryBuilder();
515
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
516
			->from('cards')
517
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
518
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
519
			->setMaxResults(1);
520
521
		$result = $query->execute();
522
		$row = $result->fetch();
523
		if (!$row) {
524
			return false;
525
		}
526
		$row['etag'] = '"' . $row['etag'] . '"';
527
		$row['carddata'] = $this->readBlob($row['carddata']);
528
529
		return $row;
530
	}
531
532
	/**
533
	 * Returns a list of cards.
534
	 *
535
	 * This method should work identical to getCard, but instead return all the
536
	 * cards in the list as an array.
537
	 *
538
	 * If the backend supports this, it may allow for some speed-ups.
539
	 *
540
	 * @param mixed $addressBookId
541
	 * @param string[] $uris
542
	 * @return array
543
	 */
544
	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...
545
		if (empty($uris)) {
546
			return [];
547
		}
548
549
		$chunks = array_chunk($uris, 100);
550
		$cards = [];
551
552
		$query = $this->db->getQueryBuilder();
553
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
554
			->from('cards')
555
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
556
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
557
558
		foreach ($chunks as $uris) {
559
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
560
			$result = $query->execute();
561
562 View Code Duplication
			while ($row = $result->fetch()) {
563
				$row['etag'] = '"' . $row['etag'] . '"';
564
				$row['carddata'] = $this->readBlob($row['carddata']);
565
				$cards[] = $row;
566
			}
567
			$result->closeCursor();
568
		}
569
		return $cards;
570
	}
571
572
	/**
573
	 * Creates a new card.
574
	 *
575
	 * The addressbook id will be passed as the first argument. This is the
576
	 * same id as it is returned from the getAddressBooksForUser method.
577
	 *
578
	 * The cardUri is a base uri, and doesn't include the full path. The
579
	 * cardData argument is the vcard body, and is passed as a string.
580
	 *
581
	 * It is possible to return an ETag from this method. This ETag is for the
582
	 * newly created resource, and must be enclosed with double quotes (that
583
	 * is, the string itself must contain the double quotes).
584
	 *
585
	 * You should only return the ETag if you store the carddata as-is. If a
586
	 * subsequent GET request on the same card does not have the same body,
587
	 * byte-by-byte and you did return an ETag here, clients tend to get
588
	 * confused.
589
	 *
590
	 * If you don't return an ETag, you can just return null.
591
	 *
592
	 * @param mixed $addressBookId
593
	 * @param string $cardUri
594
	 * @param string $cardData
595
	 * @return string
596
	 */
597
	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...
598
		$etag = md5($cardData);
599
600
		$query = $this->db->getQueryBuilder();
601
		$query->insert('cards')
602
			->values([
603
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
604
				'uri' => $query->createNamedParameter($cardUri),
605
				'lastmodified' => $query->createNamedParameter(time()),
606
				'addressbookid' => $query->createNamedParameter($addressBookId),
607
				'size' => $query->createNamedParameter(strlen($cardData)),
608
				'etag' => $query->createNamedParameter($etag),
609
			])
610
			->execute();
611
612
		$this->addChange($addressBookId, $cardUri, 1);
613
		$this->updateProperties($addressBookId, $cardUri, $cardData);
614
615 View Code Duplication
		if (!is_null($this->dispatcher)) {
616
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
617
				new GenericEvent(null, [
618
					'addressBookId' => $addressBookId,
619
					'cardUri' => $cardUri,
620
					'cardData' => $cardData]));
621
		}
622
623
		return '"' . $etag . '"';
624
	}
625
626
	/**
627
	 * Updates a card.
628
	 *
629
	 * The addressbook id will be passed as the first argument. This is the
630
	 * same id as it is returned from the getAddressBooksForUser method.
631
	 *
632
	 * The cardUri is a base uri, and doesn't include the full path. The
633
	 * cardData argument is the vcard body, and is passed as a string.
634
	 *
635
	 * It is possible to return an ETag from this method. This ETag should
636
	 * match that of the updated resource, and must be enclosed with double
637
	 * quotes (that is: the string itself must contain the actual quotes).
638
	 *
639
	 * You should only return the ETag if you store the carddata as-is. If a
640
	 * subsequent GET request on the same card does not have the same body,
641
	 * byte-by-byte and you did return an ETag here, clients tend to get
642
	 * confused.
643
	 *
644
	 * If you don't return an ETag, you can just return null.
645
	 *
646
	 * @param mixed $addressBookId
647
	 * @param string $cardUri
648
	 * @param string $cardData
649
	 * @return string
650
	 */
651
	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...
652
653
		$etag = md5($cardData);
654
		$query = $this->db->getQueryBuilder();
655
		$query->update('cards')
656
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
657
			->set('lastmodified', $query->createNamedParameter(time()))
658
			->set('size', $query->createNamedParameter(strlen($cardData)))
659
			->set('etag', $query->createNamedParameter($etag))
660
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
661
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
662
			->execute();
663
664
		$this->addChange($addressBookId, $cardUri, 2);
665
		$this->updateProperties($addressBookId, $cardUri, $cardData);
666
667 View Code Duplication
		if (!is_null($this->dispatcher)) {
668
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
669
				new GenericEvent(null, [
670
					'addressBookId' => $addressBookId,
671
					'cardUri' => $cardUri,
672
					'cardData' => $cardData]));
673
		}
674
675
		return '"' . $etag . '"';
676
	}
677
678
	/**
679
	 * Deletes a card
680
	 *
681
	 * @param mixed $addressBookId
682
	 * @param string $cardUri
683
	 * @return bool
684
	 */
685
	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...
686
		try {
687
			$cardId = $this->getCardId($addressBookId, $cardUri);
688
		} catch (\InvalidArgumentException $e) {
689
			$cardId = null;
690
		}
691
		$query = $this->db->getQueryBuilder();
692
		$ret = $query->delete('cards')
693
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
694
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
695
			->execute();
696
697
		$this->addChange($addressBookId, $cardUri, 3);
698
699 View Code Duplication
		if (!is_null($this->dispatcher)) {
700
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
701
				new GenericEvent(null, [
702
					'addressBookId' => $addressBookId,
703
					'cardUri' => $cardUri]));
704
		}
705
706
		if ($ret === 1) {
707
			if ($cardId !== null) {
708
				$this->purgeProperties($addressBookId, $cardId);
709
			}
710
			return true;
711
		}
712
713
		return false;
714
	}
715
716
	/**
717
	 * The getChanges method returns all the changes that have happened, since
718
	 * the specified syncToken in the specified address book.
719
	 *
720
	 * This function should return an array, such as the following:
721
	 *
722
	 * [
723
	 *   'syncToken' => 'The current synctoken',
724
	 *   'added'   => [
725
	 *      'new.txt',
726
	 *   ],
727
	 *   'modified'   => [
728
	 *      'modified.txt',
729
	 *   ],
730
	 *   'deleted' => [
731
	 *      'foo.php.bak',
732
	 *      'old.txt'
733
	 *   ]
734
	 * ];
735
	 *
736
	 * The returned syncToken property should reflect the *current* syncToken
737
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
738
	 * property. This is needed here too, to ensure the operation is atomic.
739
	 *
740
	 * If the $syncToken argument is specified as null, this is an initial
741
	 * sync, and all members should be reported.
742
	 *
743
	 * The modified property is an array of nodenames that have changed since
744
	 * the last token.
745
	 *
746
	 * The deleted property is an array with nodenames, that have been deleted
747
	 * from collection.
748
	 *
749
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
750
	 * 1, you only have to report changes that happened only directly in
751
	 * immediate descendants. If it's 2, it should also include changes from
752
	 * the nodes below the child collections. (grandchildren)
753
	 *
754
	 * The $limit argument allows a client to specify how many results should
755
	 * be returned at most. If the limit is not specified, it should be treated
756
	 * as infinite.
757
	 *
758
	 * If the limit (infinite or not) is higher than you're willing to return,
759
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
760
	 *
761
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
762
	 * return null.
763
	 *
764
	 * The limit is 'suggestive'. You are free to ignore it.
765
	 *
766
	 * @param string $addressBookId
767
	 * @param string $syncToken
768
	 * @param int $syncLevel
769
	 * @param int $limit
770
	 * @return array
771
	 */
772 View Code Duplication
	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...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
773
		// Current synctoken
774
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
775
		$stmt->execute([ $addressBookId ]);
776
		$currentToken = $stmt->fetchColumn(0);
777
778
		if (is_null($currentToken)) return null;
779
780
		$result = [
781
			'syncToken' => $currentToken,
782
			'added'     => [],
783
			'modified'  => [],
784
			'deleted'   => [],
785
		];
786
787
		if ($syncToken) {
788
789
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
790
			if ($limit>0) {
791
				$query .= " `LIMIT` " . (int)$limit;
792
			}
793
794
			// Fetching all changes
795
			$stmt = $this->db->prepare($query);
796
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
797
798
			$changes = [];
799
800
			// This loop ensures that any duplicates are overwritten, only the
801
			// last change on a node is relevant.
802
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
803
804
				$changes[$row['uri']] = $row['operation'];
805
806
			}
807
808
			foreach($changes as $uri => $operation) {
809
810
				switch($operation) {
811
					case 1:
812
						$result['added'][] = $uri;
813
						break;
814
					case 2:
815
						$result['modified'][] = $uri;
816
						break;
817
					case 3:
818
						$result['deleted'][] = $uri;
819
						break;
820
				}
821
822
			}
823
		} else {
824
			// No synctoken supplied, this is the initial sync.
825
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
826
			$stmt = $this->db->prepare($query);
827
			$stmt->execute([$addressBookId]);
828
829
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
830
		}
831
		return $result;
832
	}
833
834
	/**
835
	 * Adds a change record to the addressbookchanges table.
836
	 *
837
	 * @param mixed $addressBookId
838
	 * @param string $objectUri
839
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
840
	 * @return void
841
	 */
842 View Code Duplication
	protected function addChange($addressBookId, $objectUri, $operation) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
843
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
844
		$stmt = $this->db->prepare($sql);
845
		$stmt->execute([
846
			$objectUri,
847
			$addressBookId,
848
			$operation,
849
			$addressBookId
850
		]);
851
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
852
		$stmt->execute([
853
			$addressBookId
854
		]);
855
	}
856
857
	private function readBlob($cardData) {
858
		if (is_resource($cardData)) {
859
			return stream_get_contents($cardData);
860
		}
861
862
		return $cardData;
863
	}
864
865
	/**
866
	 * @param IShareable $shareable
867
	 * @param string[] $add
868
	 * @param string[] $remove
869
	 */
870
	public function updateShares(IShareable $shareable, $add, $remove) {
871
		$this->sharingBackend->updateShares($shareable, $add, $remove);
872
	}
873
874
	/**
875
	 * search contact
876
	 *
877
	 * @param int $addressBookId
878
	 * @param string $pattern which should match within the $searchProperties
879
	 * @param array $searchProperties defines the properties within the query pattern should match
880
	 * @return array an array of contacts which are arrays of key-value-pairs
881
	 */
882
	public function search($addressBookId, $pattern, $searchProperties) {
883
		$query = $this->db->getQueryBuilder();
884
		$query2 = $this->db->getQueryBuilder();
885
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
886
		foreach ($searchProperties as $property) {
887
			$query2->orWhere(
888
				$query2->expr()->andX(
889
					$query2->expr()->eq('cp.name', $query->createNamedParameter($property)),
890
					$query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))
891
				)
892
			);
893
		}
894
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
895
896
		$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
897
			->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
898
899
		$result = $query->execute();
900
		$cards = $result->fetchAll();
901
902
		$result->closeCursor();
903
904
		return array_map(function($array) {
905
			$array['carddata'] = $this->readBlob($array['carddata']);
906
			return $array;
907
		}, $cards);
908
	}
909
910
	/**
911
	 * @param int $bookId
912
	 * @param string $name
913
	 * @return array
914
	 */
915
	public function collectCardProperties($bookId, $name) {
916
		$query = $this->db->getQueryBuilder();
917
		$result = $query->selectDistinct('value')
918
			->from($this->dbCardsPropertiesTable)
919
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
920
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
921
			->execute();
922
923
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
924
		$result->closeCursor();
925
926
		return $all;
927
	}
928
929
	/**
930
	 * get URI from a given contact
931
	 *
932
	 * @param int $id
933
	 * @return string
934
	 */
935
	public function getCardUri($id) {
936
		$query = $this->db->getQueryBuilder();
937
		$query->select('uri')->from($this->dbCardsTable)
938
				->where($query->expr()->eq('id', $query->createParameter('id')))
939
				->setParameter('id', $id);
940
941
		$result = $query->execute();
942
		$uri = $result->fetch();
943
		$result->closeCursor();
944
945
		if (!isset($uri['uri'])) {
946
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
947
		}
948
949
		return $uri['uri'];
950
	}
951
952
	/**
953
	 * return contact with the given URI
954
	 *
955
	 * @param int $addressBookId
956
	 * @param string $uri
957
	 * @returns array
958
	 */
959
	public function getContact($addressBookId, $uri) {
960
		$result = [];
961
		$query = $this->db->getQueryBuilder();
962
		$query->select('*')->from($this->dbCardsTable)
963
				->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
964
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
965
		$queryResult = $query->execute();
966
		$contact = $queryResult->fetch();
967
		$queryResult->closeCursor();
968
969
		if (is_array($contact)) {
970
			$result = $contact;
971
		}
972
973
		return $result;
974
	}
975
976
	/**
977
	 * Returns the list of people whom this address book is shared with.
978
	 *
979
	 * Every element in this array should have the following properties:
980
	 *   * href - Often a mailto: address
981
	 *   * commonName - Optional, for example a first + last name
982
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
983
	 *   * readOnly - boolean
984
	 *   * summary - Optional, a description for the share
985
	 *
986
	 * @return array
987
	 */
988
	public function getShares($addressBookId) {
989
		return $this->sharingBackend->getShares($addressBookId);
990
	}
991
992
	/**
993
	 * update properties table
994
	 *
995
	 * @param int $addressBookId
996
	 * @param string $cardUri
997
	 * @param string $vCardSerialized
998
	 */
999
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1000
		$cardId = $this->getCardId($addressBookId, $cardUri);
1001
		$vCard = $this->readCard($vCardSerialized);
1002
1003
		$this->purgeProperties($addressBookId, $cardId);
1004
1005
		$query = $this->db->getQueryBuilder();
1006
		$query->insert($this->dbCardsPropertiesTable)
1007
			->values(
1008
				[
1009
					'addressbookid' => $query->createNamedParameter($addressBookId),
1010
					'cardid' => $query->createNamedParameter($cardId),
1011
					'name' => $query->createParameter('name'),
1012
					'value' => $query->createParameter('value'),
1013
					'preferred' => $query->createParameter('preferred')
1014
				]
1015
			);
1016
1017
		foreach ($vCard->children() as $property) {
1018
			if(!in_array($property->name, self::$indexProperties)) {
1019
				continue;
1020
			}
1021
			$preferred = 0;
1022
			foreach($property->parameters as $parameter) {
1023
				if ($parameter->name == 'TYPE' && strtoupper($parameter->getValue()) == 'PREF') {
1024
					$preferred = 1;
1025
					break;
1026
				}
1027
			}
1028
			$query->setParameter('name', $property->name);
1029
			$query->setParameter('value', substr($property->getValue(), 0, 254));
1030
			$query->setParameter('preferred', $preferred);
1031
			$query->execute();
1032
		}
1033
	}
1034
1035
	/**
1036
	 * read vCard data into a vCard object
1037
	 *
1038
	 * @param string $cardData
1039
	 * @return VCard
1040
	 */
1041
	protected function readCard($cardData) {
1042
		return  Reader::read($cardData);
1043
	}
1044
1045
	/**
1046
	 * delete all properties from a given card
1047
	 *
1048
	 * @param int $addressBookId
1049
	 * @param int $cardId
1050
	 */
1051
	protected function purgeProperties($addressBookId, $cardId) {
1052
		$query = $this->db->getQueryBuilder();
1053
		$query->delete($this->dbCardsPropertiesTable)
1054
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1055
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1056
		$query->execute();
1057
	}
1058
1059
	/**
1060
	 * get ID from a given contact
1061
	 *
1062
	 * @param int $addressBookId
1063
	 * @param string $uri
1064
	 * @return int
1065
	 */
1066
	protected function getCardId($addressBookId, $uri) {
1067
		$query = $this->db->getQueryBuilder();
1068
		$query->select('id')->from($this->dbCardsTable)
1069
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1070
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1071
1072
		$result = $query->execute();
1073
		$cardIds = $result->fetch();
1074
		$result->closeCursor();
1075
1076
		if (!isset($cardIds['id'])) {
1077
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1078
		}
1079
1080
		return (int)$cardIds['id'];
1081
	}
1082
1083
	/**
1084
	 * For shared address books the sharee is set in the ACL of the address book
1085
	 * @param $addressBookId
1086
	 * @param $acl
1087
	 * @return array
1088
	 */
1089
	public function applyShareAcl($addressBookId, $acl) {
1090
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1091
	}
1092
1093 View Code Duplication
	private function convertPrincipal($principalUri, $toV2) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1094
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1095
			list(, $name) = URLUtil::splitPath($principalUri);
1096
			if ($toV2 === true) {
1097
				return "principals/users/$name";
1098
			}
1099
			return "principals/$name";
1100
		}
1101
		return $principalUri;
1102
	}
1103
1104 View Code Duplication
	private function addOwnerPrincipal(&$addressbookInfo) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1105
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1106
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1107
		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1108
			$uri = $addressbookInfo[$ownerPrincipalKey];
1109
		} else {
1110
			$uri = $addressbookInfo['principaluri'];
1111
		}
1112
1113
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1114
		if (isset($principalInformation['{DAV:}displayname'])) {
1115
			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1116
		}
1117
	}
1118
}
1119