CardDavBackend   F
last analyzed

Complexity

Total Complexity 79

Size/Duplication

Total Lines 994
Duplicated Lines 7.14 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 12
dl 71
loc 994
rs 1.686
c 0
b 0
f 0

29 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A getUsersOwnAddressBooks() 0 24 3
A getAddressBooksForUser() 0 39 4
A getAddressBookById() 0 23 3
A getAddressBooksByUri() 0 25 3
A updateAddressBook() 11 32 5
B createAddressBook() 14 42 5
A deleteAddressBook() 0 23 1
A getCards() 5 18 2
A getCard() 0 18 2
A getMultipleCards() 5 29 3
A createCard() 7 31 2
A updateCard() 7 25 2
A deleteCard() 6 30 5
B getChangesForAddressBook() 0 55 10
A addChange() 0 14 1
A readBlob() 0 7 2
A updateShares() 0 3 1
A search() 0 30 2
A collectCardProperties() 0 13 1
A getCardUri() 16 16 2
A getContact() 0 16 2
A getShares() 0 3 1
B updateProperties() 0 35 6
A readCard() 0 3 1
A purgeProperties() 0 7 1
A getCardId() 0 21 3
A applyShareAcl() 0 3 1
A convertPrincipal() 0 11 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CardDavBackend often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CardDavBackend, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Björn Schießle <[email protected]>
5
 * @author Georg Ehrke <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Stefan Weil <[email protected]>
8
 * @author Thomas Müller <[email protected]>
9
 *
10
 * @copyright Copyright (c) 2018, ownCloud GmbH
11
 * @license AGPL-3.0
12
 *
13
 * This code is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License, version 3,
15
 * as published by the Free Software Foundation.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License, version 3,
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
24
 *
25
 */
