Completed
Push — master ( 813802...44186c )
by Blizzz
18:33 queued 09:48
created

CardDavBackend::getUsersOwnAddressBooks()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 20

Duplication

Lines 11
Ratio 42.31 %

Importance

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
42
use Sabre\DAV\Exception\BadRequest;
43
use Sabre\HTTP\URLUtil;
44
use Sabre\VObject\Component\VCard;
45
use Sabre\VObject\Reader;
46
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
47
use Symfony\Component\EventDispatcher\GenericEvent;
48
49
class CardDavBackend implements BackendInterface, SyncSupport {
50
51
	const PERSONAL_ADDRESSBOOK_URI = 'contacts';
52
	const PERSONAL_ADDRESSBOOK_NAME = 'Contacts';
53
54
	/** @var Principal */
55
	private $principalBackend;
56
57
	/** @var string */
58
	private $dbCardsTable = 'cards';
59
60
	/** @var string */
61
	private $dbCardsPropertiesTable = 'cards_properties';
62
63
	/** @var IDBConnection */
64
	private $db;
65
66
	/** @var Backend */
67
	private $sharingBackend;
68
69
	/** @var array properties to index */
70
	public static $indexProperties = array(
71
			'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
72
			'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD');
73
74
	/**
75
	 * @var string[] Map of uid => display name
76
	 */
77
	protected $userDisplayNames;
78
79
	/** @var IUserManager */
80
	private $userManager;
81
82
	/** @var EventDispatcherInterface */
83
	private $dispatcher;
84
85
	/**
86
	 * CardDavBackend constructor.
87
	 *
88
	 * @param IDBConnection $db
89
	 * @param Principal $principalBackend
90
	 * @param IUserManager $userManager
91
	 * @param EventDispatcherInterface $dispatcher
92
	 */
93
	public function __construct(IDBConnection $db,
94
								Principal $principalBackend,
95
								IUserManager $userManager,
96
								EventDispatcherInterface $dispatcher = null) {
97
		$this->db = $db;
98
		$this->principalBackend = $principalBackend;
99
		$this->userManager = $userManager;
100
		$this->dispatcher = $dispatcher;
101
		$this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook');
102
	}
103
104
	/**
105
	 * Return the number of address books for a principal
106
	 *
107
	 * @param $principalUri
108
	 * @return int
109
	 */
110
	public function getAddressBooksForUserCount($principalUri) {
111
		$principalUri = $this->convertPrincipal($principalUri, true);
112
		$query = $this->db->getQueryBuilder();
113
		$query->select($query->createFunction('COUNT(*)'))
114
			->from('addressbooks')
115
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
116
117
		return (int)$query->execute()->fetchColumn();
118
	}
119
120
	/**
121
	 * Returns the list of address books for a specific user.
122
	 *
123
	 * Every addressbook should have the following properties:
124
	 *   id - an arbitrary unique id
125
	 *   uri - the 'basename' part of the url
126
	 *   principaluri - Same as the passed parameter
127
	 *
128
	 * Any additional clark-notation property may be passed besides this. Some
129
	 * common ones are :
130
	 *   {DAV:}displayname
131
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
132
	 *   {http://calendarserver.org/ns/}getctag
133
	 *
134
	 * @param string $principalUri
135
	 * @return array
136
	 */
137
	function getAddressBooksForUser($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
138
		$principalUriOriginal = $principalUri;
139
		$principalUri = $this->convertPrincipal($principalUri, true);
140
		$query = $this->db->getQueryBuilder();
141
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
142
			->from('addressbooks')
143
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
144
145
		$addressBooks = [];
146
147
		$result = $query->execute();
148 View Code Duplication
		while($row = $result->fetch()) {
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...
149
			$addressBooks[$row['id']] = [
150
				'id'  => $row['id'],
151
				'uri' => $row['uri'],
152
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
153
				'{DAV:}displayname' => $row['displayname'],
154
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
155
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
156
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
157
			];
158
		}
159
		$result->closeCursor();
160
161
		// query for shared calendars
162
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
163
		$principals[]= $principalUri;
164
165
		$query = $this->db->getQueryBuilder();
166
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
167
			->from('dav_shares', 's')
168
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
169
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
170
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
171
			->setParameter('type', 'addressbook')
172
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
173
			->execute();
174
175
		while($row = $result->fetch()) {
176
			list(, $name) = URLUtil::splitPath($row['principaluri']);
177
			$uri = $row['uri'] . '_shared_by_' . $name;
178
			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
179
			if (!isset($addressBooks[$row['id']])) {
180
				$addressBooks[$row['id']] = [
181
					'id'  => $row['id'],
182
					'uri' => $uri,
183
					'principaluri' => $principalUri,
184
					'{DAV:}displayname' => $displayName,
185
					'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
186
					'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
187
					'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
188
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
189
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
190
				];
191
			}
192
		}
193
		$result->closeCursor();
194
195
		return array_values($addressBooks);
196
	}
197
198
	public function getUsersOwnAddressBooks($principalUri) {
199
		$principalUriOriginal = $principalUri;
0 ignored issues
show
Unused Code introduced by
$principalUriOriginal is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
200
		$principalUri = $this->convertPrincipal($principalUri, true);
201
		$query = $this->db->getQueryBuilder();
202
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
203
			  ->from('addressbooks')
204
			  ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
205
206
		$addressBooks = [];
207
208
		$result = $query->execute();
209 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...
210
			$addressBooks[$row['id']] = [
211
				'id'  => $row['id'],
212
				'uri' => $row['uri'],
213
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
214
				'{DAV:}displayname' => $row['displayname'],
215
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
216
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
217
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
218
			];
219
		}
220
		$result->closeCursor();
221
222
		return array_values($addressBooks);
223
	}
