CardDavBackend   F
last analyzed

Complexity

Total Complexity 132

Size/Duplication

Total Lines 1399
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 684
dl 0
loc 1399
rs 1.916
c 1
b 0
f 0
wmc 132

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A getAddressBooksForUserCount() 0 11 1
A getCard() 0 22 3
A getCards() 0 23 3
A getMultipleCards() 0 32 5
A updateShares() 0 10 1
A applyShareAcl() 0 2 1
A purgeProperties() 0 6 1
A updateCard() 0 34 3
A moveCard() 0 38 4
A deleteAddressBook() 0 35 2
A searchPrincipalUri() 0 11 1
A getAddressBookById() 0 25 3
A getCardId() 0 15 2
A getContact() 0 22 3
A getCardUri() 0 15 2
A getAddressBooksByUri() 0 33 4
B updateProperties() 0 36 6
A readCard() 0 2 1
A updateAddressBook() 0 37 5
B createAddressBook() 0 55 6
A createCard() 0 45 3
A search() 0 4 1
B getAddressBooksForUser() 0 84 10
F searchByAddressBookIds() 0 92 19
B getChangesForAddressBook() 0 79 10
A getShares() 0 2 1
B readBlob() 0 35 8
A getUsersOwnAddressBooks() 0 26 3
A collectCardProperties() 0 12 1
A deleteCard() 0 29 4
A addChange() 0 26 1
A convertPrincipal() 0 9 3
A addOwnerPrincipal() 0 12 3
A getUID() 0 12 3
A pruneOutdatedSyncTokens() 0 18 4

How to fix   Complexity   

Complex Class

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.

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
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arne Hamann <[email protected]>
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Chih-Hsuan Yen <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Georg Ehrke <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author John Molakvoæ <[email protected]>
13
 * @author matt <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Stefan Weil <[email protected]>
18
 * @author Thomas Citharel <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 *
21
 * @license AGPL-3.0
22
 *
23
 * This code is free software: you can redistribute it and/or modify
24
 * it under the terms of the GNU Affero General Public License, version 3,
25
 * as published by the Free Software Foundation.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
 * GNU Affero General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
33
 * along with this program. If not, see <http://www.gnu.org/licenses/>
34
 *
35
 */
36
namespace OCA\DAV\CardDAV;
37
38
use OCA\DAV\Connector\Sabre\Principal;
39
use OCA\DAV\DAV\Sharing\Backend;
40
use OCA\DAV\DAV\Sharing\IShareable;
41
use OCA\DAV\Events\AddressBookCreatedEvent;
42
use OCA\DAV\Events\AddressBookDeletedEvent;
43
use OCA\DAV\Events\AddressBookShareUpdatedEvent;
44
use OCA\DAV\Events\AddressBookUpdatedEvent;
45
use OCA\DAV\Events\CardCreatedEvent;
46
use OCA\DAV\Events\CardDeletedEvent;
47
use OCA\DAV\Events\CardMovedEvent;
48
use OCA\DAV\Events\CardUpdatedEvent;
49
use OCP\AppFramework\Db\TTransactional;
50
use OCP\DB\Exception;
51
use OCP\DB\QueryBuilder\IQueryBuilder;
52
use OCP\EventDispatcher\IEventDispatcher;
53
use OCP\IDBConnection;
54
use OCP\IGroupManager;
55
use OCP\IUserManager;
56
use PDO;
57
use Sabre\CardDAV\Backend\BackendInterface;
58
use Sabre\CardDAV\Backend\SyncSupport;
59
use Sabre\CardDAV\Plugin;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, OCA\DAV\CardDAV\Plugin. Consider defining an alias.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
60
use Sabre\DAV\Exception\BadRequest;
61
use Sabre\VObject\Component\VCard;
62
use Sabre\VObject\Reader;
63
64
class CardDavBackend implements BackendInterface, SyncSupport {
65
	use TTransactional;
66
67
	public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
68
	public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
69
70
	private Principal $principalBackend;
71
	private string $dbCardsTable = 'cards';
72
	private string $dbCardsPropertiesTable = 'cards_properties';
73
	private IDBConnection $db;
74
	private Backend $sharingBackend;
75
76
	/** @var array properties to index */
77
	public static array $indexProperties = [
78
		'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
79
		'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO',
80
		'CLOUD', 'X-SOCIALPROFILE'];
81
82
	/**
83
	 * @var string[] Map of uid => display name
84
	 */
85
	protected array $userDisplayNames;
86
	private IUserManager $userManager;
87
	private IEventDispatcher $dispatcher;
88
	private array $etagCache = [];
89
90
	/**
91
	 * CardDavBackend constructor.
92
	 *
93
	 * @param IDBConnection $db
94
	 * @param Principal $principalBackend
95
	 * @param IUserManager $userManager
96
	 * @param IGroupManager $groupManager
97
	 * @param IEventDispatcher $dispatcher
98
	 */
99
	public function __construct(IDBConnection $db,
100
								Principal $principalBackend,
101
								IUserManager $userManager,
102
								IGroupManager $groupManager,
103
								IEventDispatcher $dispatcher) {
104
		$this->db = $db;
105
		$this->principalBackend = $principalBackend;
106
		$this->userManager = $userManager;
107
		$this->dispatcher = $dispatcher;
108
		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
109
	}
110
111
	/**
112
	 * Return the number of address books for a principal
113
	 *
114
	 * @param $principalUri
115
	 * @return int
116
	 */
117
	public function getAddressBooksForUserCount($principalUri) {
118
		$principalUri = $this->convertPrincipal($principalUri, true);
119
		$query = $this->db->getQueryBuilder();
120
		$query->select($query->func()->count('*'))
121
			->from('addressbooks')
122
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
123
124
		$result = $query->executeQuery();
125
		$column = (int) $result->fetchOne();
126
		$result->closeCursor();
127
		return $column;
128
	}
129
130
	/**
131
	 * Returns the list of address books for a specific user.
132
	 *
133
	 * Every addressbook should have the following properties:
134
	 *   id - an arbitrary unique id
135
	 *   uri - the 'basename' part of the url
136
	 *   principaluri - Same as the passed parameter
137
	 *
138
	 * Any additional clark-notation property may be passed besides this. Some
139
	 * common ones are :
140
	 *   {DAV:}displayname
141
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
142
	 *   {http://calendarserver.org/ns/}getctag
143
	 *
144
	 * @param string $principalUri
145
	 * @return array
146
	 */
147
	public function getAddressBooksForUser($principalUri) {
148
		return $this->atomic(function () use ($principalUri) {
149
			$principalUriOriginal = $principalUri;
150
			$principalUri = $this->convertPrincipal($principalUri, true);
151
			$query = $this->db->getQueryBuilder();
152
			$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
153
				->from('addressbooks')
154
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
155
156
			$addressBooks = [];
157
158
			$result = $query->execute();
159
			while ($row = $result->fetch()) {
160
				$addressBooks[$row['id']] = [
161
					'id' => $row['id'],
162
					'uri' => $row['uri'],
163
					'principaluri' => $this->convertPrincipal($row['principaluri'], false),
164
					'{DAV:}displayname' => $row['displayname'],
165
					'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
166
					'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
167
					'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
168
				];
169
170
				$this->addOwnerPrincipal($addressBooks[$row['id']]);
171
			}
172
			$result->closeCursor();
173
174
			// query for shared addressbooks
175
			$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
176
			$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
177
178
			$principals[] = $principalUri;
179
180
			$query = $this->db->getQueryBuilder();
181
			$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
182
				->from('dav_shares', 's')
183
				->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
184
				->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
185
				->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
186
				->setParameter('type', 'addressbook')
187
				->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
188
				->execute();
189
190
			$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
191
			while ($row = $result->fetch()) {
192
				if ($row['principaluri'] === $principalUri) {
193
					continue;
194
				}
195
196
				$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
197
				if (isset($addressBooks[$row['id']])) {
198
					if ($readOnly) {
199
						// New share can not have more permissions then the old one.
200
						continue;
201
					}
202
					if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
203
						$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
204
						// Old share is already read-write, no more permissions can be gained
205
						continue;
206
					}
207
				}
208
209
				[, $name] = \Sabre\Uri\split($row['principaluri']);
210
				$uri = $row['uri'] . '_shared_by_' . $name;
211
				$displayName = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? $name ?? '') . ')';
212
213
				$addressBooks[$row['id']] = [
214
					'id' => $row['id'],
215
					'uri' => $uri,
216
					'principaluri' => $principalUriOriginal,
217
					'{DAV:}displayname' => $displayName,
218
					'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
219
					'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
220
					'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
221
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
222
					$readOnlyPropertyName => $readOnly,
223
				];
224
225
				$this->addOwnerPrincipal($addressBooks[$row['id']]);
226
			}
227
			$result->closeCursor();
228
229
			return array_values($addressBooks);
230
		}, $this->db);