26
27
namespace OCA\DAV\CardDAV;
28
29
use OC\Cache\CappedMemoryCache;
30
use OCA\DAV\Connector\Sabre\Principal;
31
use OCA\DAV\DAV\GroupPrincipalBackend;
32
use OCA\DAV\DAV\Sharing\Backend;
33
use OCA\DAV\DAV\Sharing\IShareable;
34
use OCP\DB\QueryBuilder\IQueryBuilder;
35
use OCP\IDBConnection;
36
use PDO;
37
use Sabre\CardDAV\Backend\BackendInterface;
38
use Sabre\CardDAV\Backend\SyncSupport;
39
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...
40
use Sabre\DAV\Exception\BadRequest;
41
use Sabre\VObject\Component\VCard;
42
use Sabre\VObject\Reader;
43
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
44
use Symfony\Component\EventDispatcher\GenericEvent;
45
46
class CardDavBackend implements BackendInterface, SyncSupport {
47
48
	/** @var Principal */
49
	private $principalBackend;
50
51
	/** @var string */
52
	private $dbCardsTable = 'cards';
53
54
	/** @var string */
55
	private $dbCardsPropertiesTable = 'cards_properties';
56
57
	/** @var IDBConnection */
58
	private $db;
59
60
	/** @var Backend */
61
	private $sharingBackend;
62
63
	/** @var CappedMemoryCache Cache of card URI to db row ids */
64
	private $idCache;
65
66
	/** @var array properties to index */
67
	public static $indexProperties = [
68
			'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
69
			'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'];
70
71
	/** @var EventDispatcherInterface */
72
	private $dispatcher;
73
	/** @var bool */
74
	private $legacyMode;
75
76
	/**
77
	 * CardDavBackend constructor.
78
	 *
79
	 * @param IDBConnection $db
80
	 * @param Principal $principalBackend
81
	 * @param GroupPrincipalBackend $groupPrincipalBackend
82
	 * @param EventDispatcherInterface $dispatcher
83
	 * @param bool $legacyMode
84
	 */
85
	public function __construct(IDBConnection $db,
86
								Principal $principalBackend,
87
								GroupPrincipalBackend $groupPrincipalBackend,
88
								EventDispatcherInterface $dispatcher = null,
89
								$legacyMode = false) {
90
		$this->db = $db;
91
		$this->principalBackend = $principalBackend;
92
		$this->dispatcher = $dispatcher;
93
		$this->sharingBackend = new Backend($this->db, $principalBackend, $groupPrincipalBackend, 'addressbook');
94
		$this->legacyMode = $legacyMode;
95
		$this->idCache = new CappedMemoryCache();
96
	}
97
98
	/**
99
	 * Returns the list of address books for a specific user.
100
	 *
101
	 * Every addressbook should have the following properties:
102
	 *   id - an arbitrary unique id
103
	 *   uri - the 'basename' part of the url
104
	 *   principaluri - Same as the passed parameter
105
	 *
106
	 * Any additional clark-notation property may be passed besides this. Some
107
	 * common ones are :
108
	 *   {DAV:}displayname
109
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
110
	 *   {http://calendarserver.org/ns/}getctag
111
	 *
112
	 * @param string $principalUri
113
	 * @return array
114
	 */
115
	public function getUsersOwnAddressBooks($principalUri) {
116
		$principalUri = $this->convertPrincipal($principalUri, true);
117
		$query = $this->db->getQueryBuilder();
118
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
119
			->from('addressbooks')
120
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
121
122
		$addressBooks = [];
123
124
		$result = $query->execute();
125
		while ($row = $result->fetch()) {
126
			$addressBooks[$row['id']] = [
127
				'id' => $row['id'],
128
				'uri' => $row['uri'],
129
				'principaluri' => $this->convertPrincipal($row['principaluri']),
130
				'{DAV:}displayname' => $row['displayname'],
131
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
132
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
133
				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
134
			];
135
		}
136
		$result->closeCursor();
137
		return \array_values($addressBooks);
138
	}
139
140
	/**
141
	 * Returns the list of address books for a specific user, including shared by other users.
142
	 *
143
	 * Every addressbook should have the following properties:
144
	 *   id - an arbitrary unique id
145
	 *   uri - the 'basename' part of the url
146
	 *   principaluri - Same as the passed parameter
147
	 *
148
	 * Any additional clark-notation property may be passed besides this. Some
149
	 * common ones are :
150
	 *   {DAV:}displayname
151
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
152
	 *   {http://calendarserver.org/ns/}getctag
153
	 *
154
	 * @param string $principalUri
155
	 * @return array
156
	 * @throws \Sabre\DAV\Exception
157
	 */
158
	public function getAddressBooksForUser($principalUri) {
159
		$addressBooks = $this->getUsersOwnAddressBooks($principalUri);
160
161
		// query for shared calendars
162
		$principals = $this->principalBackend->getGroupMembership($principalUri, true);
163
		$principals[]= $principalUri;
164
165
		$query = $this->db->getQueryBuilder();
166
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
167
			->from('dav_shares', 's')
168
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
169
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
170
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
171
			->setParameter('type', 'addressbook')
172
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
173
			->execute();
174
175
		while ($row = $result->fetch()) {
176
			list(, $name) = \Sabre\Uri\split($row['principaluri']);
177
			$uri = $row['uri'] . '_shared_by_' . $name;
178
			$displayName = $row['displayname'] . " ($name)";
179
			if (!isset($addressBooks[$row['id']])) {
180
				$addressBooks[$row['id']] = [
181
					'id'  => $row['id'],
182
					'uri' => $uri,
183
					'principaluri' => $principalUri,
184
					'{DAV:}displayname' => $displayName,
185
					'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
186
					'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
187
					'{http://sabredav.org/ns}sync-token' => $row['synctoken']?:'0',
188
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
189
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
190
				];
191
			}
192
		}
193
		$result->closeCursor();
194
195
		return \array_values($addressBooks);
196
	}
197
198
	/**
199
	 * @param int $addressBookId
200
	 */
201
	public function getAddressBookById($addressBookId) {
202
		$query = $this->db->getQueryBuilder();
203
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
204
			->from('addressbooks')
205
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
206
			->execute();
207
208
		$row = $result->fetch();
209
		$result->closeCursor();
210
		if ($row === false) {
211
			return null;
212
		}
213
214
		return [
215
			'id'  => $row['id'],
216
			'uri' => $row['uri'],
217
			'principaluri' => $row['principaluri'],
218
			'{DAV:}displayname' => $row['displayname'],
219
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
220
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
221
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?:'0',
222
		];
223
	}
224
225
	/**
226
	 * @param $addressBookUri
227
	 * @return array|null
228
	 */
229
	public function getAddressBooksByUri($principal, $addressBookUri) {
230
		$query = $this->db->getQueryBuilder();
231
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
232
			->from('addressbooks')
233
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
234
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
235
			->setMaxResults(1)
236
			->execute();
237
238
		$row = $result->fetch();
239
		$result->closeCursor();
240
		if ($row === false) {
241
			return null;
242
		}
243
244
		return [
245
				'id'  => $row['id'],
246
				'uri' => $row['uri'],
247
				'principaluri' => $row['principaluri'],
248
				'{DAV:}displayname' => $row['displayname'],
249
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
250
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
251
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?:'0',
252
			];
253
	}
254
255
	/**
256
	 * Updates properties for an address book.
257
	 *
258
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
259
	 * To do the actual updates, you must tell this object which properties
260
	 * you're going to process with the handle() method.
261
	 *
262
	 * Calling the handle method is like telling the PropPatch object "I
263
	 * promise I can handle updating this property".
264
	 *
265
	 * Read the PropPatch documentation for more info and examples.
266
	 *
267
	 * @param string $addressBookId
268
	 * @param \Sabre\DAV\PropPatch $propPatch
269
	 * @return void
270
	 */
271
	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
272
		$supportedProperties = [
273
			'{DAV:}displayname',
274
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
275
		];
276
277
		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
278
			$updates = [];
279 View Code Duplication
			foreach ($mutations as $property=>$newValue) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
280
				switch ($property) {
281
					case '{DAV:}displayname':
282
						$updates['displayname'] = $newValue;
283
						break;
284
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
285
						$updates['description'] = $newValue;
286
						break;
287
				}
288
			}
