Completed
Push — master ( ebbd08...bf818e )
by Thomas
47:29 queued 30:20
created

CardDavBackend::getMultipleCards()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 21

Duplication

Lines 5
Ratio 17.86 %

Importance

Changes 0
Metric Value
cc 3
eloc 21
nc 3
nop 2
dl 5
loc 28
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Björn Schießle <[email protected]>
5
 * @author Georg Ehrke <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Stefan Weil <[email protected]>
8
 * @author Thomas Müller <[email protected]>
9
 *
10
 * @copyright Copyright (c) 2018, ownCloud GmbH
11
 * @license AGPL-3.0
12
 *
13
 * This code is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License, version 3,
15
 * as published by the Free Software Foundation.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License, version 3,
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
24
 *
25
 */
26
27
namespace OCA\DAV\CardDAV;
28
29
use OC\Cache\CappedMemoryCache;
30
use OCA\DAV\Connector\Sabre\Principal;
31
use OCA\DAV\DAV\Sharing\Backend;
32
use OCA\DAV\DAV\Sharing\IShareable;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
use OCP\IDBConnection;
35
use PDO;
36
use Sabre\CardDAV\Backend\BackendInterface;
37
use Sabre\CardDAV\Backend\SyncSupport;
38
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...
39
use Sabre\DAV\Exception\BadRequest;
40
use Sabre\HTTP\URLUtil;
41
use Sabre\VObject\Component\VCard;
42
use Sabre\VObject\Reader;
43
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
44
use Symfony\Component\EventDispatcher\GenericEvent;
45
46
class CardDavBackend implements BackendInterface, SyncSupport {
47
48
	/** @var Principal */
49
	private $principalBackend;
50
51
	/** @var string */
52
	private $dbCardsTable = 'cards';
53
54
	/** @var string */
55
	private $dbCardsPropertiesTable = 'cards_properties';
56
57
	/** @var IDBConnection */
58
	private $db;
59
60
	/** @var Backend */
61
	private $sharingBackend;
62
63
	/** @var CappedMemoryCache Cache of card URI to db row ids */
64
	private $idCache;
65
66
	/** @var array properties to index */
67
	public static $indexProperties = [
68
			'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME',
69
			'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'];
70
71
	/** @var EventDispatcherInterface */
72
	private $dispatcher;
73
	/** @var bool */
74
	private $legacyMode;
75
76
	/**
77
	 * CardDavBackend constructor.
78
	 *
79
	 * @param IDBConnection $db
80
	 * @param Principal $principalBackend
81
	 * @param EventDispatcherInterface $dispatcher
82
	 */
83
	public function __construct(IDBConnection $db,
84
								Principal $principalBackend,
85
								EventDispatcherInterface $dispatcher = null,
86
								$legacyMode = false) {
87
		$this->db = $db;
88
		$this->principalBackend = $principalBackend;
89
		$this->dispatcher = $dispatcher;
90
		$this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook');
91
		$this->legacyMode = $legacyMode;
92
		$this->idCache = new CappedMemoryCache();
93
	}
94
95
	/**
96
	 * Returns the list of address books for a specific user.
97
	 *
98
	 * Every addressbook should have the following properties:
99
	 *   id - an arbitrary unique id
100
	 *   uri - the 'basename' part of the url
101
	 *   principaluri - Same as the passed parameter
102
	 *
103
	 * Any additional clark-notation property may be passed besides this. Some
104
	 * common ones are :
105
	 *   {DAV:}displayname
106
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
107
	 *   {http://calendarserver.org/ns/}getctag
108
	 *
109
	 * @param string $principalUri
110
	 * @return array
111
	 */
112
	function getUsersOwnAddressBooks($principalUri) {
113
		$principalUri = $this->convertPrincipal($principalUri, true);
114
		$query = $this->db->getQueryBuilder();
115
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
116
			->from('addressbooks')
117
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
118
119
		$addressBooks = [];
120
121
		$result = $query->execute();
122
		while ($row = $result->fetch()) {
123
			$addressBooks[$row['id']] = [
124
				'id' => $row['id'],
125
				'uri' => $row['uri'],
126
				'principaluri' => $this->convertPrincipal($row['principaluri']),
127
				'{DAV:}displayname' => $row['displayname'],
128
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
129
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
130
				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
131
			];
132
		}
133
		$result->closeCursor();
134
		return array_values($addressBooks);
135
	}
136
137
	/**
138
	 * Returns the list of address books for a specific user, including shared by other users.
139
	 *
140
	 * Every addressbook should have the following properties:
141
	 *   id - an arbitrary unique id
142
	 *   uri - the 'basename' part of the url
143
	 *   principaluri - Same as the passed parameter
144
	 *
145
	 * Any additional clark-notation property may be passed besides this. Some
146
	 * common ones are :
147
	 *   {DAV:}displayname
148
	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
149
	 *   {http://calendarserver.org/ns/}getctag
150
	 *
151
	 * @param string $principalUri
152
	 * @return array
153
	 */
154
	function getAddressBooksForUser($principalUri) {
155
		$addressBooks = $this->getUsersOwnAddressBooks($principalUri);
156
157
		// query for shared calendars
158
		$principals = $this->principalBackend->getGroupMembership($principalUri, true);
159
		$principals[]= $principalUri;
160
161
		$query = $this->db->getQueryBuilder();
162
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
163
			->from('dav_shares', 's')
164
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
165
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
166
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
167
			->setParameter('type', 'addressbook')
168
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
169
			->execute();
170
171
		while($row = $result->fetch()) {
172
			list(, $name) = URLUtil::splitPath($row['principaluri']);
173
			$uri = $row['uri'] . '_shared_by_' . $name;
174
			$displayName = $row['displayname'] . "($name)";
175
			if (!isset($addressBooks[$row['id']])) {
176
				$addressBooks[$row['id']] = [
177
					'id'  => $row['id'],
178
					'uri' => $uri,
179
					'principaluri' => $principalUri,
180
					'{DAV:}displayname' => $displayName,
181
					'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
182
					'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
183
					'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
184
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
185
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
186
				];
187
			}
188
		}
189
		$result->closeCursor();
190
191
		return array_values($addressBooks);
192
	}
193
194
	/**
195
	 * @param int $addressBookId
196
	 */
197
	public function getAddressBookById($addressBookId) {
198
		$query = $this->db->getQueryBuilder();
199
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
200
			->from('addressbooks')
201
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
202
			->execute();
203
204
		$row = $result->fetch();
205
		$result->closeCursor();
206
		if ($row === false) {
207
			return null;
208
		}
209
210
		return [
211
			'id'  => $row['id'],
212
			'uri' => $row['uri'],
213
			'principaluri' => $row['principaluri'],
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
221
	/**
222
	 * @param $addressBookUri
223
	 * @return array|null
224
	 */
225
	public function getAddressBooksByUri($principal, $addressBookUri) {
226
		$query = $this->db->getQueryBuilder();
227
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
228
			->from('addressbooks')
229
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
230
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
231
			->setMaxResults(1)
232
			->execute();
233
234
		$row = $result->fetch();
235
		$result->closeCursor();
236
		if ($row === false) {
237
			return null;
238
		}
239
240
		return [
241
				'id'  => $row['id'],
242
				'uri' => $row['uri'],
243
				'principaluri' => $row['principaluri'],
244
				'{DAV:}displayname' => $row['displayname'],
245
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
246
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
247
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
248
			];
249
	}
250
251
	/**
252
	 * Updates properties for an address book.
253
	 *
254
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
255
	 * To do the actual updates, you must tell this object which properties
256
	 * you're going to process with the handle() method.
257
	 *
258
	 * Calling the handle method is like telling the PropPatch object "I
259
	 * promise I can handle updating this property".
260
	 *
261
	 * Read the PropPatch documentation for more info and examples.
262
	 *
263
	 * @param string $addressBookId
264
	 * @param \Sabre\DAV\PropPatch $propPatch
265
	 * @return void
266
	 */
267
	function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
268
		$supportedProperties = [
269
			'{DAV:}displayname',
270
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
271
		];
272
273
		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
274
275
			$updates = [];
276 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...
277
278
				switch($property) {
279
					case '{DAV:}displayname' :
280
						$updates['displayname'] = $newValue;
281
						break;
282
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
283
						$updates['description'] = $newValue;
284
						break;
285
				}
286
			}
287
			$query = $this->db->getQueryBuilder();
288
			$query->update('addressbooks');
289
290
			foreach($updates as $key=>$value) {
291
				$query->set($key, $query->createNamedParameter($value));
292
			}
293
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
294
			->execute();
295
296
			$this->addChange($addressBookId, "", 2);
297
298
			return true;
299
300
		});
