Passed
Push — master ( 5cdc85...37718d )
by Morris
38:53 queued 21:57
created

CardDavBackend::readBlob()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

Loading history...
145
		$principalUriOriginal = $principalUri;
146
		$principalUri = $this->convertPrincipal($principalUri, true);
147
		$query = $this->db->getQueryBuilder();
148
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
149
			->from('addressbooks')
150
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
151
152
		$addressBooks = [];
153
154
		$result = $query->execute();
155
		while($row = $result->fetch()) {
156
			$addressBooks[$row['id']] = [
157
				'id'  => $row['id'],
158
				'uri' => $row['uri'],
159
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
160
				'{DAV:}displayname' => $row['displayname'],
161
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
162
				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
163
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
164
			];
165
166
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
167
		}
168
		$result->closeCursor();
169
170
		// query for shared calendars
171
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
172
		$principals = array_map(function($principal) {
173
			return urldecode($principal);
174
		}, $principals);
175
		$principals[]= $principalUri;
176
177
		$query = $this->db->getQueryBuilder();
178
		$result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
179
			->from('dav_shares', 's')
180
			->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
181
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
182
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
183
			->setParameter('type', 'addressbook')
184
			->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY)
185
			->execute();
186
187
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
188
		while($row = $result->fetch()) {
189
			if ($row['principaluri'] === $principalUri) {
190
				continue;
191
			}
192
193
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
194
			if (isset($addressBooks[$row['id']])) {
195
				if ($readOnly) {
196
					// New share can not have more permissions then the old one.
197
					continue;
198
				}
199
				if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
200
					$addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
201
					// Old share is already read-write, no more permissions can be gained
202
					continue;
203
				}
204
			}
205
206
			list(, $name) = \Sabre\Uri\split($row['principaluri']);
207
			$uri = $row['uri'] . '_shared_by_' . $name;
208
			$displayName = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
209
210
			$addressBooks[$row['id']] = [
211
				'id'  => $row['id'],
212
				'uri' => $uri,
213
				'principaluri' => $principalUriOriginal,
214
				'{DAV:}displayname' => $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
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
219
				$readOnlyPropertyName => $readOnly,
220
			];
221
222
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
223
		}
224
		$result->closeCursor();
225
226
		return array_values($addressBooks);
227
	}