289
			$query = $this->db->getQueryBuilder();
290
			$query->update('addressbooks');
291
292
			foreach ($updates as $key=>$value) {
293
				$query->set($key, $query->createNamedParameter($value));
294
			}
295
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
296
			->execute();
297
298
			$this->addChange($addressBookId, '', 2);
299
300
			return true;
301
		});
302
	}
303
304
	/**
305
	 * Creates a new address book
306
	 *
307
	 * @param string $principalUri
308
	 * @param string $url Just the 'basename' of the url.
309
	 * @param array $properties
310
	 * @return int
311
	 * @throws \BadMethodCallException
312
	 * @throws BadRequest
313
	 */
314
	public function createAddressBook($principalUri, $url, array $properties) {
315
		$values = [
316
			'displayname' => null,
317
			'description' => null,
318
			'principaluri' => $principalUri,
319
			'uri' => $url,
320
			'synctoken' => 1
321
		];
322
323 View Code Duplication
		foreach ($properties as $property=>$newValue) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
324
			switch ($property) {
325
				case '{DAV:}displayname':
326
					$values['displayname'] = $newValue;
327
					break;
328
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
329
					$values['description'] = $newValue;
330
					break;
331
				default:
332
					throw new BadRequest('Unknown property: ' . $property);
333
			}
334
		}
335
336
		// Fallback to make sure the displayname is set. Some clients may refuse
337
		// to work with addressbooks not having a displayname.
338
		if ($values['displayname'] === null) {
339
			$values['displayname'] = $url;
340
		}
341
342
		$query = $this->db->getQueryBuilder();
343
		$query->insert('addressbooks')
344
			->values([
345
				'uri' => $query->createParameter('uri'),
346
				'displayname' => $query->createParameter('displayname'),
347
				'description' => $query->createParameter('description'),
348
				'principaluri' => $query->createParameter('principaluri'),
349
				'synctoken' => $query->createParameter('synctoken'),
350
			])
351
			->setParameters($values)
352
			->execute();
353
354
		return $query->getLastInsertId();
355
	}
356
357
	/**
358
	 * Deletes an entire addressbook and all its contents
359
	 *
360
	 * @param mixed $addressBookId
361
	 * @return void
362
	 */
363
	public function deleteAddressBook($addressBookId) {
364
		$query = $this->db->getQueryBuilder();
365
		$query->delete('cards')
366
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
367
			->setParameter('addressbookid', $addressBookId)
368
			->execute();
369
370
		$query->delete('addressbookchanges')
371
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
372
			->setParameter('addressbookid', $addressBookId)
373
			->execute();
374
375
		$query->delete('addressbooks')
376
			->where($query->expr()->eq('id', $query->createParameter('id')))
377
			->setParameter('id', $addressBookId)
378
			->execute();
379
380
		$this->sharingBackend->deleteAllShares($addressBookId);
381
382
		$query->delete($this->dbCardsPropertiesTable)
383
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
384
			->execute();
385
	}