301
	}
302
303
	/**
304
	 * Creates a new address book
305
	 *
306
	 * @param string $principalUri
307
	 * @param string $url Just the 'basename' of the url.
308
	 * @param array $properties
309
	 * @return int
310
	 * @throws BadRequest
311
	 */
312
	function createAddressBook($principalUri, $url, array $properties) {
313
		$values = [
314
			'displayname' => null,
315
			'description' => null,
316
			'principaluri' => $principalUri,
317
			'uri' => $url,
318
			'synctoken' => 1
319
		];
320
321 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...
322
323
			switch($property) {
324
				case '{DAV:}displayname' :
325
					$values['displayname'] = $newValue;
326
					break;
327
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
328
					$values['description'] = $newValue;
329
					break;
330
				default :
331
					throw new BadRequest('Unknown property: ' . $property);
332
			}
333
334
		}
335
336
		// Fallback to make sure the displayname is set. Some clients may refuse
337
		// to work with addressbooks not having a displayname.
338
		if(is_null($values['displayname'])) {
339
			$values['displayname'] = $url;
340
		}
341
342
		$query = $this->db->getQueryBuilder();
343
		$query->insert('addressbooks')
344
			->values([
345
				'uri' => $query->createParameter('uri'),
346
				'displayname' => $query->createParameter('displayname'),
347
				'description' => $query->createParameter('description'),
348
				'principaluri' => $query->createParameter('principaluri'),
349
				'synctoken' => $query->createParameter('synctoken'),
350
			])
