Passed
Push — master ( 7a1b45...fbf25e )
by Christoph
13:21 queued 10s
created

CardDavBackend::applyShareAcl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 Christoph Wurst <[email protected]>
9
 * @author Georg Ehrke <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author John Molakvoæ (skjnldsv) <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Stefan Weil <[email protected]>
16
 * @author Thomas Citharel <[email protected]>
17
 * @author Thomas Müller <[email protected]>
18
 *
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program. If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
35
namespace OCA\DAV\CardDAV;
36
37
use OCA\DAV\Connector\Sabre\Principal;
38
use OCA\DAV\DAV\Sharing\Backend;
39
use OCA\DAV\DAV\Sharing\IShareable;
40
use OCA\DAV\Events\AddressBookCreatedEvent;
41
use OCA\DAV\Events\AddressBookDeletedEvent;
42
use OCA\DAV\Events\AddressBookShareUpdatedEvent;
43
use OCA\DAV\Events\AddressBookUpdatedEvent;
44
use OCA\DAV\Events\CardCreatedEvent;
45
use OCA\DAV\Events\CardDeletedEvent;
46
use OCA\DAV\Events\CardUpdatedEvent;
47
use OCP\DB\QueryBuilder\IQueryBuilder;
48
use OCP\EventDispatcher\IEventDispatcher;
49
use OCP\IDBConnection;
50
use OCP\IGroupManager;
51
use OCP\IUser;
52
use OCP\IUserManager;
53
use PDO;
54
use Sabre\CardDAV\Backend\BackendInterface;
55
use Sabre\CardDAV\Backend\SyncSupport;
56
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...
57
use Sabre\DAV\Exception\BadRequest;
58
use Sabre\VObject\Component\VCard;
59
use Sabre\VObject\Reader;
60
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
61
use Symfony\Component\EventDispatcher\GenericEvent;
62
63
class CardDavBackend implements BackendInterface, SyncSupport {
64
	public const PERSONAL_ADDRESSBOOK_URI = 'contacts';
65
	public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
66
67
	/** @var Principal */
68
	private $principalBackend;
69
70
	/** @var string */
71
	private $dbCardsTable = 'cards';
72
73
	/** @var string */
74
	private $dbCardsPropertiesTable = 'cards_properties';
75
76
	/** @var IDBConnection */
77
	private $db;
78
79
	/** @var Backend */
80
	private $sharingBackend;
81
82
	/** @var array properties to index */
83
	public static $indexProperties = [
84
		'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
85
		'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'];
86
87
	/**
88
	 * @var string[] Map of uid => display name
89
	 */
90
	protected $userDisplayNames;
91
92
	/** @var IUserManager */
93
	private $userManager;
94
95
	/** @var IEventDispatcher */
96
	private $dispatcher;
97
98
	/** @var EventDispatcherInterface */
99
	private $legacyDispatcher;
100
101
	private $etagCache = [];
102
103
	/**
104
	 * CardDavBackend constructor.
105
	 *
106
	 * @param IDBConnection $db
107
	 * @param Principal $principalBackend
108
	 * @param IUserManager $userManager
109
	 * @param IGroupManager $groupManager
110
	 * @param IEventDispatcher $dispatcher
111
	 * @param EventDispatcherInterface $legacyDispatcher
112
	 */
113
	public function __construct(IDBConnection $db,
114
								Principal $principalBackend,
115
								IUserManager $userManager,
116
								IGroupManager $groupManager,
117
								IEventDispatcher $dispatcher,
118
								EventDispatcherInterface $legacyDispatcher) {
119
		$this->db = $db;
120
		$this->principalBackend = $principalBackend;
121
		$this->userManager = $userManager;
122
		$this->dispatcher = $dispatcher;
123
		$this->legacyDispatcher = $legacyDispatcher;
124
		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook');
125
	}
126
127
	/**
128
	 * Return the number of address books for a principal
129
	 *
130
	 * @param $principalUri
131
	 * @return int
132
	 */
133
	public function getAddressBooksForUserCount($principalUri) {
134
		$principalUri = $this->convertPrincipal($principalUri, true);
135
		$query = $this->db->getQueryBuilder();
136
		$query->select($query->func()->count('*'))
137
			->from('addressbooks')
138
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
139
140
		$result = $query->execute();
141
		$column = (int) $result->fetchColumn();
142
		$result->closeCursor();
143
		return $column;
144
	}
145
146
	/**
147
	 * Returns the list of address books for a specific user.
148
	 *
149
	 * Every addressbook should have the following properties:
150
	 *   id - an arbitrary unique id
151
	 *   uri - the 'basename' part of the url
152
	 *   principaluri - Same as the passed parameter
153
	 *
154
	 * Any additional clark-notation property may be passed besides this. Some
155
	 * common ones are :
156
	 *   {DAV:}displayname
157
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
158
	 *   {http://calendarserver.org/ns/}getctag
159
	 *
160
	 * @param string $principalUri
161
	 * @return array
162
	 */
163
	public function getAddressBooksForUser($principalUri) {
164
		$principalUriOriginal = $principalUri;
165
		$principalUri = $this->convertPrincipal($principalUri, true);
166
		$query = $this->db->getQueryBuilder();
167
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
168
			->from('addressbooks')
169
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
170
171
		$addressBooks = [];
172
173
		$result = $query->execute();
174
		while ($row = $result->fetch()) {
175
			$addressBooks[$row['id']] = [
176
				'id' => $row['id'],
177
				'uri' => $row['uri'],
178
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
179
				'{DAV:}displayname' => $row['displayname'],
180
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
181
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
182
				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
183
			];
184
185
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
186
		}
187
		$result->closeCursor();
188
189
		// query for shared addressbooks
190
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
191
		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
192
193
		$principals[] = $principalUri;
194
195
		$query = $this->db->getQueryBuilder();
196
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
197
			->from('dav_shares', 's')
198
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
199
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
200
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
201
			->setParameter('type', 'addressbook')
202
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
203
			->execute();
204
205
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
206
		while ($row = $result->fetch()) {
207
			if ($row['principaluri'] === $principalUri) {
208
				continue;
209
			}
210
211
			$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
212
			if (isset($addressBooks[$row['id']])) {
213
				if ($readOnly) {
214
					// New share can not have more permissions then the old one.
215
					continue;
216
				}
217
				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
218
					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
219
					// Old share is already read-write, no more permissions can be gained
220
					continue;
221
				}
222
			}
223
224
			list(, $name) = \Sabre\Uri\split($row['principaluri']);
225
			$uri = $row['uri'] . '_shared_by_' . $name;
226
			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
227
228
			$addressBooks[$row['id']] = [
229
				'id' => $row['id'],
230
				'uri' => $uri,
231
				'principaluri' => $principalUriOriginal,
232
				'{DAV:}displayname' => $displayName,
233
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
234
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
235
				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
236
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
237
				$readOnlyPropertyName => $readOnly,
238
			];
239
240
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
241
		}
242
		$result->closeCursor();
243
244
		return array_values($addressBooks);
245
	}