386
387
	/**
388
	 * Returns all cards for a specific addressbook id.
389
	 *
390
	 * This method should return the following properties for each card:
391
	 *   * carddata - raw vcard data
392
	 *   * uri - Some unique url
393
	 *   * lastmodified - A unix timestamp
394
	 *
395
	 * It's recommended to also return the following properties:
396
	 *   * etag - A unique etag. This must change every time the card changes.
397
	 *   * size - The size of the card in bytes.
398
	 *
399
	 * If these last two properties are provided, less time will be spent
400
	 * calculating them. If they are specified, you can also ommit carddata.
401
	 * This may speed up certain requests, especially with large cards.
402
	 *
403
	 * @param mixed $addressBookId
404
	 * @return array
405
	 */
406
	public function getCards($addressBookId) {
407
		$query = $this->db->getQueryBuilder();
408
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
409
			->from('cards')
410
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
411
412
		$cards = [];
413
414
		$result = $query->execute();
415 View Code Duplication
		while ($row = $result->fetch()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
416
			$row['etag'] = '"' . $row['etag'] . '"';
417
			$row['carddata'] = $this->readBlob($row['carddata']);
418
			$cards[] = $row;
419
		}
420
		$result->closeCursor();
421
422
		return $cards;
423
	}
424
425
	/**
426
	 * Returns a specific card.
427
	 *
428
	 * The same set of properties must be returned as with getCards. The only
429
	 * exception is that 'carddata' is absolutely required.
430
	 *
431
	 * If the card does not exist, you must return false.
432
	 *
433
	 * @param mixed $addressBookId
434
	 * @param string $cardUri
435
	 * @return array|false
436
	 */
437
	public function getCard($addressBookId, $cardUri) {
438
		$query = $this->db->getQueryBuilder();
439
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
440
			->from('cards')
441
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
442
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
443
			->setMaxResults(1);
444
445
		$result = $query->execute();
446
		$row = $result->fetch();
447
		if (!$row) {
448
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Sabre\CardDAV\Backend\BackendInterface::getCard of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
449
		}
450
		$row['etag'] = '"' . $row['etag'] . '"';
451
		$row['carddata'] = $this->readBlob($row['carddata']);
452
453
		return $row;
454
	}
455
456
	/**
457
	 * Returns a list of cards.
458
	 *
459
	 * This method should work identical to getCard, but instead return all the
460
	 * cards in the list as an array.
461
	 *
462
	 * If the backend supports this, it may allow for some speed-ups.
463
	 *
464
	 * @param mixed $addressBookId
465
	 * @param string[] $uris
466
	 * @return array
467
	 */
468
	public function getMultipleCards($addressBookId, array $uris) {
469
		$chunkSize = 998;
470
		if (\count($uris) <= $chunkSize) {
471
			$query = $this->db->getQueryBuilder();
472
			$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
473
				->from('cards')
474
				->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
475
				->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
476
				->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
477
478
			$cards = [];
479
480
			$result = $query->execute();
481 View Code Duplication
			while ($row = $result->fetch()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
482
				$row['etag'] = '"' . $row['etag'] . '"';
483
				$row['carddata'] = $this->readBlob($row['carddata']);
484
				$cards[] = $row;
485
			}
486
			$result->closeCursor();
487
488
			return $cards;
489
		}
490
		$chunks = \array_chunk($uris, $chunkSize);
491
		$results = \array_map(function ($chunk) use ($addressBookId) {
492
			return $this->getMultipleCards($addressBookId, $chunk);
493
		}, $chunks);
494
495
		return \array_merge(...$results);
496
	}