351
			->setParameters($values)
352
			->execute();
353
354
		return $query->getLastInsertId();
355
	}
356
357
	/**
358
	 * Deletes an entire addressbook and all its contents
359
	 *
360
	 * @param mixed $addressBookId
361
	 * @return void
362
	 */
363
	function deleteAddressBook($addressBookId) {
364
		$query = $this->db->getQueryBuilder();
365
		$query->delete('cards')
366
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
367
			->setParameter('addressbookid', $addressBookId)
368
			->execute();
369
370
		$query->delete('addressbookchanges')
371
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
372
			->setParameter('addressbookid', $addressBookId)
373
			->execute();
374
375
		$query->delete('addressbooks')
376
			->where($query->expr()->eq('id', $query->createParameter('id')))
377
			->setParameter('id', $addressBookId)
378
			->execute();
379
380
		$this->sharingBackend->deleteAllShares($addressBookId);
381
382
		$query->delete($this->dbCardsPropertiesTable)
383
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
384
			->execute();
385
386
	}
387
388
	/**
389
	 * Returns all cards for a specific addressbook id.
390
	 *
391
	 * This method should return the following properties for each card:
392
	 *   * carddata - raw vcard data
393
	 *   * uri - Some unique url
394
	 *   * lastmodified - A unix timestamp
395
	 *
396
	 * It's recommended to also return the following properties:
397
	 *   * etag - A unique etag. This must change every time the card changes.
398
	 *   * size - The size of the card in bytes.
399
	 *
400
	 * If these last two properties are provided, less time will be spent
401
	 * calculating them. If they are specified, you can also ommit carddata.
402
	 * This may speed up certain requests, especially with large cards.
403
	 *
404
	 * @param mixed $addressBookId
405
	 * @return array
406
	 */