224
225 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...
226
		if (!isset($this->userDisplayNames[$uid])) {
227
			$user = $this->userManager->get($uid);
228
229
			if ($user instanceof IUser) {
230
				$this->userDisplayNames[$uid] = $user->getDisplayName();
231
			} else {
232
				$this->userDisplayNames[$uid] = $uid;
233
			}
234
		}
235
236
		return $this->userDisplayNames[$uid];
237
	}
238
239
	/**
240
	 * @param int $addressBookId
241
	 */
242
	public function getAddressBookById($addressBookId) {
243
		$query = $this->db->getQueryBuilder();
244
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
245
			->from('addressbooks')
246
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
247
			->execute();
248
249
		$row = $result->fetch();
250
		$result->closeCursor();
251
		if ($row === false) {
252
			return null;
253
		}
254
255
		return [
256
			'id'  => $row['id'],
257
			'uri' => $row['uri'],
258
			'principaluri' => $row['principaluri'],
259
			'{DAV:}displayname' => $row['displayname'],
260
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
261
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
262
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
263
		];
264
	}
265
266
	/**
267
	 * @param $addressBookUri
268
	 * @return array|null
269
	 */
270
	public function getAddressBooksByUri($principal, $addressBookUri) {
271
		$query = $this->db->getQueryBuilder();
272
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
273
			->from('addressbooks')
274
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
275
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
276
			->setMaxResults(1)
277
			->execute();
278
279
		$row = $result->fetch();
280
		$result->closeCursor();
281
		if ($row === false) {
282
			return null;
283
		}
284
285
		return [
286
				'id'  => $row['id'],
287
				'uri' => $row['uri'],
288
				'principaluri' => $row['principaluri'],
289
				'{DAV:}displayname' => $row['displayname'],
290
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
291
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
292
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
293
			];
294
	}
295
296
	/**
297
	 * Updates properties for an address book.
298
	 *
299
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
300
	 * To do the actual updates, you must tell this object which properties
301
	 * you're going to process with the handle() method.
302
	 *
303
	 * Calling the handle method is like telling the PropPatch object "I
304
	 * promise I can handle updating this property".
305
	 *
306
	 * Read the PropPatch documentation for more info and examples.
307
	 *
308
	 * @param string $addressBookId
309
	 * @param \Sabre\DAV\PropPatch $propPatch
310
	 * @return void
311
	 */
312
	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...
313
		$supportedProperties = [
314
			'{DAV:}displayname',
315
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
316
		];
317
318
		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
319
320
			$updates = [];
321 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...
322
323
				switch($property) {
324
					case '{DAV:}displayname' :
325
						$updates['displayname'] = $newValue;
326
						break;
327
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
328
						$updates['description'] = $newValue;
329
						break;
330
				}
331
			}
332
			$query = $this->db->getQueryBuilder();
333
			$query->update('addressbooks');