231
	}
232
233
	public function getUsersOwnAddressBooks($principalUri) {
234
		$principalUri = $this->convertPrincipal($principalUri, true);
235
		$query = $this->db->getQueryBuilder();
236
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
237
			->from('addressbooks')
238
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
239
240
		$addressBooks = [];
241
242
		$result = $query->execute();
243
		while ($row = $result->fetch()) {
244
			$addressBooks[$row['id']] = [
245
				'id' => $row['id'],
246
				'uri' => $row['uri'],
247
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
248
				'{DAV:}displayname' => $row['displayname'],
249
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
250
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
251
				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
252
			];
253
254
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
255
		}
256
		$result->closeCursor();
257
258
		return array_values($addressBooks);
259
	}
260
261
	/**
262
	 * @param int $addressBookId
263
	 */
264
	public function getAddressBookById(int $addressBookId): ?array {
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, IQueryBuilder::PARAM_INT)))
269
			->executeQuery();
270
		$row = $result->fetch();
271
		$result->closeCursor();
272
		if (!$row) {
273
			return null;
274
		}
275
276
		$addressBook = [
277
			'id' => $row['id'],
278
			'uri' => $row['uri'],
279
			'principaluri' => $row['principaluri'],
280
			'{DAV:}displayname' => $row['displayname'],
281
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
282
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
283
			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
284
		];
285
286
		$this->addOwnerPrincipal($addressBook);
287
288
		return $addressBook;
289
	}
290
291
	public function getAddressBooksByUri(string $principal, string $addressBookUri): ?array {
292
		$query = $this->db->getQueryBuilder();
293
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
294
			->from('addressbooks')
295
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
296
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
297
			->setMaxResults(1)
298
			->executeQuery();
299
300
		$row = $result->fetch();
301
		$result->closeCursor();
302
		if ($row === false) {
303
			return null;
304
		}
305
306
		$addressBook = [
307
			'id' => $row['id'],
308
			'uri' => $row['uri'],
309
			'principaluri' => $row['principaluri'],
310
			'{DAV:}displayname' => $row['displayname'],
311
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
312
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
313
			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
314
		];
315
316
		// system address books are always read only
317
		if ($principal === 'principals/system/system') {
318
			$addressBook['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'] = true;
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
	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
343
		$this->atomic(function () use ($addressBookId, $propPatch) {
344
			$supportedProperties = [
345
				'{DAV:}displayname',
346
				'{' . Plugin::NS_CARDDAV . '}addressbook-description',
347
			];
348
349
			$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
350
				$updates = [];
351
				foreach ($mutations as $property => $newValue) {
352
					switch ($property) {
353
						case '{DAV:}displayname':
354
							$updates['displayname'] = $newValue;
355
							break;
356
						case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
357
							$updates['description'] = $newValue;
358
							break;
359
					}
360
				}
361
				$query = $this->db->getQueryBuilder();
362
				$query->update('addressbooks');
363
364
				foreach ($updates as $key => $value) {
365
					$query->set($key, $query->createNamedParameter($value));
366
				}
367
				$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
368
					->executeStatement();
369
370
				$this->addChange($addressBookId, "", 2);
0 ignored issues
show
Bug introduced by
$addressBookId of type string is incompatible with the type integer expected by parameter $addressBookId of OCA\DAV\CardDAV\CardDavBackend::addChange(). ( Ignorable by Annotation )

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

370
				$this->addChange(/** @scrutinizer ignore-type */ $addressBookId, "", 2);
Loading history...
371
372
				$addressBookRow = $this->getAddressBookById((int)$addressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $addressBookRow is correct as $this->getAddressBookById((int)$addressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
373
				$shares = $this->getShares((int)$addressBookId);
374
				$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
0 ignored issues
show
Bug introduced by
$addressBookRow of type null is incompatible with the type array expected by parameter $addressBookData of OCA\DAV\Events\AddressBo...tedEvent::__construct(). ( Ignorable by Annotation )

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

374
				$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, /** @scrutinizer ignore-type */ $addressBookRow, $shares, $mutations));
Loading history...
375
376
				return true;
377
			});
378
		}, $this->db);
379
	}
380
381
	/**
382
	 * Creates a new address book
383
	 *
384
	 * @param string $principalUri
385
	 * @param string $url Just the 'basename' of the url.
386
	 * @param array $properties
387
	 * @return int
388
	 * @throws BadRequest
389
	 */