407
	function getCards($addressBookId) {
408
		$query = $this->db->getQueryBuilder();
409
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
410
			->from('cards')
411
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
412
413
		$cards = [];
414
415
		$result = $query->execute();
416 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...
417
			$row['etag'] = '"' . $row['etag'] . '"';
418
			$row['carddata'] = $this->readBlob($row['carddata']);
419
			$cards[] = $row;
420
		}
421
		$result->closeCursor();
422
423
		return $cards;
424
	}
425
426
	/**
427
	 * Returns a specific card.
428
	 *
429
	 * The same set of properties must be returned as with getCards. The only
430
	 * exception is that 'carddata' is absolutely required.
431
	 *
432
	 * If the card does not exist, you must return false.
433
	 *
434
	 * @param mixed $addressBookId
435
	 * @param string $cardUri
436
	 * @return array|false
437
	 */
438
	function getCard($addressBookId, $cardUri) {
439
		$query = $this->db->getQueryBuilder();
440
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
441
			->from('cards')
442
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
443
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
444
			->setMaxResults(1);
445
446
		$result = $query->execute();
447
		$row = $result->fetch();
448
		if (!$row) {
449
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Sabre\CardDAV\Backend\BackendInterface::getCard of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
450
		}
451
		$row['etag'] = '"' . $row['etag'] . '"';
452
		$row['carddata'] = $this->readBlob($row['carddata']);
453
454
		return $row;
455
	}
456
457
	/**
458
	 * Returns a list of cards.
459
	 *
460
	 * This method should work identical to getCard, but instead return all the
461
	 * cards in the list as an array.
462
	 *
463
	 * If the backend supports this, it may allow for some speed-ups.
464
	 *
465
	 * @param mixed $addressBookId
466
	 * @param string[] $uris
467
	 * @return array
468
	 */
469
	function getMultipleCards($addressBookId, array $uris) {
470
		if (count($uris) <= 999) {
471
			$query = $this->db->getQueryBuilder();
472
			$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
473
				->from('cards')
474
				->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
475
				->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
476
				->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
477
478
			$cards = [];
479
480
			$result = $query->execute();
481 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...
482
				$row['etag'] = '"' . $row['etag'] . '"';
483
				$row['carddata'] = $this->readBlob($row['carddata']);
484
				$cards[] = $row;
485
			}
486
			$result->closeCursor();
487
488
			return $cards;
489
		}
490
		$chunks = array_chunk($uris, 999);
491
		$results = array_map(function ($chunk) use ($addressBookId) {
492
			return $this->getMultipleCards($addressBookId, $chunk);
493
		}, $chunks);
494
495
		return call_user_func_array('array_merge', $results);
496
	}
497
498
	/**
499
	 * Creates a new card.
500
	 *
501
	 * The addressbook id will be passed as the first argument. This is the
502
	 * same id as it is returned from the getAddressBooksForUser method.
503
	 *
504
	 * The cardUri is a base uri, and doesn't include the full path. The
505
	 * cardData argument is the vcard body, and is passed as a string.
506
	 *
507
	 * It is possible to return an ETag from this method. This ETag is for the
508
	 * newly created resource, and must be enclosed with double quotes (that
509
	 * is, the string itself must contain the double quotes).
510
	 *
511
	 * You should only return the ETag if you store the carddata as-is. If a
512
	 * subsequent GET request on the same card does not have the same body,
513
	 * byte-by-byte and you did return an ETag here, clients tend to get
514
	 * confused.
515
	 *
516
	 * If you don't return an ETag, you can just return null.
517
	 *
518
	 * @param mixed $addressBookId
519
	 * @param string $cardUri
520
	 * @param string $cardData
521
	 * @return string
522
	 */
