Completed
Push — master ( a27a8d...b12464 )
by Lukas
45:55 queued 29:49
created

CardDavBackend::getUserDisplayName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 13
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 13
loc 13
rs 9.4285
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Georg Ehrke <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Stefan Weil <[email protected]>
11
 * @author Thomas Müller <[email protected]>
12
 *
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
namespace OCA\DAV\CardDAV;
30
31
use OCA\DAV\Connector\Sabre\Principal;
32
use OCP\DB\QueryBuilder\IQueryBuilder;
33
use OCA\DAV\DAV\Sharing\Backend;
34
use OCA\DAV\DAV\Sharing\IShareable;
35
use OCP\IDBConnection;
36
use OCP\IUser;
37
use OCP\IUserManager;
38
use PDO;
39
use Sabre\CardDAV\Backend\BackendInterface;
40
use Sabre\CardDAV\Backend\SyncSupport;
41
use Sabre\CardDAV\Plugin;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, OCA\DAV\CardDAV\Plugin.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
42
use Sabre\DAV\Exception\BadRequest;
43
use Sabre\HTTP\URLUtil;
44
use Sabre\VObject\Component\VCard;
45
use Sabre\VObject\Reader;
46
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
47
use Symfony\Component\EventDispatcher\GenericEvent;
48
49
class CardDavBackend implements BackendInterface, SyncSupport {
50
51
	/** @var Principal */
52
	private $principalBackend;
53
54
	/** @var string */
55
	private $dbCardsTable = 'cards';
56
57
	/** @var string */
58
	private $dbCardsPropertiesTable = 'cards_properties';
59
60
	/** @var IDBConnection */
61
	private $db;
62
63
	/** @var Backend */
64
	private $sharingBackend;
65
66
	/** @var array properties to index */
67
	public static $indexProperties = array(
68
			'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
69
			'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD');
70
71
	/**
72
	 * @var string[] Map of uid => display name
73
	 */
74
	protected $userDisplayNames;
75
76
	/** @var IUserManager */
77
	private $userManager;
78
79
	/** @var EventDispatcherInterface */
80
	private $dispatcher;
81
82
	/**
83
	 * CardDavBackend constructor.
84
	 *
85
	 * @param IDBConnection $db
86
	 * @param Principal $principalBackend
87
	 * @param IUserManager $userManager
88
	 * @param EventDispatcherInterface $dispatcher
89
	 */
90 View Code Duplication
	public function __construct(IDBConnection $db,
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
91
								Principal $principalBackend,
92
								IUserManager $userManager,
93
								EventDispatcherInterface $dispatcher = null) {
94
		$this->db = $db;
95
		$this->principalBackend = $principalBackend;
96
		$this->userManager = $userManager;
97
		$this->dispatcher = $dispatcher;
98
		$this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook');
99
	}
100
101
	/**
102
	 * Returns the list of address books for a specific user.
103
	 *
104
	 * Every addressbook should have the following properties:
105
	 *   id - an arbitrary unique id
106
	 *   uri - the 'basename' part of the url
107
	 *   principaluri - Same as the passed parameter
108
	 *
109
	 * Any additional clark-notation property may be passed besides this. Some
110
	 * common ones are :
111
	 *   {DAV:}displayname
112
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
113
	 *   {http://calendarserver.org/ns/}getctag
114
	 *
115
	 * @param string $principalUri
116
	 * @return array
117
	 */
118
	function getAddressBooksForUser($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
119
		$principalUriOriginal = $principalUri;
120
		$principalUri = $this->convertPrincipal($principalUri, true);
121
		$query = $this->db->getQueryBuilder();
122
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
123
			->from('addressbooks')
124
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
125
126
		$addressBooks = [];
127
128
		$result = $query->execute();
129
		while($row = $result->fetch()) {
130
			$addressBooks[$row['id']] = [
131
				'id'  => $row['id'],
132
				'uri' => $row['uri'],
133
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
134
				'{DAV:}displayname' => $row['displayname'],
135
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
136
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
137
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
138
			];
139
		}
140
		$result->closeCursor();
141
142
		// query for shared calendars
143
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
144
		$principals[]= $principalUri;
145
146
		$query = $this->db->getQueryBuilder();
147
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
148
			->from('dav_shares', 's')
149
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
150
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
151
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
152
			->setParameter('type', 'addressbook')
153
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
154
			->execute();
155
156
		while($row = $result->fetch()) {
157
			list(, $name) = URLUtil::splitPath($row['principaluri']);
158
			$uri = $row['uri'] . '_shared_by_' . $name;
159
			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
160
			if (!isset($addressBooks[$row['id']])) {
161
				$addressBooks[$row['id']] = [
162
					'id'  => $row['id'],
163
					'uri' => $uri,
164
					'principaluri' => $principalUri,
165
					'{DAV:}displayname' => $displayName,
166
					'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
167
					'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
168
					'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
169
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
170
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
171
				];
172
			}
173
		}
174
		$result->closeCursor();
175
176
		return array_values($addressBooks);
177
	}
178
179 View Code Duplication
	private function getUserDisplayName($uid) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
180
		if (!isset($this->userDisplayNames[$uid])) {
181
			$user = $this->userManager->get($uid);
182
183
			if ($user instanceof IUser) {
184
				$this->userDisplayNames[$uid] = $user->getDisplayName();
185
			} else {
186
				$this->userDisplayNames[$uid] = $uid;
187
			}
188
		}
189
190
		return $this->userDisplayNames[$uid];
191
	}