228
229
	public function getUsersOwnAddressBooks($principalUri) {
230
		$principalUri = $this->convertPrincipal($principalUri, true);
231
		$query = $this->db->getQueryBuilder();
232
		$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
233
			  ->from('addressbooks')
234
			  ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
235
236
		$addressBooks = [];
237
238
		$result = $query->execute();
239
		while($row = $result->fetch()) {
240
			$addressBooks[$row['id']] = [
241
				'id'  => $row['id'],
242
				'uri' => $row['uri'],
243
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
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
			$this->addOwnerPrincipal($addressBooks[$row['id']]);
251
		}
252
		$result->closeCursor();
253
254
		return array_values($addressBooks);
255
	}
256
257
	private function getUserDisplayName($uid) {
258
		if (!isset($this->userDisplayNames[$uid])) {
259
			$user = $this->userManager->get($uid);
260
261
			if ($user instanceof IUser) {
262
				$this->userDisplayNames[$uid] = $user->getDisplayName();
263
			} else {
264
				$this->userDisplayNames[$uid] = $uid;
265
			}
266
		}
267
268
		return $this->userDisplayNames[$uid];
269
	}
270
271
	/**
272
	 * @param int $addressBookId
273
	 */
274
	public function getAddressBookById($addressBookId) {
275
		$query = $this->db->getQueryBuilder();
276
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
277
			->from('addressbooks')
278
			->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
279
			->execute();
280
281
		$row = $result->fetch();
282
		$result->closeCursor();
283
		if ($row === false) {
284
			return null;
285
		}
286
287
		$addressBook = [
288
			'id'  => $row['id'],
289
			'uri' => $row['uri'],
290
			'principaluri' => $row['principaluri'],
291
			'{DAV:}displayname' => $row['displayname'],
292
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
293
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
294
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
295
		];
296
297
		$this->addOwnerPrincipal($addressBook);
298
299
		return $addressBook;
300
	}
301
302
	/**
303
	 * @param $addressBookUri
304
	 * @return array|null
305
	 */
306
	public function getAddressBooksByUri($principal, $addressBookUri) {
307
		$query = $this->db->getQueryBuilder();
308
		$result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
309
			->from('addressbooks')
310
			->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
311
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
312
			->setMaxResults(1)
313
			->execute();
314
315
		$row = $result->fetch();
316
		$result->closeCursor();
317
		if ($row === false) {
318
			return null;
319
		}
320
321
		$addressBook = [
322
			'id'  => $row['id'],
323
			'uri' => $row['uri'],
324
			'principaluri' => $row['principaluri'],
325
			'{DAV:}displayname' => $row['displayname'],
326
			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
327
			'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
328
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
329
		];
330
331
		$this->addOwnerPrincipal($addressBook);
332
333
		return $addressBook;
334
	}
335
336
	/**
337
	 * Updates properties for an address book.
338
	 *
339
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
340
	 * To do the actual updates, you must tell this object which properties
341
	 * you're going to process with the handle() method.
342
	 *
343
	 * Calling the handle method is like telling the PropPatch object "I
344
	 * promise I can handle updating this property".
345
	 *
346
	 * Read the PropPatch documentation for more info and examples.
347
	 *
348
	 * @param string $addressBookId
349
	 * @param \Sabre\DAV\PropPatch $propPatch
350
	 * @return void
351
	 */
352
	function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
353
		$supportedProperties = [
354
			'{DAV:}displayname',
355
			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
356
		];
357
358
		/**
359
		 * @suppress SqlInjectionChecker
360
		 */
361
		$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
362
363
			$updates = [];
364
			foreach($mutations as $property=>$newValue) {
365
366
				switch($property) {
367
					case '{DAV:}displayname' :
368
						$updates['displayname'] = $newValue;
369
						break;
370
					case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
371
						$updates['description'] = $newValue;
372
						break;
373
				}
374
			}
375
			$query = $this->db->getQueryBuilder();
376
			$query->update('addressbooks');
377
378
			foreach($updates as $key=>$value) {
379
				$query->set($key, $query->createNamedParameter($value));
380
			}
381
			$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
382
			->execute();
383
384
			$this->addChange($addressBookId, "", 2);
385
386
			return true;
387
388
		});
389
	}
390
391
	/**
392
	 * Creates a new address book
393
	 *
394
	 * @param string $principalUri
395
	 * @param string $url Just the 'basename' of the url.
396
	 * @param array $properties
397
	 * @return int
398
	 * @throws BadRequest
399
	 */
400
	function createAddressBook($principalUri, $url, array $properties) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
401
		$values = [
402
			'displayname' => null,
403
			'description' => null,
404
			'principaluri' => $principalUri,
405
			'uri' => $url,
406
			'synctoken' => 1
407
		];
408
409
		foreach($properties as $property=>$newValue) {
410
411
			switch($property) {
412
				case '{DAV:}displayname' :
413
					$values['displayname'] = $newValue;
414
					break;
415
				case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
416
					$values['description'] = $newValue;
417
					break;
418
				default :
419
					throw new BadRequest('Unknown property: ' . $property);
420
			}
421
422
		}
423
424
		// Fallback to make sure the displayname is set. Some clients may refuse
425
		// to work with addressbooks not having a displayname.
426
		if(is_null($values['displayname'])) {
427
			$values['displayname'] = $url;
428
		}
429
430
		$query = $this->db->getQueryBuilder();
431
		$query->insert('addressbooks')
432
			->values([
433
				'uri' => $query->createParameter('uri'),
434
				'displayname' => $query->createParameter('displayname'),
435
				'description' => $query->createParameter('description'),
436
				'principaluri' => $query->createParameter('principaluri'),
437
				'synctoken' => $query->createParameter('synctoken'),
438
			])
439
			->setParameters($values)
440
			->execute();
441
442
		return $query->getLastInsertId();
443
	}
444
445
	/**
446
	 * Deletes an entire addressbook and all its contents
447
	 *
448
	 * @param mixed $addressBookId
449
	 * @return void
450
	 */