334
335
			foreach($updates as $key=>$value) {
336
				$query->set($key, $query->createNamedParameter($value));
337
			}
338
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
339
			->execute();
340
341
			$this->addChange($addressBookId, "", 2);
342
343
			return true;
344
345
		});
346
	}
347
348
	/**
349
	 * Creates a new address book
350
	 *
351
	 * @param string $principalUri
352
	 * @param string $url Just the 'basename' of the url.
353
	 * @param array $properties
354
	 * @return int
355
	 * @throws BadRequest
356
	 */
357
	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...
358
		$values = [
359
			'displayname' => null,
360
			'description' => null,
361
			'principaluri' => $principalUri,
362
			'uri' => $url,
363
			'synctoken' => 1
364
		];
365
366 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...
367
368
			switch($property) {
369
				case '{DAV:}displayname' :
370
					$values['displayname'] = $newValue;
371
					break;
372
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
373
					$values['description'] = $newValue;
374
					break;
375
				default :
376
					throw new BadRequest('Unknown property: ' . $property);
377
			}
378
379
		}
380
381
		// Fallback to make sure the displayname is set. Some clients may refuse
382
		// to work with addressbooks not having a displayname.
383
		if(is_null($values['displayname'])) {
384
			$values['displayname'] = $url;
385
		}
386
387
		$query = $this->db->getQueryBuilder();
388
		$query->insert('addressbooks')
389
			->values([
390
				'uri' => $query->createParameter('uri'),
391
				'displayname' => $query->createParameter('displayname'),
392
				'description' => $query->createParameter('description'),
393
				'principaluri' => $query->createParameter('principaluri'),
394
				'synctoken' => $query->createParameter('synctoken'),
395
			])
396
			->setParameters($values)
397
			->execute();
398
399
		return $query->getLastInsertId();
400
	}
401
402
	/**
403
	 * Deletes an entire addressbook and all its contents
404
	 *
405
	 * @param mixed $addressBookId
406
	 * @return void
407
	 */
408
	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...
409
		$query = $this->db->getQueryBuilder();
410
		$query->delete('cards')
411
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
412
			->setParameter('addressbookid', $addressBookId)
413
			->execute();
414
415
		$query->delete('addressbookchanges')
416
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
417
			->setParameter('addressbookid', $addressBookId)
418
			->execute();
419
420
		$query->delete('addressbooks')
421
			->where($query->expr()->eq('id', $query->createParameter('id')))
422
			->setParameter('id', $addressBookId)
423
			->execute();
424
425
		$this->sharingBackend->deleteAllShares($addressBookId);
426
427
		$query->delete($this->dbCardsPropertiesTable)
428
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
429
			->execute();
430
431
	}
432
433
	/**
434
	 * Returns all cards for a specific addressbook id.
435
	 *
436
	 * This method should return the following properties for each card:
437
	 *   * carddata - raw vcard data
438
	 *   * uri - Some unique url
439
	 *   * lastmodified - A unix timestamp
440
	 *
441
	 * It's recommended to also return the following properties:
442
	 *   * etag - A unique etag. This must change every time the card changes.
443
	 *   * size - The size of the card in bytes.
444
	 *
445
	 * If these last two properties are provided, less time will be spent
446
	 * calculating them. If they are specified, you can also ommit carddata.
447
	 * This may speed up certain requests, especially with large cards.
448
	 *
449
	 * @param mixed $addressBookId
450
	 * @return array
451
	 */
452
	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...
453
		$query = $this->db->getQueryBuilder();
454
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
455
			->from('cards')
456
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
457
458
		$cards = [];
459
460
		$result = $query->execute();
461 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...
462
			$row['etag'] = '"' . $row['etag'] . '"';
463
			$row['carddata'] = $this->readBlob($row['carddata']);
464
			$cards[] = $row;
465
		}
466
		$result->closeCursor();
467
468
		return $cards;
469
	}
470
471
	/**
472
	 * Returns a specific card.
473
	 *
474
	 * The same set of properties must be returned as with getCards. The only
475
	 * exception is that 'carddata' is absolutely required.
476
	 *
477
	 * If the card does not exist, you must return false.
478
	 *
479
	 * @param mixed $addressBookId
480
	 * @param string $cardUri
481
	 * @return array
482
	 */
483
	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...
484
		$query = $this->db->getQueryBuilder();
485
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
486
			->from('cards')