523
	function createCard($addressBookId, $cardUri, $cardData) {
524
		$etag = md5($cardData);
525
526
		$query = $this->db->getQueryBuilder();
527
		$query->insert('cards')
528
			->values([
529
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
530
				'uri' => $query->createNamedParameter($cardUri),
531
				'lastmodified' => $query->createNamedParameter(time()),
532
				'addressbookid' => $query->createNamedParameter($addressBookId),
533
				'size' => $query->createNamedParameter(strlen($cardData)),
534
				'etag' => $query->createNamedParameter($etag),
535
			])
536
			->execute();
537
538
		// Cache the ID so that it can be used for the updateProperties method
539
		$this->idCache->set($addressBookId.$cardUri, $query->getLastInsertId());
540
541
		$this->addChange($addressBookId, $cardUri, 1);
542
		$this->updateProperties($addressBookId, $cardUri, $cardData);
543
544 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...
545
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
546
				new GenericEvent(null, [
547
					'addressBookId' => $addressBookId,
548
					'cardUri' => $cardUri,
549
					'cardData' => $cardData]));
550
		}
551
552
		return '"' . $etag . '"';
553
	}
554
555
	/**
556
	 * Updates a card.
557
	 *
558
	 * The addressbook id will be passed as the first argument. This is the
559
	 * same id as it is returned from the getAddressBooksForUser method.
560
	 *
561
	 * The cardUri is a base uri, and doesn't include the full path. The
562
	 * cardData argument is the vcard body, and is passed as a string.
563
	 *
564
	 * It is possible to return an ETag from this method. This ETag should
565
	 * match that of the updated resource, and must be enclosed with double
566
	 * quotes (that is: the string itself must contain the actual quotes).
567
	 *
568
	 * You should only return the ETag if you store the carddata as-is. If a
569
	 * subsequent GET request on the same card does not have the same body,
570
	 * byte-by-byte and you did return an ETag here, clients tend to get
571
	 * confused.
572
	 *
573
	 * If you don't return an ETag, you can just return null.
574
	 *
575
	 * @param mixed $addressBookId
576
	 * @param string $cardUri
577
	 * @param string $cardData
578
	 * @return string
579
	 */
580
	function updateCard($addressBookId, $cardUri, $cardData) {
581
582
		$etag = md5($cardData);
583
		$query = $this->db->getQueryBuilder();
584
		$query->update('cards')
585
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
586
			->set('lastmodified', $query->createNamedParameter(time()))
587
			->set('size', $query->createNamedParameter(strlen($cardData)))
588
			->set('etag', $query->createNamedParameter($etag))
589
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
590
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
591
			->execute();
592
593
		$this->addChange($addressBookId, $cardUri, 2);
594
		$this->updateProperties($addressBookId, $cardUri, $cardData);
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::updateCard',
598
				new GenericEvent(null, [
599
					'addressBookId' => $addressBookId,
600
					'cardUri' => $cardUri,
601
					'cardData' => $cardData]));
602
		}
603
604
		return '"' . $etag . '"';
605
	}
606
607
	/**
608
	 * Deletes a card
609
	 *
610
	 * @param mixed $addressBookId
611
	 * @param string $cardUri
612
	 * @return bool
613
	 */
614
	function deleteCard($addressBookId, $cardUri) {
615
		try {
616
			$cardId = $this->getCardId($addressBookId, $cardUri);
617
		} catch (\InvalidArgumentException $e) {
618
			$cardId = null;
619
		}
620
		$query = $this->db->getQueryBuilder();
621
		$ret = $query->delete('cards')
622
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
623
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
624
			->execute();
625
626
		$this->addChange($addressBookId, $cardUri, 3);
627
628 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...
629
			$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
630
				new GenericEvent(null, [
631
					'addressBookId' => $addressBookId,
632
					'cardUri' => $cardUri]));
633
		}
634
635
		if ($ret === 1) {
636
			if ($cardId !== null) {
637
				$this->purgeProperties($addressBookId, $cardId);
638
			}
639
			return true;
640
		}