192
193
	/**
194
	 * @param int $addressBookId
195
	 */
196
	public function getAddressBookById($addressBookId) {
197
		$query = $this->db->getQueryBuilder();
198
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
199
			->from('addressbooks')
200
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
201
			->execute();
202
203
		$row = $result->fetch();
204
		$result->closeCursor();
205
		if ($row === false) {
206
			return null;
207
		}
208
209
		return [
210
			'id'  => $row['id'],
211
			'uri' => $row['uri'],
212
			'principaluri' => $row['principaluri'],
213
			'{DAV:}displayname' => $row['displayname'],
214
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
215
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
216
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
217
		];
218
	}
219
220
	/**
221
	 * @param $addressBookUri
222
	 * @return array|null
223
	 */
224
	public function getAddressBooksByUri($principal, $addressBookUri) {
225
		$query = $this->db->getQueryBuilder();
226
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
227
			->from('addressbooks')
228
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
229
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
230
			->setMaxResults(1)
231
			->execute();
232
233
		$row = $result->fetch();
234
		$result->closeCursor();
235
		if ($row === false) {
236
			return null;
237
		}
238
239
		return [
240
				'id'  => $row['id'],
241
				'uri' => $row['uri'],
242
				'principaluri' => $row['principaluri'],
243
				'{DAV:}displayname' => $row['displayname'],
244
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
245
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
246
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
247
			];
248
	}
249
250
	/**
251
	 * Updates properties for an address book.
252
	 *
253
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
254
	 * To do the actual updates, you must tell this object which properties
255
	 * you're going to process with the handle() method.
256
	 *
257
	 * Calling the handle method is like telling the PropPatch object "I
258
	 * promise I can handle updating this property".
259
	 *
260
	 * Read the PropPatch documentation for more info and examples.
261
	 *
262
	 * @param string $addressBookId
263
	 * @param \Sabre\DAV\PropPatch $propPatch
264
	 * @return void
265
	 */
266
	function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
267
		$supportedProperties = [
268
			'{DAV:}displayname',
269
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
270
		];
271
272
		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
273
274
			$updates = [];
275 View Code Duplication
			foreach($mutations as $property=>$newValue) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
276
277
				switch($property) {
278
					case '{DAV:}displayname' :
279
						$updates['displayname'] = $newValue;
280
						break;
281
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
282
						$updates['description'] = $newValue;
283
						break;
284
				}
285
			}
286
			$query = $this->db->getQueryBuilder();
287
			$query->update('addressbooks');
288
289
			foreach($updates as $key=>$value) {
290
				$query->set($key, $query->createNamedParameter($value));
291
			}
292
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
293
			->execute();
294
295
			$this->addChange($addressBookId, "", 2);
296
297
			return true;
298
299
		});
300
	}