246
247
	public function getUsersOwnAddressBooks($principalUri) {
248
		$principalUri = $this->convertPrincipal($principalUri, true);
249
		$query = $this->db->getQueryBuilder();
250
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
251
			->from('addressbooks')
252
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
253
254
		$addressBooks = [];
255
256
		$result = $query->execute();
257
		while ($row = $result->fetch()) {
258
			$addressBooks[$row['id']] = [
259
				'id' => $row['id'],
260
				'uri' => $row['uri'],
261
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
262
				'{DAV:}displayname' => $row['displayname'],
263
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
264
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
265
				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
266
			];
267
268
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
269
		}
270
		$result->closeCursor();
271
272
		return array_values($addressBooks);
273
	}
274
275
	private function getUserDisplayName($uid) {
276
		if (!isset($this->userDisplayNames[$uid])) {
277
			$user = $this->userManager->get($uid);
278
279
			if ($user instanceof IUser) {
280
				$this->userDisplayNames[$uid] = $user->getDisplayName();
281
			} else {
282
				$this->userDisplayNames[$uid] = $uid;
283
			}
284
		}
285
286
		return $this->userDisplayNames[$uid];
287
	}
288
289
	/**
290
	 * @param int $addressBookId
291
	 */
292
	public function getAddressBookById($addressBookId) {
293
		$query = $this->db->getQueryBuilder();
294
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
295
			->from('addressbooks')
296
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
297
			->execute();
298
299
		$row = $result->fetch();
300
		$result->closeCursor();
301
		if ($row === false) {
302
			return null;
303
		}
304
305
		$addressBook = [
306
			'id' => $row['id'],
307
			'uri' => $row['uri'],
308
			'principaluri' => $row['principaluri'],
309
			'{DAV:}displayname' => $row['displayname'],
310
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
311
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
312
			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
313
		];
314
315
		$this->addOwnerPrincipal($addressBook);
316
317
		return $addressBook;
318
	}