451
	function deleteAddressBook($addressBookId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
452
		$query = $this->db->getQueryBuilder();
453
		$query->delete('cards')
454
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
455
			->setParameter('addressbookid', $addressBookId)
456
			->execute();
457
458
		$query->delete('addressbookchanges')
459
			->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
460
			->setParameter('addressbookid', $addressBookId)
461
			->execute();
462
463
		$query->delete('addressbooks')
464
			->where($query->expr()->eq('id', $query->createParameter('id')))
465
			->setParameter('id', $addressBookId)
466
			->execute();
467
468
		$this->sharingBackend->deleteAllShares($addressBookId);
469
470
		$query->delete($this->dbCardsPropertiesTable)
471
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
472
			->execute();
473
474
	}
475
476
	/**
477
	 * Returns all cards for a specific addressbook id.
478
	 *
479
	 * This method should return the following properties for each card:
480
	 *   * carddata - raw vcard data
481
	 *   * uri - Some unique url
482
	 *   * lastmodified - A unix timestamp
483
	 *
484
	 * It's recommended to also return the following properties:
485
	 *   * etag - A unique etag. This must change every time the card changes.
486
	 *   * size - The size of the card in bytes.
487
	 *
488
	 * If these last two properties are provided, less time will be spent
489
	 * calculating them. If they are specified, you can also ommit carddata.
490
	 * This may speed up certain requests, especially with large cards.
491
	 *
492
	 * @param mixed $addressBookId
493
	 * @return array
494
	 */
495
	function getCards($addressBookId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
496
		$query = $this->db->getQueryBuilder();
497
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
498
			->from('cards')
499
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
500
501
		$cards = [];
502
503
		$result = $query->execute();
504
		while($row = $result->fetch()) {
505
			$row['etag'] = '"' . $row['etag'] . '"';
506
			$row['carddata'] = $this->readBlob($row['carddata']);
507
			$cards[] = $row;
508
		}
509
		$result->closeCursor();
510
511
		return $cards;
512
	}
513
514
	/**
515
	 * Returns a specific card.
516
	 *
517
	 * The same set of properties must be returned as with getCards. The only
518
	 * exception is that 'carddata' is absolutely required.
519
	 *
520
	 * If the card does not exist, you must return false.
521
	 *
522
	 * @param mixed $addressBookId
523
	 * @param string $cardUri
524
	 * @return array
525
	 */
526
	function getCard($addressBookId, $cardUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
527
		$query = $this->db->getQueryBuilder();
528
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
529
			->from('cards')
530
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
531
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
532
			->setMaxResults(1);
533
534
		$result = $query->execute();
535
		$row = $result->fetch();
536
		if (!$row) {
537
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
538
		}
539
		$row['etag'] = '"' . $row['etag'] . '"';
540
		$row['carddata'] = $this->readBlob($row['carddata']);
541
542
		return $row;
543
	}
544
545
	/**
546
	 * Returns a list of cards.
547
	 *
548
	 * This method should work identical to getCard, but instead return all the
549
	 * cards in the list as an array.
550
	 *
551
	 * If the backend supports this, it may allow for some speed-ups.
552
	 *
553
	 * @param mixed $addressBookId
554
	 * @param string[] $uris
555
	 * @return array
556
	 */
557
	function getMultipleCards($addressBookId, array $uris) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
558
		if (empty($uris)) {
559
			return [];
560
		}
561
562
		$chunks = array_chunk($uris, 100);
563
		$cards = [];
564
565
		$query = $this->db->getQueryBuilder();
566
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
567
			->from('cards')
568
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
569
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
570
571
		foreach ($chunks as $uris) {
572
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
573
			$result = $query->execute();
574
575
			while ($row = $result->fetch()) {
576
				$row['etag'] = '"' . $row['etag'] . '"';
577
				$row['carddata'] = $this->readBlob($row['carddata']);
578
				$cards[] = $row;
579
			}
580
			$result->closeCursor();
581
		}
582
		return $cards;
583
	}
584
585
	/**
586
	 * Creates a new card.
587
	 *
588
	 * The addressbook id will be passed as the first argument. This is the
589
	 * same id as it is returned from the getAddressBooksForUser method.
590
	 *
591
	 * The cardUri is a base uri, and doesn't include the full path. The
592
	 * cardData argument is the vcard body, and is passed as a string.
593
	 *
594
	 * It is possible to return an ETag from this method. This ETag is for the
595
	 * newly created resource, and must be enclosed with double quotes (that
596
	 * is, the string itself must contain the double quotes).
597
	 *
598
	 * You should only return the ETag if you store the carddata as-is. If a
599
	 * subsequent GET request on the same card does not have the same body,
600
	 * byte-by-byte and you did return an ETag here, clients tend to get
601
	 * confused.
602
	 *
603
	 * If you don't return an ETag, you can just return null.
604
	 *
605
	 * @param mixed $addressBookId
606
	 * @param string $cardUri
607
	 * @param string $cardData
608
	 * @return string
609
	 */
