Completed
Push — master ( d18396...6481a3 )
by Thomas
20:16 queued 08:00
created

CardDavBackend::readCard()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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