319
320
	/**
321
	 * @param $addressBookUri
322
	 * @return array|null
323
	 */
324
	public function getAddressBooksByUri($principal, $addressBookUri) {
325
		$query = $this->db->getQueryBuilder();
326
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
327
			->from('addressbooks')
328
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
329
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
330
			->setMaxResults(1)
331
			->execute();
332
333
		$row = $result->fetch();
334
		$result->closeCursor();
335
		if ($row === false) {
336
			return null;
337
		}
338
339
		$addressBook = [
340
			'id' => $row['id'],
341
			'uri' => $row['uri'],
342
			'principaluri' => $row['principaluri'],
343
			'{DAV:}displayname' => $row['displayname'],
344
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
345
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
346
			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
347
		];
348
349
		$this->addOwnerPrincipal($addressBook);
350
351
		return $addressBook;
352
	}
353
354
	/**
355
	 * Updates properties for an address book.
356
	 *
357
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
358
	 * To do the actual updates, you must tell this object which properties
359
	 * you're going to process with the handle() method.
360
	 *
361
	 * Calling the handle method is like telling the PropPatch object "I
362
	 * promise I can handle updating this property".
363
	 *
364
	 * Read the PropPatch documentation for more info and examples.
365
	 *
366
	 * @param string $addressBookId
367
	 * @param \Sabre\DAV\PropPatch $propPatch
368
	 * @return void
369
	 */
370
	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
371
		$supportedProperties = [
372
			'{DAV:}displayname',
373
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
374
		];
375
376
		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
377
			$updates = [];
378
			foreach ($mutations as $property => $newValue) {
379
				switch ($property) {
380
					case '{DAV:}displayname':
381
						$updates['displayname'] = $newValue;
382
						break;
383
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
384
						$updates['description'] = $newValue;
385
						break;
386
				}
387
			}
388
			$query = $this->db->getQueryBuilder();
389
			$query->update('addressbooks');
390
391
			foreach ($updates as $key => $value) {
392
				$query->set($key, $query->createNamedParameter($value));
393
			}
394
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
395
				->execute();
396
397
			$this->addChange($addressBookId, "", 2);
398
399
			$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...
400
			$shares = $this->getShares($addressBookId);
401
			$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

401
			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, /** @scrutinizer ignore-type */ $addressBookRow, $shares, $mutations));
Loading history...
402
403
			return true;
404
		});
405
	}
406
407
	/**
408
	 * Creates a new address book
409
	 *
410
	 * @param string $principalUri
411
	 * @param string $url Just the 'basename' of the url.
412
	 * @param array $properties
413
	 * @return int
414
	 * @throws BadRequest
415
	 */
416
	public function createAddressBook($principalUri, $url, array $properties) {
417
		$values = [
418
			'displayname' => null,
419
			'description' => null,
420
			'principaluri' => $principalUri,
421
			'uri' => $url,
422
			'synctoken' => 1
423
		];
424
425
		foreach ($properties as $property => $newValue) {
426
			switch ($property) {
427
				case '{DAV:}displayname':
428
					$values['displayname'] = $newValue;
429
					break;
430
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
431
					$values['description'] = $newValue;
432
					break;
433
				default:
434
					throw new BadRequest('Unknown property: ' . $property);
435
			}
436
		}
437
438
		// Fallback to make sure the displayname is set. Some clients may refuse
439
		// to work with addressbooks not having a displayname.
440
		if (is_null($values['displayname'])) {
441
			$values['displayname'] = $url;
442
		}
443
444
		$query = $this->db->getQueryBuilder();
445
		$query->insert('addressbooks')
446
			->values([
447
				'uri' => $query->createParameter('uri'),
448
				'displayname' => $query->createParameter('displayname'),
449
				'description' => $query->createParameter('description'),
450
				'principaluri' => $query->createParameter('principaluri'),
451
				'synctoken' => $query->createParameter('synctoken'),
452
			])
453
			->setParameters($values)
454
			->execute();
455
456
		$addressBookId = $query->getLastInsertId();
457
		$addressBookRow = $this->getAddressBookById($addressBookId);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $addressBookRow 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...
458
		$this->dispatcher->dispatchTyped(new AddressBookCreatedEvent((int)$addressBookId, $addressBookRow));
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

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

690
		$this->dispatcher->dispatchTyped(new CardCreatedEvent((int)$addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $shares, $objectRow));
Loading history...
691
		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CardDAV\CardDavBackend::createCard' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

692
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
693
                           dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',

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

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

Loading history...
693
				'addressBookId' => $addressBookId,
694
				'cardUri' => $cardUri,
695
				'cardData' => $cardData]));