301
302
	/**
303
	 * Creates a new address book
304
	 *
305
	 * @param string $principalUri
306
	 * @param string $url Just the 'basename' of the url.
307
	 * @param array $properties
308
	 * @return int
309
	 * @throws BadRequest
310
	 */
311
	function createAddressBook($principalUri, $url, array $properties) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
312
		$values = [
313
			'displayname' => null,
314
			'description' => null,
315
			'principaluri' => $principalUri,
316
			'uri' => $url,
317
			'synctoken' => 1
318
		];
319
320 View Code Duplication
		foreach($properties as $property=>$newValue) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
321
322
			switch($property) {
323
				case '{DAV:}displayname' :
324
					$values['displayname'] = $newValue;
325
					break;
326
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
327
					$values['description'] = $newValue;
328
					break;
329
				default :
330
					throw new BadRequest('Unknown property: ' . $property);
331
			}
332
333
		}
334
335
		// Fallback to make sure the displayname is set. Some clients may refuse
336
		// to work with addressbooks not having a displayname.
337
		if(is_null($values['displayname'])) {
338
			$values['displayname'] = $url;
339
		}
340
341
		$query = $this->db->getQueryBuilder();
342
		$query->insert('addressbooks')
343
			->values([
344
				'uri' => $query->createParameter('uri'),
345
				'displayname' => $query->createParameter('displayname'),
346
				'description' => $query->createParameter('description'),
347
				'principaluri' => $query->createParameter('principaluri'),
348
				'synctoken' => $query->createParameter('synctoken'),
349
			])
350
			->setParameters($values)
351
			->execute();
352
353
		return $query->getLastInsertId();
354
	}
355
356
	/**
357
	 * Deletes an entire addressbook and all its contents
358
	 *
359
	 * @param mixed $addressBookId
360
	 * @return void
361
	 */
362
	function deleteAddressBook($addressBookId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
363
		$query = $this->db->getQueryBuilder();
364
		$query->delete('cards')
365
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
366
			->setParameter('addressbookid', $addressBookId)
367
			->execute();
368
369
		$query->delete('addressbookchanges')
370
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
371
			->setParameter('addressbookid', $addressBookId)
372
			->execute();
373
374
		$query->delete('addressbooks')
375
			->where($query->expr()->eq('id', $query->createParameter('id')))
376
			->setParameter('id', $addressBookId)
377
			->execute();
378
379
		$this->sharingBackend->deleteAllShares($addressBookId);
380
381
		$query->delete($this->dbCardsPropertiesTable)
382
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
383
			->execute();
384
385
	}
386
387
	/**
388
	 * Returns all cards for a specific addressbook id.
389
	 *
390
	 * This method should return the following properties for each card:
391
	 *   * carddata - raw vcard data
392
	 *   * uri - Some unique url
393
	 *   * lastmodified - A unix timestamp
394
	 *
395
	 * It's recommended to also return the following properties:
396
	 *   * etag - A unique etag. This must change every time the card changes.
397
	 *   * size - The size of the card in bytes.
398
	 *
399
	 * If these last two properties are provided, less time will be spent
400
	 * calculating them. If they are specified, you can also ommit carddata.
401
	 * This may speed up certain requests, especially with large cards.
402
	 *
403
	 * @param mixed $addressBookId
404
	 * @return array
405
	 */
406
	function getCards($addressBookId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
407
		$query = $this->db->getQueryBuilder();
408
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
409
			->from('cards')
410
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
411
412
		$cards = [];
413
414
		$result = $query->execute();
415 View Code Duplication
		while($row = $result->fetch()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
416
			$row['etag'] = '"' . $row['etag'] . '"';
417
			$row['carddata'] = $this->readBlob($row['carddata']);
418
			$cards[] = $row;
419
		}
420
		$result->closeCursor();
421
422
		return $cards;
423
	}
424
425
	/**
426
	 * Returns a specific card.
427
	 *
428
	 * The same set of properties must be returned as with getCards. The only
429
	 * exception is that 'carddata' is absolutely required.
430
	 *
431
	 * If the card does not exist, you must return false.
432
	 *
433
	 * @param mixed $addressBookId
434
	 * @param string $cardUri
435
	 * @return array
436
	 */