610
	function createCard($addressBookId, $cardUri, $cardData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
611
		$etag = md5($cardData);
612
613
		$query = $this->db->getQueryBuilder();
614
		$query->insert('cards')
615
			->values([
616
				'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB),
617
				'uri' => $query->createNamedParameter($cardUri),
618
				'lastmodified' => $query->createNamedParameter(time()),
619
				'addressbookid' => $query->createNamedParameter($addressBookId),
620
				'size' => $query->createNamedParameter(strlen($cardData)),
621
				'etag' => $query->createNamedParameter($etag),
622
			])
623
			->execute();
624
625
		$this->addChange($addressBookId, $cardUri, 1);
626
		$this->updateProperties($addressBookId, $cardUri, $cardData);
627
628
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard',
629
			new GenericEvent(null, [
630
				'addressBookId' => $addressBookId,
631
				'cardUri' => $cardUri,
632
				'cardData' => $cardData]));
633
634
		return '"' . $etag . '"';
635
	}
636
637
	/**
638
	 * Updates a card.
639
	 *
640
	 * The addressbook id will be passed as the first argument. This is the
641
	 * same id as it is returned from the getAddressBooksForUser method.
642
	 *
643
	 * The cardUri is a base uri, and doesn't include the full path. The
644
	 * cardData argument is the vcard body, and is passed as a string.
645
	 *
646
	 * It is possible to return an ETag from this method. This ETag should
647
	 * match that of the updated resource, and must be enclosed with double
648
	 * quotes (that is: the string itself must contain the actual quotes).
649
	 *
650
	 * You should only return the ETag if you store the carddata as-is. If a
651
	 * subsequent GET request on the same card does not have the same body,
652
	 * byte-by-byte and you did return an ETag here, clients tend to get
653
	 * confused.
654
	 *
655
	 * If you don't return an ETag, you can just return null.
656
	 *
657
	 * @param mixed $addressBookId
658
	 * @param string $cardUri
659
	 * @param string $cardData
660
	 * @return string
661
	 */
662
	function updateCard($addressBookId, $cardUri, $cardData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
663
664
		$etag = md5($cardData);
665
		$query = $this->db->getQueryBuilder();
666
		$query->update('cards')
667
			->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
668
			->set('lastmodified', $query->createNamedParameter(time()))
669
			->set('size', $query->createNamedParameter(strlen($cardData)))
670
			->set('etag', $query->createNamedParameter($etag))
671
			->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
672
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
673
			->execute();
674
675
		$this->addChange($addressBookId, $cardUri, 2);
676
		$this->updateProperties($addressBookId, $cardUri, $cardData);
677
678
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard',
679
			new GenericEvent(null, [
680
				'addressBookId' => $addressBookId,
681
				'cardUri' => $cardUri,
682
				'cardData' => $cardData]));
683
684
		return '"' . $etag . '"';
685
	}
686
687
	/**
688
	 * Deletes a card
689
	 *
690
	 * @param mixed $addressBookId
691
	 * @param string $cardUri
692
	 * @return bool
693
	 */
694
	function deleteCard($addressBookId, $cardUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
695
		try {
696
			$cardId = $this->getCardId($addressBookId, $cardUri);
697
		} catch (\InvalidArgumentException $e) {
698
			$cardId = null;
699
		}
700
		$query = $this->db->getQueryBuilder();
701
		$ret = $query->delete('cards')
702
			->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
703
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
704
			->execute();
705
706
		$this->addChange($addressBookId, $cardUri, 3);
707
708
		$this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard',
709
			new GenericEvent(null, [
710
				'addressBookId' => $addressBookId,
711
				'cardUri' => $cardUri]));
712
713
		if ($ret === 1) {
714
			if ($cardId !== null) {
715
				$this->purgeProperties($addressBookId, $cardId);
716
			}
717
			return true;
718
		}