390
	public function createAddressBook($principalUri, $url, array $properties) {
391
		if (strlen($url) > 255) {
392
			throw new BadRequest('URI too long. Address book not created');
393
		}
394
395
		$values = [
396
			'displayname' => null,
397
			'description' => null,
398
			'principaluri' => $principalUri,
399
			'uri' => $url,
400
			'synctoken' => 1
401
		];
402
403
		foreach ($properties as $property => $newValue) {
404
			switch ($property) {
405
				case '{DAV:}displayname':
406
					$values['displayname'] = $newValue;
407
					break;
408
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
409
					$values['description'] = $newValue;
410
					break;
411
				default:
412
					throw new BadRequest('Unknown property: ' . $property);
413
			}
414
		}
415
416
		// Fallback to make sure the displayname is set. Some clients may refuse
417
		// to work with addressbooks not having a displayname.
418
		if (is_null($values['displayname'])) {
419
			$values['displayname'] = $url;
420
		}
421
422
		[$addressBookId, $addressBookRow] = $this->atomic(function () use ($values) {
423
			$query = $this->db->getQueryBuilder();
424
			$query->insert('addressbooks')
425
				->values([
426
					'uri' => $query->createParameter('uri'),
427
					'displayname' => $query->createParameter('displayname'),
428
					'description' => $query->createParameter('description'),
429
					'principaluri' => $query->createParameter('principaluri'),
430
					'synctoken' => $query->createParameter('synctoken'),
431
				])
432
				->setParameters($values)
433
				->execute();
434
435
			$addressBookId = $query->getLastInsertId();
436
			return [
437
				$addressBookId,
438
				$this->getAddressBookById($addressBookId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getAddressBookById($addressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
439
			];
440
		}, $this->db);
441
442
		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent($addressBookId, $addressBookRow));
443
444
		return $addressBookId;
445
	}
446
447
	/**
448
	 * Deletes an entire addressbook and all its contents
449
	 *
450
	 * @param mixed $addressBookId
451
	 * @return void
452
	 */
453
	public function deleteAddressBook($addressBookId) {
454
		$this->atomic(function () use ($addressBookId) {
455
			$addressBookId = (int)$addressBookId;
456
			$addressBookData = $this->getAddressBookById($addressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $addressBookData is correct as $this->getAddressBookById($addressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
457
			$shares = $this->getShares($addressBookId);
458
459
			$query = $this->db->getQueryBuilder();
460
			$query->delete($this->dbCardsTable)
461
				->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
462
				->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT)
463
				->executeStatement();
464
465
			$query = $this->db->getQueryBuilder();
466
			$query->delete('addressbookchanges')
467
				->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
468
				->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT)
469
				->executeStatement();
470
471
			$query = $this->db->getQueryBuilder();
472
			$query->delete('addressbooks')
473
				->where($query->expr()->eq('id', $query->createParameter('id')))
474
				->setParameter('id', $addressBookId, IQueryBuilder::PARAM_INT)
475
				->executeStatement();
476
477
			$this->sharingBackend->deleteAllShares($addressBookId);
478
479
			$query = $this->db->getQueryBuilder();
480
			$query->delete($this->dbCardsPropertiesTable)
481
				->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId, IQueryBuilder::PARAM_INT)))
482
				->executeStatement();
483
484
			if ($addressBookData) {
0 ignored issues
show
introduced by
$addressBookData is of type null, thus it always evaluated to false.
Loading history...
485
				$this->dispatcher->dispatchTyped(new AddressBookDeletedEvent($addressBookId, $addressBookData, $shares));
486
			}
487
		}, $this->db);
488
	}
489
490
	/**
491
	 * Returns all cards for a specific addressbook id.
492
	 *
493
	 * This method should return the following properties for each card:
494
	 *   * carddata - raw vcard data
495
	 *   * uri - Some unique url
496
	 *   * lastmodified - A unix timestamp
497
	 *
498
	 * It's recommended to also return the following properties:
499
	 *   * etag - A unique etag. This must change every time the card changes.
500
	 *   * size - The size of the card in bytes.
501
	 *
502
	 * If these last two properties are provided, less time will be spent
503
	 * calculating them. If they are specified, you can also omit carddata.
504
	 * This may speed up certain requests, especially with large cards.
505
	 *
506
	 * @param mixed $addressbookId
507
	 * @return array
508
	 */
509
	public function getCards($addressbookId) {
510
		$query = $this->db->getQueryBuilder();
511
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
512
			->from($this->dbCardsTable)
513
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressbookId)));
514
515
		$cards = [];
516
517
		$result = $query->execute();
518
		while ($row = $result->fetch()) {
519
			$row['etag'] = '"' . $row['etag'] . '"';
520
521
			$modified = false;
522
			$row['carddata'] = $this->readBlob($row['carddata'], $modified);
523
			if ($modified) {
524
				$row['size'] = strlen($row['carddata']);
525
			}
526
527
			$cards[] = $row;
528
		}
529
		$result->closeCursor();
530
531
		return $cards;
532
	}
533
534
	/**
535
	 * Returns a specific card.
536
	 *
537
	 * The same set of properties must be returned as with getCards. The only
538
	 * exception is that 'carddata' is absolutely required.
539
	 *
540
	 * If the card does not exist, you must return false.
541
	 *
542
	 * @param mixed $addressBookId
543
	 * @param string $cardUri
544
	 * @return array
545
	 */
546
	public function getCard($addressBookId, $cardUri) {
547
		$query = $this->db->getQueryBuilder();
548
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
549
			->from($this->dbCardsTable)
550
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
551
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
552
			->setMaxResults(1);
553
554
		$result = $query->execute();
555
		$row = $result->fetch();
556
		if (!$row) {
557
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
558
		}
559
		$row['etag'] = '"' . $row['etag'] . '"';
560
561
		$modified = false;
562
		$row['carddata'] = $this->readBlob($row['carddata'], $modified);
563
		if ($modified) {
564
			$row['size'] = strlen($row['carddata']);
565
		}
566
567
		return $row;
568
	}
569
570
	/**
571
	 * Returns a list of cards.
572
	 *
573
	 * This method should work identical to getCard, but instead return all the
574
	 * cards in the list as an array.
575
	 *
576
	 * If the backend supports this, it may allow for some speed-ups.
577
	 *
578
	 * @param mixed $addressBookId
579
	 * @param array $uris
580
	 * @return array
581
	 */