696
697
		return '"' . $etag . '"';
698
	}
699
700
	/**
701
	 * Updates a card.
702
	 *
703
	 * The addressbook id will be passed as the first argument. This is the
704
	 * same id as it is returned from the getAddressBooksForUser method.
705
	 *
706
	 * The cardUri is a base uri, and doesn't include the full path. The
707
	 * cardData argument is the vcard body, and is passed as a string.
708
	 *
709
	 * It is possible to return an ETag from this method. This ETag should
710
	 * match that of the updated resource, and must be enclosed with double
711
	 * quotes (that is: the string itself must contain the actual quotes).
712
	 *
713
	 * You should only return the ETag if you store the carddata as-is. If a
714
	 * subsequent GET request on the same card does not have the same body,
715
	 * byte-by-byte and you did return an ETag here, clients tend to get
716
	 * confused.
717
	 *
718
	 * If you don't return an ETag, you can just return null.
719
	 *
720
	 * @param mixed $addressBookId
721
	 * @param string $cardUri
722
	 * @param string $cardData
723
	 * @return string
724
	 */
725
	public function updateCard($addressBookId, $cardUri, $cardData) {
726
		$uid = $this->getUID($cardData);
727
		$etag = md5($cardData);
728
		$query = $this->db->getQueryBuilder();
729
730
		// check for recently stored etag and stop if it is the same
731
		$etagCacheKey = "$addressBookId#$cardUri";
732
		if (isset($this->etagCache[$etagCacheKey]) && $this->etagCache[$etagCacheKey] === $etag) {
733
			return '"' . $etag . '"';
734
		}
735
736
		$query->update($this->dbCardsTable)
737
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
738
			->set('lastmodified', $query->createNamedParameter(time()))
739
			->set('size', $query->createNamedParameter(strlen($cardData)))
740
			->set('etag', $query->createNamedParameter($etag))
741
			->set('uid', $query->createNamedParameter($uid))
742
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
743
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
744
			->execute();
745
746
		$this->etagCache[$etagCacheKey] = $etag;
747
748
		$this->addChange($addressBookId, $cardUri, 2);
749
		$this->updateProperties($addressBookId, $cardUri, $cardData);
750
751
		$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...
752
		$shares = $this->getShares($addressBookId);
753
		$objectRow = $this->getCard($addressBookId, $cardUri);
754
		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$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

754
		$this->dispatcher->dispatchTyped(new CardUpdatedEvent((int)$addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $shares, $objectRow));
Loading history...
755
		$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CardDAV\CardDavBackend::updateCard' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

756
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
757
                           dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',

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

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

Loading history...
757
				'addressBookId' => $addressBookId,
758
				'cardUri' => $cardUri,
759
				'cardData' => $cardData]));
760
761
		return '"' . $etag . '"';
762
	}
763
764
	/**
765
	 * Deletes a card
766
	 *
767
	 * @param mixed $addressBookId
768
	 * @param string $cardUri
769
	 * @return bool
770
	 */
771
	public function deleteCard($addressBookId, $cardUri) {
772
		$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...
773
		$shares = $this->getShares($addressBookId);
774
		$objectRow = $this->getCard($addressBookId, $cardUri);
775
776
		try {
777
			$cardId = $this->getCardId($addressBookId, $cardUri);
778
		} catch (\InvalidArgumentException $e) {
779
			$cardId = null;
780
		}
781
		$query = $this->db->getQueryBuilder();
782
		$ret = $query->delete($this->dbCardsTable)
783
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
784
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
785
			->execute();
786
787
		$this->addChange($addressBookId, $cardUri, 3);
788
789
		if ($ret === 1) {
790
			if ($cardId !== null) {
791
				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$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

791
				$this->dispatcher->dispatchTyped(new CardDeletedEvent((int)$addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $shares, $objectRow));
Loading history...
792
				$this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CardDAV\CardDavBackend::deleteCard' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

793
				$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
794
                             dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',

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

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

Loading history...
794
						'addressBookId' => $addressBookId,
795
						'cardUri' => $cardUri]));