487
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
488
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
489
			->setMaxResults(1);
490
491
		$result = $query->execute();
492
		$row = $result->fetch();
493
		if (!$row) {
494
			return false;
495
		}
496
		$row['etag'] = '"' . $row['etag'] . '"';
497
		$row['carddata'] = $this->readBlob($row['carddata']);
498
499
		return $row;
500
	}
501
502
	/**
503
	 * Returns a list of cards.
504
	 *
505
	 * This method should work identical to getCard, but instead return all the
506
	 * cards in the list as an array.
507
	 *
508
	 * If the backend supports this, it may allow for some speed-ups.
509
	 *
510
	 * @param mixed $addressBookId
511
	 * @param string[] $uris
512
	 * @return array
513
	 */
514
	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...
515
		if (empty($uris)) {
516
			return [];
517
		}
518
519
		$chunks = array_chunk($uris, 100);
520
		$cards = [];
521
522
		$query = $this->db->getQueryBuilder();
523
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
524
			->from('cards')
525
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
526
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
527
528
		foreach ($chunks as $uris) {
529
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
530
			$result = $query->execute();
531
532 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...
533
				$row['etag'] = '"' . $row['etag'] . '"';
534
				$row['carddata'] = $this->readBlob($row['carddata']);
535
				$cards[] = $row;
536
			}
537
			$result->closeCursor();
538
		}
539
		return $cards;
540
	}
541
542
	/**
543
	 * Creates a new card.
544
	 *
545
	 * The addressbook id will be passed as the first argument. This is the
546
	 * same id as it is returned from the getAddressBooksForUser method.
547
	 *
548
	 * The cardUri is a base uri, and doesn't include the full path. The
549
	 * cardData argument is the vcard body, and is passed as a string.
550
	 *
551
	 * It is possible to return an ETag from this method. This ETag is for the
552
	 * newly created resource, and must be enclosed with double quotes (that
553
	 * is, the string itself must contain the double quotes).
554
	 *
555
	 * You should only return the ETag if you store the carddata as-is. If a
556
	 * subsequent GET request on the same card does not have the same body,
557
	 * byte-by-byte and you did return an ETag here, clients tend to get
558
	 * confused.
559
	 *
560
	 * If you don't return an ETag, you can just return null.
561
	 *
562
	 * @param mixed $addressBookId
563
	 * @param string $cardUri
564
	 * @param string $cardData
565
	 * @return string
566
	 */
567
	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...
568
		$etag = md5($cardData);
569
570
		$query = $this->db->getQueryBuilder();
571
		$query->insert('cards')
572
			->values([
573
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
574
				'uri' => $query->createNamedParameter($cardUri),
575
				'lastmodified' => $query->createNamedParameter(time()),
576
				'addressbookid' => $query->createNamedParameter($addressBookId),
577
				'size' => $query->createNamedParameter(strlen($cardData)),
578
				'etag' => $query->createNamedParameter($etag),
579
			])
580
			->execute();
581
582
		$this->addChange($addressBookId, $cardUri, 1);
583
		$this->updateProperties($addressBookId, $cardUri, $cardData);
584
585 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...
586
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
587
				new GenericEvent(null, [
588
					'addressBookId' => $addressBookId,
589
					'cardUri' => $cardUri,
590
					'cardData' => $cardData]));
591
		}
592
593
		return '"' . $etag . '"';
594
	}
595
596
	/**
597
	 * Updates a card.
598
	 *
599
	 * The addressbook id will be passed as the first argument. This is the
600
	 * same id as it is returned from the getAddressBooksForUser method.
601
	 *
602
	 * The cardUri is a base uri, and doesn't include the full path. The
603
	 * cardData argument is the vcard body, and is passed as a string.
604
	 *
605
	 * It is possible to return an ETag from this method. This ETag should
606
	 * match that of the updated resource, and must be enclosed with double
607
	 * quotes (that is: the string itself must contain the actual quotes).
608
	 *
609
	 * You should only return the ETag if you store the carddata as-is. If a
610
	 * subsequent GET request on the same card does not have the same body,
611
	 * byte-by-byte and you did return an ETag here, clients tend to get
612
	 * confused.
613
	 *
614
	 * If you don't return an ETag, you can just return null.
615
	 *
616
	 * @param mixed $addressBookId
617
	 * @param string $cardUri
618
	 * @param string $cardData
619
	 * @return string
620
	 */