437
	function getCard($addressBookId, $cardUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
438
		$query = $this->db->getQueryBuilder();
439
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
440
			->from('cards')
441
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
442
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
443
			->setMaxResults(1);
444
445
		$result = $query->execute();
446
		$row = $result->fetch();
447
		if (!$row) {
448
			return false;
449
		}
450
		$row['etag'] = '"' . $row['etag'] . '"';
451
		$row['carddata'] = $this->readBlob($row['carddata']);
452
453
		return $row;
454
	}
455
456
	/**
457
	 * Returns a list of cards.
458
	 *
459
	 * This method should work identical to getCard, but instead return all the
460
	 * cards in the list as an array.
461
	 *
462
	 * If the backend supports this, it may allow for some speed-ups.
463
	 *
464
	 * @param mixed $addressBookId
465
	 * @param string[] $uris
466
	 * @return array
467
	 */
468
	function getMultipleCards($addressBookId, array $uris) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
469
		$query = $this->db->getQueryBuilder();
470
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
471
			->from('cards')
472
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
473
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
474
			->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
475
476
		$cards = [];
477
478
		$result = $query->execute();
479 View Code Duplication
		while($row = $result->fetch()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
480
			$row['etag'] = '"' . $row['etag'] . '"';
481
			$row['carddata'] = $this->readBlob($row['carddata']);
482
			$cards[] = $row;
483
		}
484
		$result->closeCursor();
485
486
		return $cards;
487
	}
488
489
	/**
490
	 * Creates a new card.
491
	 *
492
	 * The addressbook id will be passed as the first argument. This is the
493
	 * same id as it is returned from the getAddressBooksForUser method.
494
	 *
495
	 * The cardUri is a base uri, and doesn't include the full path. The
496
	 * cardData argument is the vcard body, and is passed as a string.
497
	 *
498
	 * It is possible to return an ETag from this method. This ETag is for the
499
	 * newly created resource, and must be enclosed with double quotes (that
500
	 * is, the string itself must contain the double quotes).
501
	 *
502
	 * You should only return the ETag if you store the carddata as-is. If a
503
	 * subsequent GET request on the same card does not have the same body,
504
	 * byte-by-byte and you did return an ETag here, clients tend to get
505
	 * confused.
506
	 *
507
	 * If you don't return an ETag, you can just return null.
508
	 *
509
	 * @param mixed $addressBookId
510
	 * @param string $cardUri
511
	 * @param string $cardData
512
	 * @return string
513
	 */
514
	function createCard($addressBookId, $cardUri, $cardData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
515
		$etag = md5($cardData);
516
517
		$query = $this->db->getQueryBuilder();
518
		$query->insert('cards')
519
			->values([
520
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
521
				'uri' => $query->createNamedParameter($cardUri),
522
				'lastmodified' => $query->createNamedParameter(time()),
523
				'addressbookid' => $query->createNamedParameter($addressBookId),
524
				'size' => $query->createNamedParameter(strlen($cardData)),
525
				'etag' => $query->createNamedParameter($etag),
526
			])
527
			->execute();
528
529
		$this->addChange($addressBookId, $cardUri, 1);
530
		$this->updateProperties($addressBookId, $cardUri, $cardData);
531
532 View Code Duplication
		if (!is_null($this->dispatcher)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
533
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
534
				new GenericEvent(null, [
535
					'addressBookId' => $addressBookId,
536
					'cardUri' => $cardUri,
537
					'cardData' => $cardData]));
538
		}
539
540
		return '"' . $etag . '"';
541
	}
542
543
	/**
544
	 * Updates a card.
545
	 *
546
	 * The addressbook id will be passed as the first argument. This is the
547
	 * same id as it is returned from the getAddressBooksForUser method.
548
	 *
549
	 * The cardUri is a base uri, and doesn't include the full path. The
550
	 * cardData argument is the vcard body, and is passed as a string.
551
	 *
552
	 * It is possible to return an ETag from this method. This ETag should
553
	 * match that of the updated resource, and must be enclosed with double
554
	 * quotes (that is: the string itself must contain the actual quotes).
555
	 *
556
	 * You should only return the ETag if you store the carddata as-is. If a
557
	 * subsequent GET request on the same card does not have the same body,
558
	 * byte-by-byte and you did return an ETag here, clients tend to get
559
	 * confused.
560
	 *
561
	 * If you don't return an ETag, you can just return null.
562
	 *
563
	 * @param mixed $addressBookId
564
	 * @param string $cardUri
565
	 * @param string $cardData
566
	 * @return string
567
	 */