582
	public function getMultipleCards($addressBookId, array $uris) {
583
		if (empty($uris)) {
584
			return [];
585
		}
586
587
		$chunks = array_chunk($uris, 100);
588
		$cards = [];
589
590
		$query = $this->db->getQueryBuilder();
591
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
592
			->from($this->dbCardsTable)
593
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
594
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
595
596
		foreach ($chunks as $uris) {
597
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
598
			$result = $query->execute();
599
600
			while ($row = $result->fetch()) {
601
				$row['etag'] = '"' . $row['etag'] . '"';
602
603
				$modified = false;
604
				$row['carddata'] = $this->readBlob($row['carddata'], $modified);
605
				if ($modified) {
606
					$row['size'] = strlen($row['carddata']);
607
				}
608
609
				$cards[] = $row;
610
			}
611
			$result->closeCursor();
612
		}
613
		return $cards;
614
	}
615
616
	/**
617
	 * Creates a new card.
618
	 *
619
	 * The addressbook id will be passed as the first argument. This is the
620
	 * same id as it is returned from the getAddressBooksForUser method.
621
	 *
622
	 * The cardUri is a base uri, and doesn't include the full path. The
623
	 * cardData argument is the vcard body, and is passed as a string.
624
	 *
625
	 * It is possible to return an ETag from this method. This ETag is for the
626
	 * newly created resource, and must be enclosed with double quotes (that
627
	 * is, the string itself must contain the double quotes).
628
	 *
629
	 * You should only return the ETag if you store the carddata as-is. If a
630
	 * subsequent GET request on the same card does not have the same body,
631
	 * byte-by-byte and you did return an ETag here, clients tend to get
632
	 * confused.
633
	 *
634
	 * If you don't return an ETag, you can just return null.
635
	 *
636
	 * @param mixed $addressBookId
637
	 * @param string $cardUri
638
	 * @param string $cardData
639
	 * @param bool $checkAlreadyExists
640
	 * @return string
641
	 */