796
797
				$this->purgeProperties($addressBookId, $cardId);
798
			}
799
			return true;
800
		}
801
802
		return false;
803
	}
804
805
	/**
806
	 * The getChanges method returns all the changes that have happened, since
807
	 * the specified syncToken in the specified address book.
808
	 *
809
	 * This function should return an array, such as the following:
810
	 *
811
	 * [
812
	 *   'syncToken' => 'The current synctoken',
813
	 *   'added'   => [
814
	 *      'new.txt',
815
	 *   ],
816
	 *   'modified'   => [
817
	 *      'modified.txt',
818
	 *   ],
819
	 *   'deleted' => [
820
	 *      'foo.php.bak',
821
	 *      'old.txt'
822
	 *   ]
823
	 * ];
824
	 *
825
	 * The returned syncToken property should reflect the *current* syncToken
826
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
827
	 * property. This is needed here too, to ensure the operation is atomic.
828
	 *
829
	 * If the $syncToken argument is specified as null, this is an initial
830
	 * sync, and all members should be reported.
831
	 *
832
	 * The modified property is an array of nodenames that have changed since
833
	 * the last token.
834
	 *
835
	 * The deleted property is an array with nodenames, that have been deleted
836
	 * from collection.
837
	 *
838
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
839
	 * 1, you only have to report changes that happened only directly in
840
	 * immediate descendants. If it's 2, it should also include changes from
841
	 * the nodes below the child collections. (grandchildren)
842
	 *
843
	 * The $limit argument allows a client to specify how many results should
844
	 * be returned at most. If the limit is not specified, it should be treated
845
	 * as infinite.
846
	 *
847
	 * If the limit (infinite or not) is higher than you're willing to return,
848
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
849
	 *
850
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
851
	 * return null.
852
	 *
853
	 * The limit is 'suggestive'. You are free to ignore it.
854
	 *
855
	 * @param string $addressBookId
856
	 * @param string $syncToken
857
	 * @param int $syncLevel
858
	 * @param int $limit
859
	 * @return array
860
	 */
861
	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
862
		// Current synctoken
863
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
864
		$stmt->execute([$addressBookId]);
865
		$currentToken = $stmt->fetchColumn(0);
866
867
		if (is_null($currentToken)) {
868
			return null;
869
		}
870
871
		$result = [
872
			'syncToken' => $currentToken,
873
			'added' => [],
874
			'modified' => [],
875
			'deleted' => [],
876
		];
877
878
		if ($syncToken) {
879
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
880
			if ($limit > 0) {
881
				$query .= " LIMIT " . (int)$limit;
882
			}
883
884
			// Fetching all changes
885
			$stmt = $this->db->prepare($query);
886
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
887
888
			$changes = [];
889
890
			// This loop ensures that any duplicates are overwritten, only the
891
			// last change on a node is relevant.
892
			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
893
				$changes[$row['uri']] = $row['operation'];
894
			}
895
896
			foreach ($changes as $uri => $operation) {
897
				switch ($operation) {
898
					case 1:
899
						$result['added'][] = $uri;
900
						break;
901
					case 2:
902
						$result['modified'][] = $uri;
903
						break;
904
					case 3:
905
						$result['deleted'][] = $uri;
906
						break;
907
				}
908
			}
909
		} else {
910
			// No synctoken supplied, this is the initial sync.
911
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
912
			$stmt = $this->db->prepare($query);
913
			$stmt->execute([$addressBookId]);
914
915
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
916
		}
917
		return $result;
918
	}
919
920
	/**
921
	 * Adds a change record to the addressbookchanges table.
922
	 *
923
	 * @param mixed $addressBookId
924
	 * @param string $objectUri
925
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
926
	 * @return void
927
	 */
928
	protected function addChange($addressBookId, $objectUri, $operation) {
929
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
930
		$stmt = $this->db->prepare($sql);
931
		$stmt->execute([
932
			$objectUri,
933
			$addressBookId,
934
			$operation,
935
			$addressBookId
936
		]);
937
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
938
		$stmt->execute([
939
			$addressBookId
940
		]);
941
	}
942
943
	/**
944
	 * @param resource|string $cardData
945
	 * @param bool $modified
946
	 * @return string
947
	 */
