Completed
Push — stable10 ( ed98fa...ea5b52 )
by Roeland
09:56
created

CardDavBackend::getMultipleCards()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 19

Duplication

Lines 5
Ratio 18.52 %

Importance

Changes 0
Metric Value
cc 4
eloc 19
nc 4
nop 2
dl 5
loc 27
rs 8.5806
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 PDO;
37
use Sabre\CardDAV\Backend\BackendInterface;
38
use Sabre\CardDAV\Backend\SyncSupport;
39
use Sabre\CardDAV\Plugin;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, OCA\DAV\CardDAV\Plugin.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

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