642
	public function createCard($addressBookId, $cardUri, $cardData, bool $checkAlreadyExists = true) {
643
		$etag = md5($cardData);
644
		$uid = $this->getUID($cardData);
645
		return $this->atomic(function () use ($addressBookId, $cardUri, $cardData, $checkAlreadyExists, $etag, $uid) {
646
			if ($checkAlreadyExists) {
647
				$q = $this->db->getQueryBuilder();
648
				$q->select('uid')
649
					->from($this->dbCardsTable)
650
					->where($q->expr()->eq('addressbookid', $q->createNamedParameter($addressBookId)))
651
					->andWhere($q->expr()->eq('uid', $q->createNamedParameter($uid)))
652
					->setMaxResults(1);
653
				$result = $q->executeQuery();
654
				$count = (bool)$result->fetchOne();
655
				$result->closeCursor();
656
				if ($count) {
657
					throw new \Sabre\DAV\Exception\BadRequest('VCard object with uid already exists in this addressbook collection.');
658
				}
659
			}
660
661
			$query = $this->db->getQueryBuilder();
662
			$query->insert('cards')
663
				->values([
664
					'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
665
					'uri' => $query->createNamedParameter($cardUri),
666
					'lastmodified' => $query->createNamedParameter(time()),
667
					'addressbookid' => $query->createNamedParameter($addressBookId),
668
					'size' => $query->createNamedParameter(strlen($cardData)),
669
					'etag' => $query->createNamedParameter($etag),
670
					'uid' => $query->createNamedParameter($uid),
671
				])
672
				->execute();
673
674
			$etagCacheKey = "$addressBookId#$cardUri";
675
			$this->etagCache[$etagCacheKey] = $etag;
676
677
			$this->addChange($addressBookId, $cardUri, 1);
678
			$this->updateProperties($addressBookId, $cardUri, $cardData);
679
680
			$addressBookData = $this->getAddressBookById($addressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $addressBookData is correct as $this->getAddressBookById($addressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
681
			$shares = $this->getShares($addressBookId);
682
			$objectRow = $this->getCard($addressBookId, $cardUri);
683
			$this->dispatcher->dispatchTyped(new CardCreatedEvent($addressBookId, $addressBookData, $shares, $objectRow));
0 ignored issues
show
Bug introduced by
$addressBookData of type null is incompatible with the type array expected by parameter $addressBookData of OCA\DAV\Events\CardCreatedEvent::__construct(). ( Ignorable by Annotation )

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

683
			$this->dispatcher->dispatchTyped(new CardCreatedEvent($addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $shares, $objectRow));
Loading history...
684
685
			return '"' . $etag . '"';
686
		}, $this->db);
687
	}
688
689
	/**
690
	 * Updates a card.
691
	 *
692
	 * The addressbook id will be passed as the first argument. This is the
693
	 * same id as it is returned from the getAddressBooksForUser method.
694
	 *
695
	 * The cardUri is a base uri, and doesn't include the full path. The
696
	 * cardData argument is the vcard body, and is passed as a string.
697
	 *
698
	 * It is possible to return an ETag from this method. This ETag should
699
	 * match that of the updated resource, and must be enclosed with double
700
	 * quotes (that is: the string itself must contain the actual quotes).
701
	 *
702
	 * You should only return the ETag if you store the carddata as-is. If a
703
	 * subsequent GET request on the same card does not have the same body,
704
	 * byte-by-byte and you did return an ETag here, clients tend to get
705
	 * confused.
706
	 *
707
	 * If you don't return an ETag, you can just return null.
708
	 *
709
	 * @param mixed $addressBookId
710
	 * @param string $cardUri
711
	 * @param string $cardData
712
	 * @return string
713
	 */
714
	public function updateCard($addressBookId, $cardUri, $cardData) {
715
		$uid = $this->getUID($cardData);
716
		$etag = md5($cardData);
717
718
		return $this->atomic(function () use ($addressBookId, $cardUri, $cardData, $uid, $etag) {
719
			$query = $this->db->getQueryBuilder();
720
721
			// check for recently stored etag and stop if it is the same
722
			$etagCacheKey = "$addressBookId#$cardUri";
723
			if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
724
				return '"' . $etag . '"';
725
			}
726
727
			$query->update($this->dbCardsTable)
728
				->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
729
				->set('lastmodified', $query->createNamedParameter(time()))
730
				->set('size', $query->createNamedParameter(strlen($cardData)))
731
				->set('etag', $query->createNamedParameter($etag))
732
				->set('uid', $query->createNamedParameter($uid))
733
				->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
734
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
735
				->execute();
736
737
			$this->etagCache[$etagCacheKey] = $etag;
738
739
			$this->addChange($addressBookId, $cardUri, 2);
740
			$this->updateProperties($addressBookId, $cardUri, $cardData);
741
742
			$addressBookData = $this->getAddressBookById($addressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $addressBookData is correct as $this->getAddressBookById($addressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
743
			$shares = $this->getShares($addressBookId);
744
			$objectRow = $this->getCard($addressBookId, $cardUri);
745
			$this->dispatcher->dispatchTyped(new CardUpdatedEvent($addressBookId, $addressBookData, $shares, $objectRow));
0 ignored issues
show
Bug introduced by
$addressBookData of type null is incompatible with the type array expected by parameter $addressBookData of OCA\DAV\Events\CardUpdatedEvent::__construct(). ( Ignorable by Annotation )

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

745
			$this->dispatcher->dispatchTyped(new CardUpdatedEvent($addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $shares, $objectRow));
Loading history...
746
			return '"' . $etag . '"';
747
		}, $this->db);
748
	}
749
750
	/**
751
	 * @throws Exception
752
	 */
753
	public function moveCard(int $sourceAddressBookId, int $targetAddressBookId, string $cardUri, string $oldPrincipalUri): bool {
754
		return $this->atomic(function () use ($sourceAddressBookId, $targetAddressBookId, $cardUri, $oldPrincipalUri) {
0 ignored issues
show
Unused Code introduced by
The import $oldPrincipalUri is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
755
			$card = $this->getCard($sourceAddressBookId, $cardUri);
756
			if (empty($card)) {
757
				return false;
758
			}
759
760
			$query = $this->db->getQueryBuilder();
761
			$query->update('cards')
762
				->set('addressbookid', $query->createNamedParameter($targetAddressBookId, IQueryBuilder::PARAM_INT))
763
				->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
764
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($sourceAddressBookId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
765
				->executeStatement();
766
767
			$this->purgeProperties($sourceAddressBookId, (int)$card['id']);
768
			$this->updateProperties($sourceAddressBookId, $card['uri'], $card['carddata']);
769
770
			$this->addChange($sourceAddressBookId, $card['uri'], 3);
771
			$this->addChange($targetAddressBookId, $card['uri'], 1);
772
773
			$card = $this->getCard($targetAddressBookId, $cardUri);
774
			// Card wasn't found - possibly because it was deleted in the meantime by a different client
775
			if (empty($card)) {
776
				return false;
777
			}
778
779
			$targetAddressBookRow = $this->getAddressBookById($targetAddressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $targetAddressBookRow is correct as $this->getAddressBookById($targetAddressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
780
			// the address book this card is being moved to does not exist any longer
781
			if (empty($targetAddressBookRow)) {
782
				return false;
783
			}
784
785
			$sourceShares = $this->getShares($sourceAddressBookId);
786
			$targetShares = $this->getShares($targetAddressBookId);
787
			$sourceAddressBookRow = $this->getAddressBookById($sourceAddressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $sourceAddressBookRow is correct as $this->getAddressBookById($sourceAddressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
788
			$this->dispatcher->dispatchTyped(new CardMovedEvent($sourceAddressBookId, $sourceAddressBookRow, $targetAddressBookId, $targetAddressBookRow, $sourceShares, $targetShares, $card));
0 ignored issues
show
Bug introduced by
$sourceAddressBookRow of type null is incompatible with the type array expected by parameter $sourceAddressBookData of OCA\DAV\Events\CardMovedEvent::__construct(). ( Ignorable by Annotation )

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

788
			$this->dispatcher->dispatchTyped(new CardMovedEvent($sourceAddressBookId, /** @scrutinizer ignore-type */ $sourceAddressBookRow, $targetAddressBookId, $targetAddressBookRow, $sourceShares, $targetShares, $card));
Loading history...
789
			return true;
790
		}, $this->db);
791
	}
792
793
	/**
794
	 * Deletes a card
795
	 *
796
	 * @param mixed $addressBookId
797
	 * @param string $cardUri
798
	 * @return bool
799
	 */
800
	public function deleteCard($addressBookId, $cardUri) {
801
		return $this->atomic(function () use ($addressBookId, $cardUri) {
802
			$addressBookData = $this->getAddressBookById($addressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $addressBookData is correct as $this->getAddressBookById($addressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
803
			$shares = $this->getShares($addressBookId);
804
			$objectRow = $this->getCard($addressBookId, $cardUri);
805
806
			try {
807
				$cardId = $this->getCardId($addressBookId, $cardUri);
808
			} catch (\InvalidArgumentException $e) {
809
				$cardId = null;
810
			}
811
			$query = $this->db->getQueryBuilder();
812
			$ret = $query->delete($this->dbCardsTable)
813
				->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
814
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
815
				->executeStatement();
816
817
			$this->addChange($addressBookId, $cardUri, 3);
818
819
			if ($ret === 1) {
820
				if ($cardId !== null) {
821
					$this->dispatcher->dispatchTyped(new CardDeletedEvent($addressBookId, $addressBookData, $shares, $objectRow));
0 ignored issues
show
Bug introduced by
$addressBookData of type null is incompatible with the type array expected by parameter $addressBookData of OCA\DAV\Events\CardDeletedEvent::__construct(). ( Ignorable by Annotation )

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

821
					$this->dispatcher->dispatchTyped(new CardDeletedEvent($addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $shares, $objectRow));
Loading history...
822
					$this->purgeProperties($addressBookId, $cardId);
823
				}
824
				return true;
825
			}
826
827
			return false;
828
		}, $this->db);
829
	}
830
831
	/**
832
	 * The getChanges method returns all the changes that have happened, since
833
	 * the specified syncToken in the specified address book.
834
	 *
835
	 * This function should return an array, such as the following:
836
	 *
837
	 * [
838
	 *   'syncToken' => 'The current synctoken',
839
	 *   'added'   => [
840
	 *      'new.txt',
841
	 *   ],
842
	 *   'modified'   => [
843
	 *      'modified.txt',
844
	 *   ],
845
	 *   'deleted' => [
846
	 *      'foo.php.bak',
847
	 *      'old.txt'
848
	 *   ]
849
	 * ];
850
	 *
851
	 * The returned syncToken property should reflect the *current* syncToken
852
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
853
	 * property. This is needed here too, to ensure the operation is atomic.
854
	 *
855
	 * If the $syncToken argument is specified as null, this is an initial
856
	 * sync, and all members should be reported.
857
	 *
858
	 * The modified property is an array of nodenames that have changed since
859
	 * the last token.
860
	 *
861
	 * The deleted property is an array with nodenames, that have been deleted
862
	 * from collection.
863
	 *
864
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
865
	 * 1, you only have to report changes that happened only directly in
866
	 * immediate descendants. If it's 2, it should also include changes from
867
	 * the nodes below the child collections. (grandchildren)
868
	 *
869
	 * The $limit argument allows a client to specify how many results should
870
	 * be returned at most. If the limit is not specified, it should be treated
871
	 * as infinite.
872
	 *
873
	 * If the limit (infinite or not) is higher than you're willing to return,
874
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
875
	 *
876
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
877
	 * return null.
878
	 *
879
	 * The limit is 'suggestive'. You are free to ignore it.
880
	 *
881
	 * @param string $addressBookId
882
	 * @param string $syncToken
883
	 * @param int $syncLevel
884
	 * @param int|null $limit
885
	 * @return array
886
	 */
887
	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
888
		// Current synctoken
889
		return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) {
0 ignored issues
show
Unused Code introduced by
The import $syncLevel is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
890
			$qb = $this->db->getQueryBuilder();
891
			$qb->select('synctoken')
892
				->from('addressbooks')
893
				->where(
894
					$qb->expr()->eq('id', $qb->createNamedParameter($addressBookId))
895
				);
896
			$stmt = $qb->executeQuery();
897
			$currentToken = $stmt->fetchOne();
898
			$stmt->closeCursor();
899
900
			if (is_null($currentToken)) {
901
				return [];
902
			}
903
904
			$result = [
905
				'syncToken' => $currentToken,
906
				'added' => [],
907
				'modified' => [],
908
				'deleted' => [],
909
			];
910
911
			if ($syncToken) {
912
				$qb = $this->db->getQueryBuilder();
913
				$qb->select('uri', 'operation')
914
					->from('addressbookchanges')
915
					->where(
916
						$qb->expr()->andX(
917
							$qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
918
							$qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
919
							$qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
920
						)
921
					)->orderBy('synctoken');
922
923
				if (is_int($limit) && $limit > 0) {
924
					$qb->setMaxResults($limit);
925
				}
926
927
				// Fetching all changes
928
				$stmt = $qb->executeQuery();
929
930
				$changes = [];
931
932
				// This loop ensures that any duplicates are overwritten, only the
933
				// last change on a node is relevant.
934
				while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
935
					$changes[$row['uri']] = $row['operation'];
936
				}
937
				$stmt->closeCursor();
938
939
				foreach ($changes as $uri => $operation) {
940
					switch ($operation) {
941
						case 1:
942
							$result['added'][] = $uri;
943
							break;
944
						case 2:
945
							$result['modified'][] = $uri;
946
							break;
947
						case 3:
948
							$result['deleted'][] = $uri;
949
							break;
950
					}
951
				}
952
			} else {
953
				$qb = $this->db->getQueryBuilder();
954
				$qb->select('uri')
955
					->from('cards')
956
					->where(
957
						$qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
958
					);
959
				// No synctoken supplied, this is the initial sync.
960
				$stmt = $qb->executeQuery();
961
				$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
962
				$stmt->closeCursor();
963
			}
964
			return $result;
965
		}, $this->db);
966
	}
967
968
	/**
969
	 * Adds a change record to the addressbookchanges table.
970
	 *
971
	 * @param mixed $addressBookId
972
	 * @param string $objectUri
973
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
974
	 * @return void
975
	 */
976
	protected function addChange(int $addressBookId, string $objectUri, int $operation): void {
977
		$this->atomic(function () use ($addressBookId, $objectUri, $operation) {
978
			$query = $this->db->getQueryBuilder();
979
			$query->select('synctoken')
980
				->from('addressbooks')
981
				->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)));
982
			$result = $query->executeQuery();
983
			$syncToken = (int)$result->fetchOne();
984
			$result->closeCursor();
985
986
			$query = $this->db->getQueryBuilder();
987
			$query->insert('addressbookchanges')
988
				->values([
989
					'uri' => $query->createNamedParameter($objectUri),
990
					'synctoken' => $query->createNamedParameter($syncToken),
991
					'addressbookid' => $query->createNamedParameter($addressBookId),
992
					'operation' => $query->createNamedParameter($operation),
993
				])
994
				->executeStatement();
995
996
			$query = $this->db->getQueryBuilder();
997
			$query->update('addressbooks')
998
				->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
999
				->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
1000
				->executeStatement();
1001
		}, $this->db);
1002
	}
1003
1004
	/**
1005
	 * @param resource|string $cardData
1006
	 * @param bool $modified
1007
	 * @return string
1008
	 */
1009
	private function readBlob($cardData, &$modified = false) {
1010
		if (is_resource($cardData)) {
1011
			$cardData = stream_get_contents($cardData);
1012
		}
1013
1014
		// Micro optimisation
1015
		// don't loop through
1016
		if (strpos($cardData, 'PHOTO:data:') === 0) {
1017
			return $cardData;
1018
		}
1019
1020
		$cardDataArray = explode("\r\n", $cardData);
1021
1022
		$cardDataFiltered = [];
1023
		$removingPhoto = false;
1024
		foreach ($cardDataArray as $line) {
1025
			if (strpos($line, 'PHOTO:data:') === 0
1026
				&& strpos($line, 'PHOTO:data:image/') !== 0) {
1027
				// Filter out PHOTO data of non-images
1028
				$removingPhoto = true;
1029
				$modified = true;
1030
				continue;
1031
			}
1032
1033
			if ($removingPhoto) {
1034
				if (strpos($line, ' ') === 0) {
1035
					continue;
1036
				}
1037
				// No leading space means this is a new property
1038
				$removingPhoto = false;
1039
			}
1040
1041
			$cardDataFiltered[] = $line;
1042
		}
1043
		return implode("\r\n", $cardDataFiltered);
1044
	}
1045
1046
	/**
1047
	 * @param list<array{href: string, commonName: string, readOnly: bool}> $add
1048
	 * @param list<string> $remove
0 ignored issues
show
Bug introduced by
The type OCA\DAV\CardDAV\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1049
	 */
1050
	public function updateShares(IShareable $shareable, array $add, array $remove): void {
1051
		$this->atomic(function () use ($shareable, $add, $remove) {
1052
			$addressBookId = $shareable->getResourceId();
1053
			$addressBookData = $this->getAddressBookById($addressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $addressBookData is correct as $this->getAddressBookById($addressBookId) targeting OCA\DAV\CardDAV\CardDavB...d::getAddressBookById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1054
			$oldShares = $this->getShares($addressBookId);
1055
1056
			$this->sharingBackend->updateShares($shareable, $add, $remove);
1057
1058
			$this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, $addressBookData, $oldShares, $add, $remove));
0 ignored issues
show
Bug introduced by
$addressBookData of type null is incompatible with the type array expected by parameter $addressBookData of OCA\DAV\Events\AddressBo...tedEvent::__construct(). ( Ignorable by Annotation )

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

1058
			$this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $oldShares, $add, $remove));
Loading history...
1059
		}, $this->db);
1060
	}