948
	private function readBlob($cardData, &$modified = false) {
949
		if (is_resource($cardData)) {
950
			$cardData = stream_get_contents($cardData);
951
		}
952
953
		$cardDataArray = explode("\r\n", $cardData);
954
955
		$cardDataFiltered = [];
956
		$removingPhoto = false;
957
		foreach ($cardDataArray as $line) {
958
			if (strpos($line, 'PHOTO:data:') === 0
959
				&& strpos($line, 'PHOTO:data:image/') !== 0) {
960
				// Filter out PHOTO data of non-images
961
				$removingPhoto = true;
962
				$modified = true;
963
				continue;
964
			}
965
966
			if ($removingPhoto) {
967
				if (strpos($line, ' ') === 0) {
968
					continue;
969
				}
970
				// No leading space means this is a new property
971
				$removingPhoto = false;
972
			}
973
974
			$cardDataFiltered[] = $line;
975
		}
976
977
		return implode("\r\n", $cardDataFiltered);
978
	}
979
980
	/**
981
	 * @param IShareable $shareable
982
	 * @param string[] $add
983
	 * @param string[] $remove
984
	 */
985
	public function updateShares(IShareable $shareable, $add, $remove) {
986
		$addressBookId = $shareable->getResourceId();
987
		$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...
988
		$oldShares = $this->getShares($addressBookId);
989
990
		$this->sharingBackend->updateShares($shareable, $add, $remove);
991
992
		$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

992
		$this->dispatcher->dispatchTyped(new AddressBookShareUpdatedEvent($addressBookId, /** @scrutinizer ignore-type */ $addressBookData, $oldShares, $add, $remove));
Loading history...
993
	}
994
995
	/**
996
	 * Search contacts in a specific address-book
997
	 *
998
	 * @param int $addressBookId
999
	 * @param string $pattern which should match within the $searchProperties
1000
	 * @param array $searchProperties defines the properties within the query pattern should match
1001
	 * @param array $options = array() to define the search behavior
1002
	 *    - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
1003
	 *    - 'limit' - Set a numeric limit for the search results
1004
	 *    - 'offset' - Set the offset for the limited search results
1005
	 * @return array an array of contacts which are arrays of key-value-pairs
1006
	 */
1007
	public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
1008
		return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
1009
	}
1010
1011
	/**
1012
	 * Search contacts in all address-books accessible by a user
1013
	 *
1014
	 * @param string $principalUri
1015
	 * @param string $pattern
1016
	 * @param array $searchProperties
1017
	 * @param array $options
1018
	 * @return array
1019
	 */
1020
	public function searchPrincipalUri(string $principalUri,
1021
									   string $pattern,
1022
									   array $searchProperties,
1023
									   array $options = []): array {
1024
		$addressBookIds = array_map(static function ($row):int {
1025
			return (int) $row['id'];
1026
		}, $this->getAddressBooksForUser($principalUri));
1027
1028
		return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
1029
	}
1030
1031
	/**
1032
	 * @param array $addressBookIds
1033
	 * @param string $pattern
1034
	 * @param array $searchProperties
1035
	 * @param array $options
1036
	 * @return array
1037
	 */
1038
	private function searchByAddressBookIds(array $addressBookIds,
1039
											string $pattern,
1040
											array $searchProperties,
1041
											array $options = []): array {
1042
		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
1043
1044
		$query2 = $this->db->getQueryBuilder();
1045
1046
		$addressBookOr = $query2->expr()->orX();
1047
		foreach ($addressBookIds as $addressBookId) {
1048
			$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1049
		}
1050
1051
		if ($addressBookOr->count() === 0) {
1052
			return [];
1053
		}
1054
1055
		$propertyOr = $query2->expr()->orX();
1056
		foreach ($searchProperties as $property) {
1057
			if ($escapePattern) {
1058
				if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
1059
					// There can be no spaces in emails
1060
					continue;
1061
				}
1062
1063
				if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
1064
					// There can be no chars in cloud ids which are not valid for user ids plus :/
1065
					// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1066
					continue;
1067
				}
1068
			}
1069
1070
			$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1071
		}
1072
1073
		if ($propertyOr->count() === 0) {
1074
			return [];
1075
		}
1076
1077
		$query2->selectDistinct('cp.cardid')
1078
			->from($this->dbCardsPropertiesTable, 'cp')
1079
			->andWhere($addressBookOr)
1080
			->andWhere($propertyOr);
1081
1082
		// No need for like when the pattern is empty
1083
		if ('' !== $pattern) {
1084
			if (!$escapePattern) {
1085
				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter($pattern)));