497
498
	/**
499
	 * Creates a new card.
500
	 *
501
	 * The addressbook id will be passed as the first argument. This is the
502
	 * same id as it is returned from the getAddressBooksForUser method.
503
	 *
504
	 * The cardUri is a base uri, and doesn't include the full path. The
505
	 * cardData argument is the vcard body, and is passed as a string.
506
	 *
507
	 * It is possible to return an ETag from this method. This ETag is for the
508
	 * newly created resource, and must be enclosed with double quotes (that
509
	 * is, the string itself must contain the double quotes).
510
	 *
511
	 * You should only return the ETag if you store the carddata as-is. If a
512
	 * subsequent GET request on the same card does not have the same body,
513
	 * byte-by-byte and you did return an ETag here, clients tend to get
514
	 * confused.
515
	 *
516
	 * If you don't return an ETag, you can just return null.
517
	 *
518
	 * @param mixed $addressBookId
519
	 * @param string $cardUri
520
	 * @param string $cardData
521
	 * @return string
522
	 * @throws \BadMethodCallException
523
	 */
524
	public function createCard($addressBookId, $cardUri, $cardData) {
525
		$etag = \md5($cardData);
526
527
		$query = $this->db->getQueryBuilder();
528
		$query->insert('cards')
529
			->values([
530
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
531
				'uri' => $query->createNamedParameter($cardUri),
532
				'lastmodified' => $query->createNamedParameter(\time()),
533
				'addressbookid' => $query->createNamedParameter($addressBookId),
534
				'size' => $query->createNamedParameter(\strlen($cardData)),
535
				'etag' => $query->createNamedParameter($etag),
536
			])
537
			->execute();
538
539
		// Cache the ID so that it can be used for the updateProperties method
540
		$this->idCache->set($addressBookId.$cardUri, $query->getLastInsertId());
541
542
		$this->addChange($addressBookId, $cardUri, 1);
543
		$this->updateProperties($addressBookId, $cardUri, $cardData);
544
545 View Code Duplication
		if ($this->dispatcher !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
546
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
547
				new GenericEvent(null, [
548
					'addressBookId' => $addressBookId,
549
					'cardUri' => $cardUri,
550
					'cardData' => $cardData]));
551
		}
552
553
		return '"' . $etag . '"';
554
	}
555
556
	/**
557
	 * Updates a card.
558
	 *
559
	 * The addressbook id will be passed as the first argument. This is the
560
	 * same id as it is returned from the getAddressBooksForUser method.
561
	 *
562
	 * The cardUri is a base uri, and doesn't include the full path. The
563
	 * cardData argument is the vcard body, and is passed as a string.
564
	 *
565
	 * It is possible to return an ETag from this method. This ETag should
566
	 * match that of the updated resource, and must be enclosed with double
567
	 * quotes (that is: the string itself must contain the actual quotes).
568
	 *
569
	 * You should only return the ETag if you store the carddata as-is. If a
570
	 * subsequent GET request on the same card does not have the same body,
571
	 * byte-by-byte and you did return an ETag here, clients tend to get
572
	 * confused.
573
	 *
574
	 * If you don't return an ETag, you can just return null.
575
	 *
576
	 * @param mixed $addressBookId
577
	 * @param string $cardUri
578
	 * @param string $cardData
579
	 * @return string
580
	 */
581
	public function updateCard($addressBookId, $cardUri, $cardData) {
582
		$etag = \md5($cardData);
583
		$query = $this->db->getQueryBuilder();
584
		$query->update('cards')
585
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
586
			->set('lastmodified', $query->createNamedParameter(\time()))
587
			->set('size', $query->createNamedParameter(\strlen($cardData)))
588
			->set('etag', $query->createNamedParameter($etag))
589
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
590
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
591
			->execute();
592
593
		$this->addChange($addressBookId, $cardUri, 2);
594
		$this->updateProperties($addressBookId, $cardUri, $cardData);
595
596 View Code Duplication
		if ($this->dispatcher !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
597
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
598
				new GenericEvent(null, [
599
					'addressBookId' => $addressBookId,
600
					'cardUri' => $cardUri,
601
					'cardData' => $cardData]));
602
		}
603
604
		return '"' . $etag . '"';
605
	}
606
607
	/**
608
	 * Deletes a card
609
	 *
610
	 * @param mixed $addressBookId
611
	 * @param string $cardUri
612
	 * @return bool
613
	 */