1061
1062
	/**
1063
	 * Search contacts in a specific address-book
1064
	 *
1065
	 * @param int $addressBookId
1066
	 * @param string $pattern which should match within the $searchProperties
1067
	 * @param array $searchProperties defines the properties within the query pattern should match
1068
	 * @param array $options = array() to define the search behavior
1069
	 * 	  - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
1070
	 *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1071
	 *    - 'limit' - Set a numeric limit for the search results
1072
	 *    - 'offset' - Set the offset for the limited search results
1073
	 *    - 'wildcard' - Whether the search should use wildcards
1074
	 * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
1075
	 * @return array an array of contacts which are arrays of key-value-pairs
1076
	 */
1077
	public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1078
		return $this->atomic(function () use ($addressBookId, $pattern, $searchProperties, $options) {
1079
			return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1080
		}, $this->db);
1081
	}
1082
1083
	/**
1084
	 * Search contacts in all address-books accessible by a user
1085
	 *
1086
	 * @param string $principalUri
1087
	 * @param string $pattern
1088
	 * @param array $searchProperties
1089
	 * @param array $options
1090
	 * @return array
1091
	 */
1092
	public function searchPrincipalUri(string $principalUri,
1093
									   string $pattern,
1094
									   array $searchProperties,
1095
									   array $options = []): array {
1096
		return $this->atomic(function () use ($principalUri, $pattern, $searchProperties, $options) {
1097
			$addressBookIds = array_map(static function ($row):int {
1098
				return (int) $row['id'];
1099
			}, $this->getAddressBooksForUser($principalUri));
1100
1101
			return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1102
		}, $this->db);
1103
	}