719
720
		return false;
721
	}
722
723
	/**
724
	 * The getChanges method returns all the changes that have happened, since
725
	 * the specified syncToken in the specified address book.
726
	 *
727
	 * This function should return an array, such as the following:
728
	 *
729
	 * [
730
	 *   'syncToken' => 'The current synctoken',
731
	 *   'added'   => [
732
	 *      'new.txt',
733
	 *   ],
734
	 *   'modified'   => [
735
	 *      'modified.txt',
736
	 *   ],
737
	 *   'deleted' => [
738
	 *      'foo.php.bak',
739
	 *      'old.txt'
740
	 *   ]
741
	 * ];
742
	 *
743
	 * The returned syncToken property should reflect the *current* syncToken
744
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
745
	 * property. This is needed here too, to ensure the operation is atomic.
746
	 *
747
	 * If the $syncToken argument is specified as null, this is an initial
748
	 * sync, and all members should be reported.
749
	 *
750
	 * The modified property is an array of nodenames that have changed since
751
	 * the last token.
752
	 *
753
	 * The deleted property is an array with nodenames, that have been deleted
754
	 * from collection.
755
	 *
756
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
757
	 * 1, you only have to report changes that happened only directly in
758
	 * immediate descendants. If it's 2, it should also include changes from
759
	 * the nodes below the child collections. (grandchildren)
760
	 *
761
	 * The $limit argument allows a client to specify how many results should
762
	 * be returned at most. If the limit is not specified, it should be treated
763
	 * as infinite.
764
	 *
765
	 * If the limit (infinite or not) is higher than you're willing to return,
766
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
767
	 *
768
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
769
	 * return null.
770
	 *
771
	 * The limit is 'suggestive'. You are free to ignore it.
772
	 *
773
	 * @param string $addressBookId
774
	 * @param string $syncToken
775
	 * @param int $syncLevel
776
	 * @param int $limit
777
	 * @return array
778
	 */
779
	function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

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

Loading history...
780
		// Current synctoken
781
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
782
		$stmt->execute([ $addressBookId ]);
783
		$currentToken = $stmt->fetchColumn(0);
784
785
		if (is_null($currentToken)) return null;
0 ignored issues
show
introduced by
The condition is_null($currentToken) is always false.
Loading history...
786
787
		$result = [
788
			'syncToken' => $currentToken,
789
			'added'     => [],
790
			'modified'  => [],
791
			'deleted'   => [],
792
		];
793
794
		if ($syncToken) {
795
796
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
797
			if ($limit>0) {
798
				$query .= " `LIMIT` " . (int)$limit;
799
			}
800
801
			// Fetching all changes
802
			$stmt = $this->db->prepare($query);
803
			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
804
805
			$changes = [];
806
807
			// This loop ensures that any duplicates are overwritten, only the
808
			// last change on a node is relevant.
809
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
810
811
				$changes[$row['uri']] = $row['operation'];
812
813
			}
814
815
			foreach($changes as $uri => $operation) {
816
817
				switch($operation) {
818
					case 1:
819
						$result['added'][] = $uri;
820
						break;
821
					case 2:
822
						$result['modified'][] = $uri;
823
						break;
824
					case 3:
825
						$result['deleted'][] = $uri;
826
						break;
827
				}
828
829
			}
830
		} else {
831
			// No synctoken supplied, this is the initial sync.
832
			$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
833
			$stmt = $this->db->prepare($query);
834
			$stmt->execute([$addressBookId]);
835
836
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
837
		}
838
		return $result;
839
	}
840
841
	/**
842
	 * Adds a change record to the addressbookchanges table.
843
	 *
844
	 * @param mixed $addressBookId
845
	 * @param string $objectUri
846
	 * @param int $operation 1 = add, 2 = modify, 3 = delete
847
	 * @return void
848
	 */
849
	protected function addChange($addressBookId, $objectUri, $operation) {
850
		$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
851
		$stmt = $this->db->prepare($sql);
852
		$stmt->execute([
853
			$objectUri,
854
			$addressBookId,
855
			$operation,
856
			$addressBookId
857
		]);
858
		$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
859
		$stmt->execute([
860
			$addressBookId
861
		]);
862
	}