641
642
		return false;
643
	}
644
645
	/**
646
	 * The getChanges method returns all the changes that have happened, since
647
	 * the specified syncToken in the specified address book.
648
	 *
649
	 * This function should return an array, such as the following:
650
	 *
651
	 * [
652
	 *   'syncToken' => 'The current synctoken',
653
	 *   'added'   => [
654
	 *      'new.txt',
655
	 *   ],
656
	 *   'modified'   => [
657
	 *      'modified.txt',
658
	 *   ],
659
	 *   'deleted' => [
660
	 *      'foo.php.bak',
661
	 *      'old.txt'
662
	 *   ]
663
	 * ];
664
	 *
665
	 * The returned syncToken property should reflect the *current* syncToken
666
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
667
	 * property. This is needed here too, to ensure the operation is atomic.
668
	 *
669
	 * If the $syncToken argument is specified as null, this is an initial
670
	 * sync, and all members should be reported.
671
	 *
672
	 * The modified property is an array of nodenames that have changed since
673
	 * the last token.
674
	 *
675
	 * The deleted property is an array with nodenames, that have been deleted
676
	 * from collection.
677
	 *
678
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
679
	 * 1, you only have to report changes that happened only directly in
680
	 * immediate descendants. If it's 2, it should also include changes from
681
	 * the nodes below the child collections. (grandchildren)
682
	 *
683
	 * The $limit argument allows a client to specify how many results should
684
	 * be returned at most. If the limit is not specified, it should be treated
685
	 * as infinite.
686
	 *
687
	 * If the limit (infinite or not) is higher than you're willing to return,
688
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
689
	 *
690
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
691
	 * return null.
692
	 *
693
	 * The limit is 'suggestive'. You are free to ignore it.
694
	 *
695
	 * @param string $addressBookId
696
	 * @param string $syncToken
697
	 * @param int $syncLevel
698
	 * @param int $limit
699
	 * @return array
700
	 */
701 View Code Duplication
	function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
702
		// Current synctoken
703
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
704
		$stmt->execute([ $addressBookId ]);
705
		$currentToken = $stmt->fetchColumn(0);
706
707
		if (is_null($currentToken)) return null;
708
709
		$result = [
710
			'syncToken' => $currentToken,
711
			'added'     => [],
712
			'modified'  => [],
713
			'deleted'   => [],
714
		];
715
716
		if ($syncToken) {
717
718
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
719
			if ($limit>0) {
720
				$query .= " `LIMIT` " . (int)$limit;
721
			}
722
723
			// Fetching all changes
724
			$stmt = $this->db->prepare($query);
725
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
726
727
			$changes = [];
728
729
			// This loop ensures that any duplicates are overwritten, only the
730
			// last change on a node is relevant.
731
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
732
733
				$changes[$row['uri']] = $row['operation'];
734
735
			}
736
737
			foreach($changes as $uri => $operation) {
738
739
				switch($operation) {
740
					case 1:
741
						$result['added'][] = $uri;
742
						break;
743
					case 2:
744
						$result['modified'][] = $uri;
745
						break;
746
					case 3:
747
						$result['deleted'][] = $uri;
748
						break;
749
				}
750
751
			}
752
		} else {
753
			// No synctoken supplied, this is the initial sync.
754
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
755
			$stmt = $this->db->prepare($query);
756
			$stmt->execute([$addressBookId]);
757
758
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
759
		}
760
		return $result;
761
	}
762
763
	/**
764
	 * Adds a change record to the addressbookchanges table.
765
	 *
766
	 * @param mixed $addressBookId
767
	 * @param string $objectUri
768
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
769
	 * @return void
770
	 */
771 View Code Duplication
	protected function addChange($addressBookId, $objectUri, $operation) {
772
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
773
		$stmt = $this->db->prepare($sql);
774
		$stmt->execute([
775
			$objectUri,
776
			$addressBookId,
777
			$operation,
778
			$addressBookId
779
		]);
780
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
781
		$stmt->execute([
782
			$addressBookId
783
		]);
784
	}