568
	function updateCard($addressBookId, $cardUri, $cardData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
569
570
		$etag = md5($cardData);
571
		$query = $this->db->getQueryBuilder();
572
		$query->update('cards')
573
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
574
			->set('lastmodified', $query->createNamedParameter(time()))
575
			->set('size', $query->createNamedParameter(strlen($cardData)))
576
			->set('etag', $query->createNamedParameter($etag))
577
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
578
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
579
			->execute();
580
581
		$this->addChange($addressBookId, $cardUri, 2);
582
		$this->updateProperties($addressBookId, $cardUri, $cardData);
583
584 View Code Duplication
		if (!is_null($this->dispatcher)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
585
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
586
				new GenericEvent(null, [
587
					'addressBookId' => $addressBookId,
588
					'cardUri' => $cardUri,
589
					'cardData' => $cardData]));
590
		}
591
592
		return '"' . $etag . '"';
593
	}
594
595
	/**
596
	 * Deletes a card
597
	 *
598
	 * @param mixed $addressBookId
599
	 * @param string $cardUri
600
	 * @return bool
601
	 */
602
	function deleteCard($addressBookId, $cardUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
603
		try {
604
			$cardId = $this->getCardId($addressBookId, $cardUri);
605
		} catch (\InvalidArgumentException $e) {
606
			$cardId = null;
607
		}
608
		$query = $this->db->getQueryBuilder();
609
		$ret = $query->delete('cards')
610
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
611
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
612
			->execute();
613
614
		$this->addChange($addressBookId, $cardUri, 3);
615
616 View Code Duplication
		if (!is_null($this->dispatcher)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
617
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
618
				new GenericEvent(null, [
619
					'addressBookId' => $addressBookId,
620
					'cardUri' => $cardUri]));
621
		}
622
623
		if ($ret === 1) {
624
			if ($cardId !== null) {
625
				$this->purgeProperties($addressBookId, $cardId);
626
			}
627
			return true;
628
		}
629
630
		return false;
631
	}
632
633
	/**
634
	 * The getChanges method returns all the changes that have happened, since
635
	 * the specified syncToken in the specified address book.
636
	 *
637
	 * This function should return an array, such as the following:
638
	 *
639
	 * [
640
	 *   'syncToken' => 'The current synctoken',
641
	 *   'added'   => [
642
	 *      'new.txt',
643
	 *   ],
644
	 *   'modified'   => [
645
	 *      'modified.txt',
646
	 *   ],
647
	 *   'deleted' => [
648
	 *      'foo.php.bak',
649
	 *      'old.txt'
650
	 *   ]
651
	 * ];
652
	 *
653
	 * The returned syncToken property should reflect the *current* syncToken
654
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
655
	 * property. This is needed here too, to ensure the operation is atomic.
656
	 *
657
	 * If the $syncToken argument is specified as null, this is an initial
658
	 * sync, and all members should be reported.
659
	 *
660
	 * The modified property is an array of nodenames that have changed since
661
	 * the last token.
662
	 *
663
	 * The deleted property is an array with nodenames, that have been deleted
664
	 * from collection.
665
	 *
666
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
667
	 * 1, you only have to report changes that happened only directly in
668
	 * immediate descendants. If it's 2, it should also include changes from
669
	 * the nodes below the child collections. (grandchildren)
670
	 *
671
	 * The $limit argument allows a client to specify how many results should
672
	 * be returned at most. If the limit is not specified, it should be treated
673
	 * as infinite.
674
	 *
675
	 * If the limit (infinite or not) is higher than you're willing to return,
676
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
677
	 *
678
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
679
	 * return null.
680
	 *
681
	 * The limit is 'suggestive'. You are free to ignore it.
682
	 *
683
	 * @param string $addressBookId
684
	 * @param string $syncToken
685
	 * @param int $syncLevel
686
	 * @param int $limit
687
	 * @return array
688
	 */