1104
1105
	/**
1106
	 * @param array $addressBookIds
1107
	 * @param string $pattern
1108
	 * @param array $searchProperties
1109
	 * @param array $options
1110
	 * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
1111
	 * @return array
1112
	 */
1113
	private function searchByAddressBookIds(array $addressBookIds,
1114
											string $pattern,
1115
											array $searchProperties,
1116
											array $options = []): array {
1117
		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1118
		$useWildcards = !\array_key_exists('wildcard', $options) || $options['wildcard'] !== false;
1119
1120
		$query2 = $this->db->getQueryBuilder();
1121
1122
		$addressBookOr = $query2->expr()->orX();
1123
		foreach ($addressBookIds as $addressBookId) {
1124
			$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1125
		}
1126
1127
		if ($addressBookOr->count() === 0) {
1128
			return [];
1129
		}
1130
1131
		$propertyOr = $query2->expr()->orX();
1132
		foreach ($searchProperties as $property) {
1133
			if ($escapePattern) {
1134
				if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1135
					// There can be no spaces in emails
1136
					continue;
1137
				}
1138
1139
				if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1140
					// There can be no chars in cloud ids which are not valid for user ids plus :/
1141
					// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1142
					continue;
1143
				}
1144
			}
1145
1146
			$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1147
		}
1148
1149
		if ($propertyOr->count() === 0) {
1150
			return [];
1151
		}
1152
1153
		$query2->selectDistinct('cp.cardid')
1154
			->from($this->dbCardsPropertiesTable, 'cp')
1155
			->andWhere($addressBookOr)
1156
			->andWhere($propertyOr);
1157
1158
		// No need for like when the pattern is empty
1159
		if ('' !== $pattern) {
1160
			if (!$useWildcards) {
1161
				$query2->andWhere($query2->expr()->eq('cp.value', $query2->createNamedParameter($pattern)));
1162
			} elseif (!$escapePattern) {
1163
				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1164
			} else {
1165
				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1166
			}
1167
		}
1168
1169
		if (isset($options['limit'])) {
1170
			$query2->setMaxResults($options['limit']);
1171
		}
1172
		if (isset($options['offset'])) {
1173
			$query2->setFirstResult($options['offset']);
1174
		}
1175
1176
		$result = $query2->execute();
1177
		$matches = $result->fetchAll();
1178
		$result->closeCursor();
1179
		$matches = array_map(function ($match) {
1180
			return (int)$match['cardid'];
1181
		}, $matches);
1182
1183
		$cards = [];
1184
		$query = $this->db->getQueryBuilder();
1185
		$query->select('c.addressbookid', 'c.carddata', 'c.uri')
1186
			->from($this->dbCardsTable, 'c')
1187
			->where($query->expr()->in('c.id', $query->createParameter('matches')));
1188
1189
		foreach (array_chunk($matches, 1000) as $matchesChunk) {
1190
			$query->setParameter('matches', $matchesChunk, IQueryBuilder::PARAM_INT_ARRAY);
1191
			$result = $query->executeQuery();
1192
			$cards = array_merge($cards, $result->fetchAll());
1193
			$result->closeCursor();
1194
		}
1195
1196
		return array_map(function ($array) {
1197
			$array['addressbookid'] = (int) $array['addressbookid'];
1198
			$modified = false;
1199
			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
1200
			if ($modified) {
1201
				$array['size'] = strlen($array['carddata']);
1202
			}
1203
			return $array;
1204
		}, $cards);
1205
	}
1206
1207
	/**
1208
	 * @param int $bookId
1209
	 * @param string $name
1210
	 * @return array
1211
	 */
1212
	public function collectCardProperties($bookId, $name) {
1213
		$query = $this->db->getQueryBuilder();
1214
		$result = $query->selectDistinct('value')
1215
			->from($this->dbCardsPropertiesTable)
1216
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1217
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1218
			->execute();
1219
1220
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
1221
		$result->closeCursor();
1222
1223
		return $all;
1224
	}
1225
1226
	/**
1227
	 * get URI from a given contact
1228
	 *
1229
	 * @param int $id
1230
	 * @return string
1231
	 */
1232
	public function getCardUri($id) {
1233
		$query = $this->db->getQueryBuilder();
1234
		$query->select('uri')->from($this->dbCardsTable)
1235
			->where($query->expr()->eq('id', $query->createParameter('id')))
1236
			->setParameter('id', $id);
1237
1238
		$result = $query->execute();
1239
		$uri = $result->fetch();
1240
		$result->closeCursor();
1241
1242
		if (!isset($uri['uri'])) {
1243
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1244
		}
1245
1246
		return $uri['uri'];
1247
	}
1248
1249
	/**
1250
	 * return contact with the given URI
1251
	 *
1252
	 * @param int $addressBookId
1253
	 * @param string $uri
1254
	 * @returns array
1255
	 */