785
786
	private function readBlob($cardData) {
787
		if (is_resource($cardData)) {
788
			return stream_get_contents($cardData);
789
		}
790
791
		return $cardData;
792
	}
793
794
	/**
795
	 * @param IShareable $shareable
796
	 * @param string[] $add
797
	 * @param string[] $remove
798
	 */
799
	public function updateShares(IShareable $shareable, $add, $remove) {
800
		$this->sharingBackend->updateShares($shareable, $add, $remove);
801
	}
802
803
	/**
804
	 * search contact
805
	 *
806
	 * @param int $addressBookId
807
	 * @param string $pattern which should match within the $searchProperties
808
	 * @param array $searchProperties defines the properties within the query pattern should match
809
	 * @param int $limit
810
	 * @param int $offset
811
	 * @return array an array of contacts which are arrays of key-value-pairs
812
	 */
813
	public function search($addressBookId, $pattern, $searchProperties, $limit = 100, $offset = 0) {
814
		$query = $this->db->getQueryBuilder();
815
		$query2 = $this->db->getQueryBuilder();
816
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
817
		foreach ($searchProperties as $property) {
818
			$query2->orWhere(
819
				$query2->expr()->andX(
820
					$query2->expr()->eq('cp.name', $query->createNamedParameter($property)),
821
					$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...
822
				)
823
			);
824
		}
825
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
826
827
		$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...
828
			->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
829
830
		$query->setFirstResult($offset)->setMaxResults($limit);
831
		$query->orderBy('c.uri');
832
833
		$result = $query->execute();
834
		$cards = $result->fetchAll();
835
836
		$result->closeCursor();
837
838
		return array_map(function($array) {
839
			$array['carddata'] = $this->readBlob($array['carddata']);
840
			return $array;
841
		}, $cards);
842
	}
843
844
	/**
845
	 * @param int $bookId
846
	 * @param string $name
847
	 * @return array
848
	 */
849
	public function collectCardProperties($bookId, $name) {
850
		$query = $this->db->getQueryBuilder();
851
		$result = $query->selectDistinct('value')
852
			->from($this->dbCardsPropertiesTable)
853
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
854
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
855
			->execute();
856
857
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
858
		$result->closeCursor();
859
860
		return $all;
861
	}
862
863
	/**
864
	 * get URI from a given contact
865
	 *
866
	 * @param int $id
867
	 * @return string
868
	 */
869 View Code Duplication
	public function getCardUri($id) {
870
		$query = $this->db->getQueryBuilder();
871
		$query->select('uri')->from($this->dbCardsTable)
872
				->where($query->expr()->eq('id', $query->createParameter('id')))
873
				->setParameter('id', $id);
874
875
		$result = $query->execute();
876
		$uri = $result->fetch();
877
		$result->closeCursor();
878
879
		if (!isset($uri['uri'])) {
880
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
881
		}
882
883
		return $uri['uri'];
884
	}
885
886
	/**
887
	 * return contact with the given URI
888
	 *
889
	 * @param int $addressBookId
890
	 * @param string $uri
891
	 * @returns array
892
	 */
893
	public function getContact($addressBookId, $uri) {
894
		$result = [];
895
		$query = $this->db->getQueryBuilder();
896
		$query->select('*')->from($this->dbCardsTable)
897
				->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
898
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
899
		$queryResult = $query->execute();
900
		$contact = $queryResult->fetch();
901
		$queryResult->closeCursor();
902
903
		if (is_array($contact)) {
904
			$result = $contact;
905
		}
906
907
		return $result;
908
	}
909
910
	/**
911
	 * Returns the list of people whom this address book is shared with.
912
	 *
913
	 * Every element in this array should have the following properties:
914
	 *   * href - Often a mailto: address
915
	 *   * commonName - Optional, for example a first + last name
916
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
917
	 *   * readOnly - boolean
918
	 *   * summary - Optional, a description for the share
919
	 *
920
	 * @return array
921
	 */