863
864
	private function readBlob($cardData) {
865
		if (is_resource($cardData)) {
866
			return stream_get_contents($cardData);
867
		}
868
869
		return $cardData;
870
	}
871
872
	/**
873
	 * @param IShareable $shareable
874
	 * @param string[] $add
875
	 * @param string[] $remove
876
	 */
877
	public function updateShares(IShareable $shareable, $add, $remove) {
878
		$this->sharingBackend->updateShares($shareable, $add, $remove);
879
	}
880
881
	/**
882
	 * search contact
883
	 *
884
	 * @param int $addressBookId
885
	 * @param string $pattern which should match within the $searchProperties
886
	 * @param array $searchProperties defines the properties within the query pattern should match
887
	 * @return array an array of contacts which are arrays of key-value-pairs
888
	 */
889
	public function search($addressBookId, $pattern, $searchProperties) {
890
		$query = $this->db->getQueryBuilder();
891
		$query2 = $this->db->getQueryBuilder();
892
893
		$query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp');
894
		$query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
895
		$or = $query2->expr()->orX();
896
		foreach ($searchProperties as $property) {
897
			$or->add($query2->expr()->eq('cp.name', $query->createNamedParameter($property)));
898
		}
899
		$query2->andWhere($or);
900
901
		// No need for like when the pattern is empty
902
		if ('' !== $pattern) {
903
			$query2->andWhere($query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
904
		}
905
906
		$query->select('c.carddata', 'c.uri')->from($this->dbCardsTable, 'c')
907
			->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL())));
908
909
		$result = $query->execute();
910
		$cards = $result->fetchAll();
911
912
		$result->closeCursor();
913
914
		return array_map(function($array) {
915
			$array['carddata'] = $this->readBlob($array['carddata']);
916
			return $array;
917
		}, $cards);
918
	}
919
920
	/**
921
	 * @param int $bookId
922
	 * @param string $name
923
	 * @return array
924
	 */
925
	public function collectCardProperties($bookId, $name) {
926
		$query = $this->db->getQueryBuilder();
927
		$result = $query->selectDistinct('value')
928
			->from($this->dbCardsPropertiesTable)
929
			->where($query->expr()->eq('name', $query->createNamedParameter($name)))
930
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
931
			->execute();
932
933
		$all = $result->fetchAll(PDO::FETCH_COLUMN);
934
		$result->closeCursor();
935
936
		return $all;
937
	}
938
939
	/**
940
	 * get URI from a given contact
941
	 *
942
	 * @param int $id
943
	 * @return string
944
	 */
945
	public function getCardUri($id) {
946
		$query = $this->db->getQueryBuilder();
947
		$query->select('uri')->from($this->dbCardsTable)
948
				->where($query->expr()->eq('id', $query->createParameter('id')))
949
				->setParameter('id', $id);
950
951
		$result = $query->execute();
952
		$uri = $result->fetch();
953
		$result->closeCursor();
954
955
		if (!isset($uri['uri'])) {
956
			throw new \InvalidArgumentException('Card does not exists: ' . $id);
957
		}
958
959
		return $uri['uri'];
960
	}
961
962
	/**
963
	 * return contact with the given URI
964
	 *
965
	 * @param int $addressBookId
966
	 * @param string $uri
967
	 * @returns array
968
	 */
969
	public function getContact($addressBookId, $uri) {
970
		$result = [];
971
		$query = $this->db->getQueryBuilder();
972
		$query->select('*')->from($this->dbCardsTable)
973
				->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
974
				->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
975
		$queryResult = $query->execute();
976
		$contact = $queryResult->fetch();
977
		$queryResult->closeCursor();
978
979
		if (is_array($contact)) {
980
			$result = $contact;
981
		}
982
983
		return $result;
984
	}
985
986
	/**
987
	 * Returns the list of people whom this address book is shared with.
988
	 *
989
	 * Every element in this array should have the following properties:
990
	 *   * href - Often a mailto: address
991
	 *   * commonName - Optional, for example a first + last name
992
	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
993
	 *   * readOnly - boolean
994
	 *   * summary - Optional, a description for the share
995
	 *
996
	 * @return array
997
	 */
998
	public function getShares($addressBookId) {
999
		return $this->sharingBackend->getShares($addressBookId);
1000
	}