1086
			} else {
1087
				$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
1088
			}
1089
		}
1090
1091
		if (isset($options['limit'])) {
1092
			$query2->setMaxResults($options['limit']);
1093
		}
1094
		if (isset($options['offset'])) {
1095
			$query2->setFirstResult($options['offset']);
1096
		}
1097
1098
		$result = $query2->execute();
1099
		$matches = $result->fetchAll();
1100
		$result->closeCursor();
1101
		$matches = array_map(function ($match) {
1102
			return (int)$match['cardid'];
1103
		}, $matches);
1104
1105
		$query = $this->db->getQueryBuilder();
1106
		$query->select('c.addressbookid', 'c.carddata', 'c.uri')
1107
			->from($this->dbCardsTable, 'c')
1108
			->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
1109
1110
		$result = $query->execute();
1111
		$cards = $result->fetchAll();
1112
1113
		$result->closeCursor();
1114
1115
		return array_map(function ($array) {
1116
			$array['addressbookid'] = (int) $array['addressbookid'];
1117
			$modified = false;
1118
			$array['carddata'] = $this->readBlob($array['carddata'], $modified);
1119
			if ($modified) {
1120
				$array['size'] = strlen($array['carddata']);
1121
			}
1122
			return $array;
1123
		}, $cards);
1124
	}
1125
1126
	/**
1127
	 * @param int $bookId
1128
	 * @param string $name
1129
	 * @return array
1130
	 */
1131
	public function collectCardProperties($bookId, $name) {
1132
		$query = $this->db->getQueryBuilder();
1133
		$result = $query->selectDistinct('value')
1134
			->from($this->dbCardsPropertiesTable)
1135
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
1136
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
1137
			->execute();
1138
1139
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
1140
		$result->closeCursor();
1141
1142
		return $all;
1143
	}
1144
1145
	/**
1146
	 * get URI from a given contact
1147
	 *
1148
	 * @param int $id
1149
	 * @return string
1150
	 */
1151
	public function getCardUri($id) {
1152
		$query = $this->db->getQueryBuilder();
1153
		$query->select('uri')->from($this->dbCardsTable)
1154
			->where($query->expr()->eq('id', $query->createParameter('id')))
1155
			->setParameter('id', $id);
1156
1157
		$result = $query->execute();
1158
		$uri = $result->fetch();
1159
		$result->closeCursor();
1160
1161
		if (!isset($uri['uri'])) {
1162
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
1163
		}
1164
1165
		return $uri['uri'];
1166
	}
1167
1168
	/**
1169
	 * return contact with the given URI
1170
	 *
1171
	 * @param int $addressBookId
1172
	 * @param string $uri
1173
	 * @returns array
1174
	 */
1175
	public function getContact($addressBookId, $uri) {
1176
		$result = [];
1177
		$query = $this->db->getQueryBuilder();
1178
		$query->select('*')->from($this->dbCardsTable)
1179
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1180
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1181
		$queryResult = $query->execute();
1182
		$contact = $queryResult->fetch();
1183
		$queryResult->closeCursor();
1184
1185
		if (is_array($contact)) {
1186
			$modified = false;
1187
			$contact['etag'] = '"' . $contact['etag'] . '"';
1188
			$contact['carddata'] = $this->readBlob($contact['carddata'], $modified);
1189
			if ($modified) {
1190
				$contact['size'] = strlen($contact['carddata']);
1191
			}
1192
1193
			$result = $contact;
1194
		}
1195
1196
		return $result;
1197
	}
1198
1199
	/**
1200
	 * Returns the list of people whom this address book is shared with.
1201
	 *
1202
	 * Every element in this array should have the following properties:
1203
	 *   * href - Often a mailto: address
1204
	 *   * commonName - Optional, for example a first + last name
1205
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
1206
	 *   * readOnly - boolean
1207
	 *   * summary - Optional, a description for the share
1208
	 *
1209
	 * @return array
1210
	 */
1211
	public function getShares($addressBookId) {
1212
		return $this->sharingBackend->getShares($addressBookId);
1213
	}
1214
1215
	/**
1216
	 * update properties table
1217
	 *
1218
	 * @param int $addressBookId
1219
	 * @param string $cardUri
1220
	 * @param string $vCardSerialized
1221
	 */
1222
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1223
		$cardId = $this->getCardId($addressBookId, $cardUri);