621
	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...
622
623
		$etag = md5($cardData);
624
		$query = $this->db->getQueryBuilder();
625
		$query->update('cards')
626
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
627
			->set('lastmodified', $query->createNamedParameter(time()))
628
			->set('size', $query->createNamedParameter(strlen($cardData)))
629
			->set('etag', $query->createNamedParameter($etag))
630
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
631
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
632
			->execute();
633
634
		$this->addChange($addressBookId, $cardUri, 2);
635
		$this->updateProperties($addressBookId, $cardUri, $cardData);
636
637 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...
638
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
639
				new GenericEvent(null, [
640
					'addressBookId' => $addressBookId,
641
					'cardUri' => $cardUri,
642
					'cardData' => $cardData]));
643
		}
644
645
		return '"' . $etag . '"';
646
	}
647
648
	/**
649
	 * Deletes a card
650
	 *
651
	 * @param mixed $addressBookId
652
	 * @param string $cardUri
653
	 * @return bool
654
	 */
655
	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...
656
		try {
657
			$cardId = $this->getCardId($addressBookId, $cardUri);
658
		} catch (\InvalidArgumentException $e) {
659
			$cardId = null;
660
		}
661
		$query = $this->db->getQueryBuilder();
662
		$ret = $query->delete('cards')
663
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
664
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
665
			->execute();
666
667
		$this->addChange($addressBookId, $cardUri, 3);
668
669 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...
670
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
671
				new GenericEvent(null, [
672
					'addressBookId' => $addressBookId,
673
					'cardUri' => $cardUri]));
674
		}
675
676
		if ($ret === 1) {
677
			if ($cardId !== null) {
678
				$this->purgeProperties($addressBookId, $cardId);
679
			}
680
			return true;
681
		}
682
683
		return false;
684
	}
685
686
	/**
687
	 * The getChanges method returns all the changes that have happened, since
688
	 * the specified syncToken in the specified address book.
689
	 *
690
	 * This function should return an array, such as the following:
691
	 *
692
	 * [
693
	 *   'syncToken' => 'The current synctoken',
694
	 *   'added'   => [
695
	 *      'new.txt',
696
	 *   ],
697
	 *   'modified'   => [
698
	 *      'modified.txt',
699
	 *   ],
700
	 *   'deleted' => [
701
	 *      'foo.php.bak',
702
	 *      'old.txt'
703
	 *   ]
704
	 * ];
705
	 *
706
	 * The returned syncToken property should reflect the *current* syncToken
707
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
708
	 * property. This is needed here too, to ensure the operation is atomic.
709
	 *
710
	 * If the $syncToken argument is specified as null, this is an initial
711
	 * sync, and all members should be reported.
712
	 *
713
	 * The modified property is an array of nodenames that have changed since
714
	 * the last token.
715
	 *
716
	 * The deleted property is an array with nodenames, that have been deleted
717
	 * from collection.
718
	 *
719
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
720
	 * 1, you only have to report changes that happened only directly in
721
	 * immediate descendants. If it's 2, it should also include changes from
722
	 * the nodes below the child collections. (grandchildren)
723
	 *
724
	 * The $limit argument allows a client to specify how many results should
725
	 * be returned at most. If the limit is not specified, it should be treated
726
	 * as infinite.
727
	 *
728
	 * If the limit (infinite or not) is higher than you're willing to return,
729
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
730
	 *
731
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
732
	 * return null.
733
	 *
734
	 * The limit is 'suggestive'. You are free to ignore it.
735
	 *
736
	 * @param string $addressBookId
737
	 * @param string $syncToken
738
	 * @param int $syncLevel
739
	 * @param int $limit
740
	 * @return array
741
	 */
742 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...
743
		// Current synctoken
744
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
745
		$stmt->execute([ $addressBookId ]);
746
		$currentToken = $stmt->fetchColumn(0);
747
748
		if (is_null($currentToken)) return null;
749
750
		$result = [
751
			'syncToken' => $currentToken,
752
			'added'     => [],
753
			'modified'  => [],
754
			'deleted'   => [],
755
		];
756
757
		if ($syncToken) {
758
759
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
760
			if ($limit>0) {
761
				$query .= " `LIMIT` " . (int)$limit;
762
			}
763
764
			// Fetching all changes
765
			$stmt = $this->db->prepare($query);
766
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
767
768
			$changes = [];
769
770
			// This loop ensures that any duplicates are overwritten, only the
771
			// last change on a node is relevant.
772
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
773
774
				$changes[$row['uri']] = $row['operation'];
775
776
			}
