Completed
Push — master ( 830834...005b3d )
by Thomas
10:38
created

CardDavBackend::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 7

Duplication

Lines 8
Ratio 100 %

Importance

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