614
	public function deleteCard($addressBookId, $cardUri) {
615
		try {
616
			$cardId = $this->getCardId($addressBookId, $cardUri);
617
		} catch (\InvalidArgumentException $e) {
618
			$cardId = null;
619
		}
620
		$query = $this->db->getQueryBuilder();
621
		$ret = $query->delete('cards')
622
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
623
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
624
			->execute();
625
626
		$this->addChange($addressBookId, $cardUri, 3);
627
628 View Code Duplication
		if ($this->dispatcher !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
629
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
630
				new GenericEvent(null, [
631
					'addressBookId' => $addressBookId,
632
					'cardUri' => $cardUri]));
633
		}
634
635
		if ($ret === 1) {
636
			if ($cardId !== null) {
637
				$this->purgeProperties($addressBookId, $cardId);
638
			}
639
			return true;
640
		}
641
642
		return false;
643
	}
644
645
	/**
646
	 * The getChanges method returns all the changes that have happened, since
647
	 * the specified syncToken in the specified address book.
648
	 *
649
	 * This function should return an array, such as the following:
650
	 *
651
	 * [
652
	 *   'syncToken' => 'The current synctoken',
653
	 *   'added'   => [
654
	 *      'new.txt',
655
	 *   ],
656
	 *   'modified'   => [
657
	 *      'modified.txt',
658
	 *   ],
659
	 *   'deleted' => [
660
	 *      'foo.php.bak',
661
	 *      'old.txt'
662
	 *   ]
663
	 * ];
664
	 *
665
	 * The returned syncToken property should reflect the *current* syncToken
666
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
667
	 * property. This is needed here too, to ensure the operation is atomic.
668
	 *
669
	 * If the $syncToken argument is specified as null, this is an initial
670
	 * sync, and all members should be reported.
671
	 *
672
	 * The modified property is an array of nodenames that have changed since
673
	 * the last token.
674
	 *
675
	 * The deleted property is an array with nodenames, that have been deleted
676
	 * from collection.
677
	 *
678
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
679
	 * 1, you only have to report changes that happened only directly in
680
	 * immediate descendants. If it's 2, it should also include changes from
681
	 * the nodes below the child collections. (grandchildren)
682
	 *
683
	 * The $limit argument allows a client to specify how many results should
684
	 * be returned at most. If the limit is not specified, it should be treated
685
	 * as infinite.
686
	 *
687
	 * If the limit (infinite or not) is higher than you're willing to return,
688
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
689
	 *
690
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
691
	 * return null.
692
	 *
693
	 * The limit is 'suggestive'. You are free to ignore it.
694
	 *
695
	 * @param string $addressBookId
696
	 * @param string $syncToken
697
	 * @param int $syncLevel
698
	 * @param int $limit
699
	 * @return array
700
	 */
701
	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
702
		// Current synctoken
703
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
704
		$stmt->execute([ $addressBookId ]);
705
		$currentToken = $stmt->fetchColumn();
706
707
		if ($currentToken === null) {
708
			return null;
709
		}
710
711
		$result = [
712
			'syncToken' => $currentToken,
713
			'added'     => [],
714
			'modified'  => [],
715
			'deleted'   => [],
716
		];
717
718
		if ($syncToken) {
719
			$query = 'SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`';
720
721
			// Fetching all changes
722
			$stmt = $this->db->prepare($query, $limit ?: null, $limit ? 0 : null);
723
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
724
725
			$changes = [];
726
727
			// This loop ensures that any duplicates are overwritten, only the
728
			// last change on a node is relevant.
729
			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
730
				$changes[$row['uri']] = $row['operation'];
731
			}
732
733
			foreach ($changes as $uri => $operation) {
734
				switch ($operation) {
735
					case 1:
736
						$result['added'][] = $uri;
737
						break;
738
					case 2:
739
						$result['modified'][] = $uri;
740
						break;
741
					case 3:
742
						$result['deleted'][] = $uri;
743
						break;
744
				}
745
			}
746
		} else {
747
			// No synctoken supplied, this is the initial sync.
748
			$query = 'SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?';
749
			$stmt = $this->db->prepare($query);
750
			$stmt->execute([$addressBookId]);
751
752
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
753
		}
754
		return $result;
755
	}
756
757
	/**
758
	 * Adds a change record to the addressbookchanges table.
759
	 *
760
	 * @param mixed $addressBookId
761
	 * @param string $objectUri
762
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
763
	 * @return void
764
	 */