777
778
			foreach($changes as $uri => $operation) {
779
780
				switch($operation) {
781
					case 1:
782
						$result['added'][] = $uri;
783
						break;
784
					case 2:
785
						$result['modified'][] = $uri;
786
						break;
787
					case 3:
788
						$result['deleted'][] = $uri;
789
						break;
790
				}
791
792
			}
793
		} else {
794
			// No synctoken supplied, this is the initial sync.
795
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
796
			$stmt = $this->db->prepare($query);
797
			$stmt->execute([$addressBookId]);
798
799
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
800
		}
801
		return $result;
802
	}
803
804
	/**
805
	 * Adds a change record to the addressbookchanges table.
806
	 *
807
	 * @param mixed $addressBookId
808
	 * @param string $objectUri
809
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
810
	 * @return void
811
	 */
812 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...
813
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
814
		$stmt = $this->db->prepare($sql);
815
		$stmt->execute([
816
			$objectUri,
817
			$addressBookId,
818
			$operation,
819
			$addressBookId
820
		]);
821
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
822
		$stmt->execute([
823
			$addressBookId
824
		]);
825
	}
826
827
	private function readBlob($cardData) {
828
		if (is_resource($cardData)) {
829
			return stream_get_contents($cardData);
830
		}
831
832
		return $cardData;
833
	}
834
835
	/**
836
	 * @param IShareable $shareable
837
	 * @param string[] $add
838
	 * @param string[] $remove
839
	 */
840
	public function updateShares(IShareable $shareable, $add, $remove) {
841
		$this->sharingBackend->updateShares($shareable, $add, $remove);
842
	}
843
844
	/**
845
	 * search contact
846
	 *
847
	 * @param int $addressBookId
848
	 * @param string $pattern which should match within the $searchProperties
849
	 * @param array $searchProperties defines the properties within the query pattern should match
850
	 * @return array an array of contacts which are arrays of key-value-pairs
851
	 */
852
	public function search($addressBookId, $pattern, $searchProperties) {
853
		$query = $this->db->getQueryBuilder();
854
		$query2 = $this->db->getQueryBuilder();
855
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
856
		foreach ($searchProperties as $property) {
857
			$query2->orWhere(
858
				$query2->expr()->andX(
859
					$query2->expr()->eq('cp.name', $query->createNamedParameter($property)),
860
					$query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))
861
				)
862
			);
863
		}
864
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
865
866
		$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
867
			->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
868
869
		$result = $query->execute();
870
		$cards = $result->fetchAll();
871
872
		$result->closeCursor();
873
874
		return array_map(function($array) {
875
			$array['carddata'] = $this->readBlob($array['carddata']);
876
			return $array;
877
		}, $cards);
878
	}
879
880
	/**
881
	 * @param int $bookId
882
	 * @param string $name
883
	 * @return array
884
	 */
885
	public function collectCardProperties($bookId, $name) {
886
		$query = $this->db->getQueryBuilder();
887
		$result = $query->selectDistinct('value')
888
			->from($this->dbCardsPropertiesTable)
889
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
890
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
891
			->execute();
892
893
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
894
		$result->closeCursor();
895
896
		return $all;
897
	}
898
899
	/**
900
	 * get URI from a given contact
901
	 *
902
	 * @param int $id
903
	 * @return string
904
	 */
905
	public function getCardUri($id) {
906
		$query = $this->db->getQueryBuilder();
907
		$query->select('uri')->from($this->dbCardsTable)
908
				->where($query->expr()->eq('id', $query->createParameter('id')))
909
				->setParameter('id', $id);
910
911
		$result = $query->execute();
912
		$uri = $result->fetch();
913
		$result->closeCursor();
914
915
		if (!isset($uri['uri'])) {
916
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
917
		}
918
919
		return $uri['uri'];
920
	}
921
922
	/**
923
	 * return contact with the given URI
924
	 *
925
	 * @param int $addressBookId
926
	 * @param string $uri
927
	 * @returns array
928
	 */