1001
1002
	/**
1003
	 * update properties table
1004
	 *
1005
	 * @param int $addressBookId
1006
	 * @param string $cardUri
1007
	 * @param string $vCardSerialized
1008
	 */
1009
	protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
1010
		$cardId = $this->getCardId($addressBookId, $cardUri);
1011
		$vCard = $this->readCard($vCardSerialized);
1012
1013
		$this->purgeProperties($addressBookId, $cardId);
1014
1015
		$query = $this->db->getQueryBuilder();
1016
		$query->insert($this->dbCardsPropertiesTable)
1017
			->values(
1018
				[
1019
					'addressbookid' => $query->createNamedParameter($addressBookId),
1020
					'cardid' => $query->createNamedParameter($cardId),
1021
					'name' => $query->createParameter('name'),
1022
					'value' => $query->createParameter('value'),
1023
					'preferred' => $query->createParameter('preferred')
1024
				]
1025
			);
1026
1027
		foreach ($vCard->children() as $property) {
1028
			if(!in_array($property->name, self::$indexProperties)) {
1029
				continue;
1030
			}
1031
			$preferred = 0;
1032
			foreach($property->parameters as $parameter) {
1033
				if ($parameter->name === 'TYPE' && strtoupper($parameter->getValue()) === 'PREF') {
1034
					$preferred = 1;
1035
					break;
1036
				}
1037
			}
1038
			$query->setParameter('name', $property->name);
1039
			$query->setParameter('value', substr($property->getValue(), 0, 254));
1040
			$query->setParameter('preferred', $preferred);
1041
			$query->execute();
1042
		}
1043
	}
1044
1045
	/**
1046
	 * read vCard data into a vCard object
1047
	 *
1048
	 * @param string $cardData
1049
	 * @return VCard
1050
	 */
1051
	protected function readCard($cardData) {
1052
		return  Reader::read($cardData);
1053
	}
1054
1055
	/**
1056
	 * delete all properties from a given card
1057
	 *
1058
	 * @param int $addressBookId
1059
	 * @param int $cardId
1060
	 */
1061
	protected function purgeProperties($addressBookId, $cardId) {
1062
		$query = $this->db->getQueryBuilder();
1063
		$query->delete($this->dbCardsPropertiesTable)
1064
			->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
1065
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1066
		$query->execute();
1067
	}
1068
1069
	/**
1070
	 * get ID from a given contact
1071
	 *
1072
	 * @param int $addressBookId
1073
	 * @param string $uri
1074
	 * @return int
1075
	 */
1076
	protected function getCardId($addressBookId, $uri) {
1077
		$query = $this->db->getQueryBuilder();
1078
		$query->select('id')->from($this->dbCardsTable)
1079
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
1080
			->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
1081
1082
		$result = $query->execute();
1083
		$cardIds = $result->fetch();
1084
		$result->closeCursor();
1085
1086
		if (!isset($cardIds['id'])) {
1087
			throw new \InvalidArgumentException('Card does not exists: ' . $uri);
1088
		}
1089
1090
		return (int)$cardIds['id'];
1091
	}
1092
1093
	/**
1094
	 * For shared address books the sharee is set in the ACL of the address book
1095
	 * @param $addressBookId
1096
	 * @param $acl
1097
	 * @return array
1098
	 */
1099
	public function applyShareAcl($addressBookId, $acl) {
1100
		return $this->sharingBackend->applyShareAcl($addressBookId, $acl);
1101
	}
1102
1103
	private function convertPrincipal($principalUri, $toV2) {
1104
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1105
			list(, $name) = \Sabre\Uri\split($principalUri);
1106
			if ($toV2 === true) {
1107
				return "principals/users/$name";
1108
			}
1109
			return "principals/$name";
1110
		}
1111
		return $principalUri;
1112
	}
1113
1114
	private function addOwnerPrincipal(&$addressbookInfo) {
1115
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
1116
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
1117
		if (isset($addressbookInfo[$ownerPrincipalKey])) {
1118
			$uri = $addressbookInfo[$ownerPrincipalKey];
1119
		} else {
1120
			$uri = $addressbookInfo['principaluri'];
1121
		}
1122
1123
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
1124
		if (isset($principalInformation['{DAV:}displayname'])) {
1125
			$addressbookInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
1126
		}
1127
	}
1128
}
1129