765
	protected function addChange($addressBookId, $objectUri, $operation) {
766
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
767
		$stmt = $this->db->prepare($sql);
768
		$stmt->execute([
769
			$objectUri,
770
			$addressBookId,
771
			$operation,
772
			$addressBookId
773
		]);
774
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
775
		$stmt->execute([
776
			$addressBookId
777
		]);
778
	}
779
780
	private function readBlob($cardData) {
781
		if (\is_resource($cardData)) {
782
			return \stream_get_contents($cardData);
783
		}
784
785
		return $cardData;
786
	}
787
788
	/**
789
	 * @param IShareable $shareable
790
	 * @param string[] $add
791
	 * @param string[] $remove
792
	 */
793
	public function updateShares(IShareable $shareable, $add, $remove) {
794
		$this->sharingBackend->updateShares($shareable, $add, $remove);
795
	}
796
797
	/**
798
	 * search contact
799
	 *
800
	 * @param int $addressBookId
801
	 * @param string $pattern which should match within the $searchProperties
802
	 * @param array $searchProperties defines the properties within the query pattern should match
803
	 * @param int $limit
804
	 * @param int $offset
805
	 * @return array an array of contacts which are arrays of key-value-pairs
806
	 */
807
	public function search($addressBookId, $pattern, $searchProperties, $limit = 100, $offset = 0) {
808
		$query = $this->db->getQueryBuilder();
809
		$query2 = $this->db->getQueryBuilder();
810
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
811
		foreach ($searchProperties as $property) {
812
			$query2->orWhere(
813
				$query2->expr()->andX(
814
					$query2->expr()->eq('cp.name', $query->createNamedParameter($property)),
815
					$query2->expr()->iLike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))
0 ignored issues
show
Unused Code introduced by
The call to IExpressionBuilder::andX() has too many arguments starting with $query2->expr()->iLike('...meter($pattern) . '%')).

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

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

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
816
				)
817
			);
818
		}
819
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
820
821
		$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
0 ignored issues
show
Unused Code introduced by
The call to IQueryBuilder::select() has too many arguments starting with 'c.uri'.

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

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

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
822
			->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
823
824
		$query->setFirstResult($offset)->setMaxResults($limit);
825
		$query->orderBy('c.uri');
826
827
		$result = $query->execute();
828
		$cards = $result->fetchAll();
829
830
		$result->closeCursor();
831
832
		return \array_map(function ($array) {
833
			$array['carddata'] = $this->readBlob($array['carddata']);
834
			return $array;
835
		}, $cards);
836
	}
837
838
	/**
839
	 * @param int $bookId
840
	 * @param string $name
841
	 * @return array
842
	 */
843
	public function collectCardProperties($bookId, $name) {
844
		$query = $this->db->getQueryBuilder();
845
		$result = $query->selectDistinct('value')
846
			->from($this->dbCardsPropertiesTable)
847
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
848
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
849
			->execute();
850
851
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
852
		$result->closeCursor();
853
854
		return $all;
855
	}
856
857
	/**
858
	 * get URI from a given contact
859
	 *
860
	 * @param int $id
861
	 * @return string
862
	 * @throws \InvalidArgumentException
863
	 */
864 View Code Duplication
	public function getCardUri($id) {
865
		$query = $this->db->getQueryBuilder();
866
		$query->select('uri')->from($this->dbCardsTable)
867
				->where($query->expr()->eq('id', $query->createParameter('id')))
868
				->setParameter('id', $id);
869
870
		$result = $query->execute();
871
		$uri = $result->fetch();
872
		$result->closeCursor();
873
874
		if (!isset($uri['uri'])) {
875
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
876
		}
877
878
		return $uri['uri'];
879
	}
880
881
	/**
882
	 * return contact with the given URI
883
	 *
884
	 * @param int $addressBookId
885
	 * @param string $uri
886
	 * @return array
887
	 */
888
	public function getContact($addressBookId, $uri) {
889
		$result = [];
890
		$query = $this->db->getQueryBuilder();
891
		$query->select('*')->from($this->dbCardsTable)
892
				->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
893
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
894
		$queryResult = $query->execute();
895
		$contact = $queryResult->fetch();
896
		$queryResult->closeCursor();
897
898
		if (\is_array($contact)) {
899
			$result = $contact;
900
		}
901
902
		return $result;
903
	}