929
	public function getContact($addressBookId, $uri) {
930
		$result = [];
931
		$query = $this->db->getQueryBuilder();
932
		$query->select('*')->from($this->dbCardsTable)
933
				->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
934
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
935
		$queryResult = $query->execute();
936
		$contact = $queryResult->fetch();
937
		$queryResult->closeCursor();
938
939
		if (is_array($contact)) {
940
			$result = $contact;
941
		}
942
943
		return $result;
944
	}
945
946
	/**
947
	 * Returns the list of people whom this address book is shared with.
948
	 *
949
	 * Every element in this array should have the following properties:
950
	 *   * href - Often a mailto: address
951
	 *   * commonName - Optional, for example a first + last name
952
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
953
	 *   * readOnly - boolean
954
	 *   * summary - Optional, a description for the share
955
	 *
956
	 * @return array
957
	 */
958
	public function getShares($addressBookId) {
959
		return $this->sharingBackend->getShares($addressBookId);
960
	}
961
962
	/**
963
	 * update properties table
964
	 *
965
	 * @param int $addressBookId
966
	 * @param string $cardUri
967
	 * @param string $vCardSerialized
968
	 */
969
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
970
		$cardId = $this->getCardId($addressBookId, $cardUri);
971
		$vCard = $this->readCard($vCardSerialized);
972
973
		$this->purgeProperties($addressBookId, $cardId);
974
975
		$query = $this->db->getQueryBuilder();
976
		$query->insert($this->dbCardsPropertiesTable)
977
			->values(
978
				[
979
					'addressbookid' => $query->createNamedParameter($addressBookId),
980
					'cardid' => $query->createNamedParameter($cardId),
981
					'name' => $query->createParameter('name'),
982
					'value' => $query->createParameter('value'),
983
					'preferred' => $query->createParameter('preferred')
984
				]
985
			);
986
987
		foreach ($vCard->children() as $property) {
988
			if(!in_array($property->name, self::$indexProperties)) {
989
				continue;
990
			}
991
			$preferred = 0;
992
			foreach($property->parameters as $parameter) {
993
				if ($parameter->name == 'TYPE' && strtoupper($parameter->getValue()) == 'PREF') {
994
					$preferred = 1;
995
					break;
996
				}
997
			}
998
			$query->setParameter('name', $property->name);
999
			$query->setParameter('value', substr($property->getValue(), 0, 254));
1000
			$query->setParameter('preferred', $preferred);
1001
			$query->execute();
1002
		}
1003
	}
1004
1005
	/**
1006
	 * read vCard data into a vCard object
1007
	 *
1008
	 * @param string $cardData
1009
	 * @return VCard
1010
	 */
1011
	protected function readCard($cardData) {
1012
		return  Reader::read($cardData);
1013
	}
1014
1015
	/**
1016
	 * delete all properties from a given card
1017
	 *
1018
	 * @param int $addressBookId
1019
	 * @param int $cardId
1020
	 */
1021
	protected function purgeProperties($addressBookId, $cardId) {
1022
		$query = $this->db->getQueryBuilder();
1023
		$query->delete($this->dbCardsPropertiesTable)
1024
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1025
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1026
		$query->execute();
1027
	}
1028
1029
	/**
1030
	 * get ID from a given contact
1031
	 *
1032
	 * @param int $addressBookId
1033
	 * @param string $uri
1034
	 * @return int
1035
	 */
1036
	protected function getCardId($addressBookId, $uri) {
1037
		$query = $this->db->getQueryBuilder();
1038
		$query->select('id')->from($this->dbCardsTable)
1039
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1040
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1041
1042
		$result = $query->execute();
1043
		$cardIds = $result->fetch();
1044
		$result->closeCursor();
1045
1046
		if (!isset($cardIds['id'])) {
1047
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1048
		}
1049
1050
		return (int)$cardIds['id'];
1051
	}
1052
1053
	/**
1054
	 * For shared address books the sharee is set in the ACL of the address book
1055
	 * @param $addressBookId
1056
	 * @param $acl
1057
	 * @return array
1058
	 */
1059
	public function applyShareAcl($addressBookId, $acl) {
1060
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1061
	}
1062
1063 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...
1064
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1065
			list(, $name) = URLUtil::splitPath($principalUri);
1066
			if ($toV2 === true) {
1067
				return "principals/users/$name";
1068
			}
1069
			return "principals/$name";
1070
		}
1071
		return $principalUri;
1072
	}
1073
}
1074