1224
		$vCard = $this->readCard($vCardSerialized);
1225
1226
		$this->purgeProperties($addressBookId, $cardId);
1227
1228
		$query = $this->db->getQueryBuilder();
1229
		$query->insert($this->dbCardsPropertiesTable)
1230
			->values(
1231
				[
1232
					'addressbookid' => $query->createNamedParameter($addressBookId),
1233
					'cardid' => $query->createNamedParameter($cardId),
1234
					'name' => $query->createParameter('name'),
1235
					'value' => $query->createParameter('value'),
1236
					'preferred' => $query->createParameter('preferred')
1237
				]
1238
			);
1239
1240
		foreach ($vCard->children() as $property) {
1241
			if (!in_array($property->name, self::$indexProperties)) {
1242
				continue;
1243
			}
1244
			$preferred = 0;
1245
			foreach ($property->parameters as $parameter) {
1246
				if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1247
					$preferred = 1;
1248
					break;
1249
				}
1250
			}
1251
			$query->setParameter('name', $property->name);
1252
			$query->setParameter('value', mb_substr($property->getValue(), 0, 254));
1253
			$query->setParameter('preferred', $preferred);
1254
			$query->execute();
1255
		}
1256
	}
1257
1258
	/**
1259
	 * read vCard data into a vCard object
1260
	 *
1261
	 * @param string $cardData
1262
	 * @return VCard
1263
	 */
1264
	protected function readCard($cardData) {
1265
		return Reader::read($cardData);
1266
	}
1267
1268
	/**
1269
	 * delete all properties from a given card
1270
	 *
1271
	 * @param int $addressBookId
1272
	 * @param int $cardId
1273
	 */
1274
	protected function purgeProperties($addressBookId, $cardId) {
1275
		$query = $this->db->getQueryBuilder();
1276
		$query->delete($this->dbCardsPropertiesTable)
1277
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1278
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1279
		$query->execute();
1280
	}
1281
1282
	/**
1283
	 * get ID from a given contact
1284
	 *
1285
	 * @param int $addressBookId
1286
	 * @param string $uri
1287
	 * @return int
1288
	 */
1289
	protected function getCardId($addressBookId, $uri) {
1290
		$query = $this->db->getQueryBuilder();
1291
		$query->select('id')->from($this->dbCardsTable)
1292
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1293
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1294
1295
		$result = $query->execute();
1296
		$cardIds = $result->fetch();
1297
		$result->closeCursor();
1298
1299
		if (!isset($cardIds['id'])) {
1300
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1301
		}
1302
1303
		return (int)$cardIds['id'];
1304
	}
1305
1306
	/**
1307
	 * For shared address books the sharee is set in the ACL of the address book
1308
	 *
1309
	 * @param $addressBookId
1310
	 * @param $acl
1311
	 * @return array
1312
	 */
1313
	public function applyShareAcl($addressBookId, $acl) {
1314
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1315
	}
1316
1317
	private function convertPrincipal($principalUri, $toV2) {
1318
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1319
			list(, $name) = \Sabre\Uri\split($principalUri);
1320
			if ($toV2 === true) {
1321
				return "principals/users/$name";
1322
			}
1323
			return "principals/$name";
1324
		}
1325
		return $principalUri;
1326
	}
1327
1328
	private function addOwnerPrincipal(&$addressbookInfo) {
1329
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1330
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1331
		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1332
			$uri = $addressbookInfo[$ownerPrincipalKey];
1333
		} else {
1334
			$uri = $addressbookInfo['principaluri'];
1335
		}
1336
1337
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1338
		if (isset($principalInformation['{DAV:}displayname'])) {
1339
			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1340
		}
1341
	}
1342
1343
	/**
1344
	 * Extract UID from vcard
1345
	 *
1346
	 * @param string $cardData the vcard raw data
1347
	 * @return string the uid
1348
	 * @throws BadRequest if no UID is available
1349
	 */
1350
	private function getUID($cardData) {
1351
		if ($cardData != '') {
1352
			$vCard = Reader::read($cardData);
1353
			if ($vCard->UID) {
1354
				$uid = $vCard->UID->getValue();
1355
				return $uid;
1356
			}
1357
			// should already be handled, but just in case
1358
			throw new BadRequest('vCards on CardDAV servers MUST have a UID property');
1359
		}
1360
		// should already be handled, but just in case
1361
		throw new BadRequest('vCard can not be empty');
1362
	}
1363
}
1364