922
	public function getShares($addressBookId) {
923
		return $this->sharingBackend->getShares($addressBookId);
924
	}
925
926
	/**
927
	 * update properties table
928
	 *
929
	 * @param int $addressBookId
930
	 * @param string $cardUri
931
	 * @param string $vCardSerialized
932
	 */
933
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
934
		$cardId = $this->getCardId($addressBookId, $cardUri);
935
		$vCard = $this->readCard($vCardSerialized);
936
937
		$this->purgeProperties($addressBookId, $cardId);
938
939
		$query = $this->db->getQueryBuilder();
940
		$query->insert($this->dbCardsPropertiesTable)
941
			->values(
942
				[
943
					'addressbookid' => $query->createNamedParameter($addressBookId),
944
					'cardid' => $query->createNamedParameter($cardId),
945
					'name' => $query->createParameter('name'),
946
					'value' => $query->createParameter('value'),
947
					'preferred' => $query->createParameter('preferred')
948
				]
949
			);
950
951
		foreach ($vCard->children() as $property) {
952
			if(!in_array($property->name, self::$indexProperties)) {
953
				continue;
954
			}
955
			$preferred = 0;
956
			foreach($property->parameters as $parameter) {
957
				if ($parameter->name == 'TYPE' && strtoupper($parameter->getValue()) == 'PREF') {
958
					$preferred = 1;
959
					break;
960
				}
961
			}
962
			$query->setParameter('name', $property->name);
963
			$query->setParameter('value', substr($property->getValue(), 0, 254));
964
			$query->setParameter('preferred', $preferred);
965
			$query->execute();
966
		}
967
	}
968
969
	/**
970
	 * read vCard data into a vCard object
971
	 *
972
	 * @param string $cardData
973
	 * @return VCard
974
	 */
975
	protected function readCard($cardData) {
976
		return  Reader::read($cardData);
977
	}
978
979
	/**
980
	 * delete all properties from a given card
981
	 *
982
	 * @param int $addressBookId
983
	 * @param int $cardId
984
	 */
985
	protected function purgeProperties($addressBookId, $cardId) {
986
		$query = $this->db->getQueryBuilder();
987
		$query->delete($this->dbCardsPropertiesTable)
988
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
989
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
990
		$query->execute();
991
	}
992
993
	/**
994
	 * get ID from a given contact
995
	 *
996
	 * @param int $addressBookId
997
	 * @param string $uri
998
	 * @return int
999
	 */
1000
	protected function getCardId($addressBookId, $uri) {
1001
		// Try to find cardId from own cache to avoid issue with db cluster
1002
		if($this->idCache->hasKey($addressBookId.$uri)) {
1003
			return $this->idCache->get($addressBookId.$uri);
1004
		}
1005
1006
		$query = $this->db->getQueryBuilder();
1007
		$query->select('id')->from($this->dbCardsTable)
1008
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1009
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1010
1011
		$result = $query->execute();
1012
		$cardIds = $result->fetch();
1013
		$result->closeCursor();
1014
1015
		if (!isset($cardIds['id'])) {
1016
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1017
		}
1018
1019
		return (int)$cardIds['id'];
1020
	}
1021
1022
	/**
1023
	 * For shared address books the sharee is set in the ACL of the address book
1024
	 * @param $addressBookId
1025
	 * @param $acl
1026
	 * @return array
1027
	 */
1028
	public function applyShareAcl($addressBookId, $acl) {
1029
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1030
	}
1031
1032 View Code Duplication
	private function convertPrincipal($principalUri, $toV2 = null) {
1033
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1034
			list(, $name) = URLUtil::splitPath($principalUri);
1035
			$toV2 = $toV2 === null ? !$this->legacyMode : $toV2;
1036
			if ($toV2) {
1037
				return "principals/users/$name";
1038
			}
1039
			return "principals/$name";
1040
		}
1041
		return $principalUri;
1042
	}
1043
}
1044