904
905
	/**
906
	 * Returns the list of people whom this address book is shared with.
907
	 *
908
	 * Every element in this array should have the following properties:
909
	 *   * href - Often a mailto: address
910
	 *   * commonName - Optional, for example a first + last name
911
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
912
	 *   * readOnly - boolean
913
	 *   * summary - Optional, a description for the share
914
	 *
915
	 * @return array
916
	 */
917
	public function getShares($addressBookId) {
918
		return $this->sharingBackend->getShares($addressBookId);
919
	}
920
921
	/**
922
	 * update properties table
923
	 *
924
	 * @param int $addressBookId
925
	 * @param string $cardUri
926
	 * @param string $vCardSerialized
927
	 */
928
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
929
		$cardId = $this->getCardId($addressBookId, $cardUri);
930
		$vCard = $this->readCard($vCardSerialized);
931
932
		$this->purgeProperties($addressBookId, $cardId);
933
934
		$query = $this->db->getQueryBuilder();
935
		$query->insert($this->dbCardsPropertiesTable)
936
			->values(
937
				[
938
					'addressbookid' => $query->createNamedParameter($addressBookId),
939
					'cardid' => $query->createNamedParameter($cardId),
940
					'name' => $query->createParameter('name'),
941
					'value' => $query->createParameter('value'),
942
					'preferred' => $query->createParameter('preferred')
943
				]
944
			);
945
946
		foreach ($vCard->children() as $property) {
947
			if (!\in_array($property->name, self::$indexProperties)) {
948
				continue;
949
			}
950
			$preferred = 0;
951
			foreach ($property->parameters as $parameter) {
952
				if ($parameter->name == 'TYPE' && \strtoupper($parameter->getValue()) == 'PREF') {
953
					$preferred = 1;
954
					break;
955
				}
956
			}
957
			$query->setParameter('name', $property->name);
958
			$query->setParameter('value', \substr($property->getValue(), 0, 254));
959
			$query->setParameter('preferred', $preferred);
960
			$query->execute();
961
		}
962
	}
963
964
	/**
965
	 * read vCard data into a vCard object
966
	 *
967
	 * @param string $cardData
968
	 * @return VCard
969
	 */
970
	protected function readCard($cardData) {
971
		return  Reader::read($cardData);
972
	}
973
974
	/**
975
	 * delete all properties from a given card
976
	 *
977
	 * @param int $addressBookId
978
	 * @param int $cardId
979
	 */
980
	protected function purgeProperties($addressBookId, $cardId) {
981
		$query = $this->db->getQueryBuilder();
982
		$query->delete($this->dbCardsPropertiesTable)
983
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
984
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
985
		$query->execute();
986
	}
987
988
	/**
989
	 * get ID from a given contact
990
	 *
991
	 * @param int $addressBookId
992
	 * @param string $uri
993
	 * @return int
994
	 * @throws \InvalidArgumentException
995
	 */
996
	protected function getCardId($addressBookId, $uri) {
997
		// Try to find cardId from own cache to avoid issue with db cluster
998
		if ($this->idCache->hasKey($addressBookId.$uri)) {
999
			return $this->idCache->get($addressBookId.$uri);
1000
		}
1001
1002
		$query = $this->db->getQueryBuilder();
1003
		$query->select('id')->from($this->dbCardsTable)
1004
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1005
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1006
1007
		$result = $query->execute();
1008
		$cardIds = $result->fetch();
1009
		$result->closeCursor();
1010
1011
		if (!isset($cardIds['id'])) {
1012
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1013
		}
1014
1015
		return (int)$cardIds['id'];
1016
	}
1017
1018
	/**
1019
	 * For shared address books the sharee is set in the ACL of the address book
1020
	 * @param $addressBookId
1021
	 * @param $acl
1022
	 * @return array
1023
	 */
1024
	public function applyShareAcl($addressBookId, $acl) {
1025
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1026
	}
1027
1028
	private function convertPrincipal($principalUri, $toV2 = null) {
1029
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1030
			list(, $name) = \Sabre\Uri\split($principalUri);
1031
			$toV2 = $toV2 === null ? !$this->legacyMode : $toV2;
1032
			if ($toV2) {
1033
				return "principals/users/$name";
1034
			}
1035
			return "principals/$name";
1036
		}
1037
		return $principalUri;
1038
	}
1039
}
1040