1256
	public function getContact($addressBookId, $uri) {
1257
		$result = [];
1258
		$query = $this->db->getQueryBuilder();
1259
		$query->select('*')->from($this->dbCardsTable)
1260
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1261
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1262
		$queryResult = $query->execute();
1263
		$contact = $queryResult->fetch();
1264
		$queryResult->closeCursor();
1265
1266
		if (is_array($contact)) {
1267
			$modified = false;
1268
			$contact['etag'] = '"' . $contact['etag'] . '"';
1269
			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1270
			if ($modified) {
1271
				$contact['size'] = strlen($contact['carddata']);
1272
			}
1273
1274
			$result = $contact;
1275
		}
1276
1277
		return $result;
1278
	}
1279
1280
	/**
1281
	 * Returns the list of people whom this address book is shared with.
1282
	 *
1283
	 * Every element in this array should have the following properties:
1284
	 *   * href - Often a mailto: address
1285
	 *   * commonName - Optional, for example a first + last name
1286
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1287
	 *   * readOnly - boolean
1288
	 *
1289
	 * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
1290
	 */
1291
	public function getShares(int $addressBookId): array {
1292
		return $this->sharingBackend->getShares($addressBookId);
1293
	}
1294
1295
	/**
1296
	 * update properties table
1297
	 *
1298
	 * @param int $addressBookId
1299
	 * @param string $cardUri
1300
	 * @param string $vCardSerialized
1301
	 */
1302
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1303
		$this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized) {
1304
			$cardId = $this->getCardId($addressBookId, $cardUri);
1305
			$vCard = $this->readCard($vCardSerialized);
1306
1307
			$this->purgeProperties($addressBookId, $cardId);
1308
1309
			$query = $this->db->getQueryBuilder();
1310
			$query->insert($this->dbCardsPropertiesTable)
1311
				->values(
1312
					[
1313
						'addressbookid' => $query->createNamedParameter($addressBookId),
1314
						'cardid' => $query->createNamedParameter($cardId),
1315
						'name' => $query->createParameter('name'),
1316
						'value' => $query->createParameter('value'),
1317
						'preferred' => $query->createParameter('preferred')
1318
					]
1319
				);
1320
1321
			foreach ($vCard->children() as $property) {
1322
				if (!in_array($property->name, self::$indexProperties)) {
1323
					continue;
1324
				}
1325
				$preferred = 0;
1326
				foreach ($property->parameters as $parameter) {
1327
					if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1328
						$preferred = 1;
1329
						break;
1330
					}
1331
				}
1332
				$query->setParameter('name', $property->name);
1333
				$query->setParameter('value', mb_strcut($property->getValue(), 0, 254));
1334
				$query->setParameter('preferred', $preferred);
1335
				$query->execute();
1336
			}
1337
		}, $this->db);
1338
	}
1339
1340
	/**
1341
	 * read vCard data into a vCard object
1342
	 *
1343
	 * @param string $cardData
1344
	 * @return VCard
1345
	 */
1346
	protected function readCard($cardData) {
1347
		return Reader::read($cardData);
1348
	}
1349
1350
	/**
1351
	 * delete all properties from a given card
1352
	 *
1353
	 * @param int $addressBookId
1354
	 * @param int $cardId
1355
	 */
1356
	protected function purgeProperties($addressBookId, $cardId) {
1357
		$query = $this->db->getQueryBuilder();
1358
		$query->delete($this->dbCardsPropertiesTable)
1359
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1360
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1361
		$query->execute();
1362
	}
1363
1364
	/**
1365
	 * Get ID from a given contact
1366
	 */
1367
	protected function getCardId(int $addressBookId, string $uri): int {
1368
		$query = $this->db->getQueryBuilder();
1369
		$query->select('id')->from($this->dbCardsTable)
1370
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1371
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1372
1373
		$result = $query->execute();
1374
		$cardIds = $result->fetch();
1375
		$result->closeCursor();
1376
1377
		if (!isset($cardIds['id'])) {
1378
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1379
		}
1380
1381
		return (int)$cardIds['id'];
1382
	}
1383
1384
	/**
1385
	 * For shared address books the sharee is set in the ACL of the address book
1386
	 *
1387
	 * @param int $addressBookId
1388
	 * @param list<array{privilege: string, principal: string, protected: bool}> $acl
1389
	 * @return list<array{privilege: string, principal: string, protected: bool}>
1390
	 */
1391
	public function applyShareAcl(int $addressBookId, array $acl): array {
1392
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1393
	}
1394
1395
	/**
1396
	 * @throws \InvalidArgumentException
1397
	 */
1398
	public function pruneOutdatedSyncTokens(int $keep = 10_000): int {
0 ignored issues
show
Bug introduced by
The constant OCA\DAV\CardDAV\10_000 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1399
		if ($keep < 0) {
1400
			throw new \InvalidArgumentException();
1401
		}
1402
1403
		$query = $this->db->getQueryBuilder();
1404
		$query->select($query->func()->max('id'))
1405
			->from('addressbookchanges');
1406
1407
		$maxId =  $query->executeQuery()->fetchOne();
1408
		if (!$maxId || $maxId < $keep) {
1409
		    return 0;
1410
		}
1411
1412
		$query = $this->db->getQueryBuilder();
1413
		$query->delete('addressbookchanges')
1414
			->where($query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1415
		return $query->executeStatement();
1416
	}
1417
1418
	private function convertPrincipal(string $principalUri, bool $toV2): string {
1419
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1420
			[, $name] = \Sabre\Uri\split($principalUri);
1421
			if ($toV2 === true) {
1422
				return "principals/users/$name";
1423
			}
1424
			return "principals/$name";
1425
		}
1426
		return $principalUri;
1427
	}
1428
1429
	private function addOwnerPrincipal(array &$addressbookInfo): void {
1430
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1431
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1432
		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1433
			$uri = $addressbookInfo[$ownerPrincipalKey];
1434
		} else {
1435
			$uri = $addressbookInfo['principaluri'];
1436
		}
1437
1438
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1439
		if (isset($principalInformation['{DAV:}displayname'])) {
1440
			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1441
		}
1442
	}
1443
1444
	/**
1445
	 * Extract UID from vcard
1446
	 *
1447
	 * @param string $cardData the vcard raw data
1448
	 * @return string the uid
1449
	 * @throws BadRequest if no UID is available or vcard is empty
1450
	 */
1451
	private function getUID(string $cardData): string {
1452
		if ($cardData !== '') {
1453
			$vCard = Reader::read($cardData);
1454
			if ($vCard->UID) {
1455
				$uid = $vCard->UID->getValue();
1456
				return $uid;
1457
			}
1458
			// should already be handled, but just in case
1459
			throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1460
		}
1461
		// should already be handled, but just in case
1462
		throw new BadRequest('vCard can not be empty');
1463
	}
1464
}
1465