689 View Code Duplication
	function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
690
		// Current synctoken
691
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
692
		$stmt->execute([ $addressBookId ]);
693
		$currentToken = $stmt->fetchColumn(0);
694
695
		if (is_null($currentToken)) return null;
696
697
		$result = [
698
			'syncToken' => $currentToken,
699
			'added'     => [],
700
			'modified'  => [],
701
			'deleted'   => [],
702
		];
703
704
		if ($syncToken) {
705
706
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
707
			if ($limit>0) {
708
				$query .= " `LIMIT` " . (int)$limit;
709
			}
710
711
			// Fetching all changes
712
			$stmt = $this->db->prepare($query);
713
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
714
715
			$changes = [];
716
717
			// This loop ensures that any duplicates are overwritten, only the
718
			// last change on a node is relevant.
719
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
720
721
				$changes[$row['uri']] = $row['operation'];
722
723
			}
724
725
			foreach($changes as $uri => $operation) {
726
727
				switch($operation) {
728
					case 1:
729
						$result['added'][] = $uri;
730
						break;
731
					case 2:
732
						$result['modified'][] = $uri;
733
						break;
734
					case 3:
735
						$result['deleted'][] = $uri;
736
						break;
737
				}
738
739
			}
740
		} else {
741
			// No synctoken supplied, this is the initial sync.
742
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
743
			$stmt = $this->db->prepare($query);
744
			$stmt->execute([$addressBookId]);
745
746
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
747
		}
748
		return $result;
749
	}
750
751
	/**
752
	 * Adds a change record to the addressbookchanges table.
753
	 *
754
	 * @param mixed $addressBookId
755
	 * @param string $objectUri
756
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
757
	 * @return void
758
	 */
759 View Code Duplication
	protected function addChange($addressBookId, $objectUri, $operation) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
760
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
761
		$stmt = $this->db->prepare($sql);
762
		$stmt->execute([
763
			$objectUri,
764
			$addressBookId,
765
			$operation,
766
			$addressBookId
767
		]);
768
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
769
		$stmt->execute([
770
			$addressBookId
771
		]);
772
	}
773
774
	private function readBlob($cardData) {
775
		if (is_resource($cardData)) {
776
			return stream_get_contents($cardData);
777
		}
778
779
		return $cardData;
780
	}
781
782
	/**
783
	 * @param IShareable $shareable
784
	 * @param string[] $add
785
	 * @param string[] $remove
786
	 */
787
	public function updateShares(IShareable $shareable, $add, $remove) {
788
		$this->sharingBackend->updateShares($shareable, $add, $remove);
789
	}
790
791
	/**
792
	 * search contact
793
	 *
794
	 * @param int $addressBookId
795
	 * @param string $pattern which should match within the $searchProperties
796
	 * @param array $searchProperties defines the properties within the query pattern should match
797
	 * @return array an array of contacts which are arrays of key-value-pairs
798
	 */
799
	public function search($addressBookId, $pattern, $searchProperties) {
800
		$query = $this->db->getQueryBuilder();
801
		$query2 = $this->db->getQueryBuilder();
802
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
803
		foreach ($searchProperties as $property) {
804
			$query2->orWhere(
805
				$query2->expr()->andX(
806
					$query2->expr()->eq('cp.name', $query->createNamedParameter($property)),
807
					$query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))
0 ignored issues
show
Unused Code introduced by
The call to IExpressionBuilder::andX() has too many arguments starting with $query2->expr()->ilike('...meter($pattern) . '%')).

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

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

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

Loading history...
808
				)
809
			);
810
		}
811
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
812
813
		$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
0 ignored issues
show
Unused Code introduced by
The call to IQueryBuilder::select() has too many arguments starting with 'c.uri'.

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

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

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

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

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

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

Loading history...
1011
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1012
			list(, $name) = URLUtil::splitPath($principalUri);
1013
			if ($toV2 === true) {
1014
				return "principals/users/$name";
1015
			}
1016
			return "principals/$name";
1017
		}
1018
		return $principalUri;
1019
	}
1020
}
1021