Passed
Push — master ( 1f2a9d...b6d734 )
by Christoph
16:55 queued 12s
created

SystemAddressbook::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 8
dl 0
loc 17
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2018, Roeland Jago Douma <[email protected]>
7
 *
8
 * @author Joas Schilling <[email protected]>
9
 * @author Julius Härtl <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 * @author Anna Larch <[email protected]>
12
 *
13
 * @license GNU AGPL version 3 or any later version
14
 *
15
 * This program is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License as
17
 * published by the Free Software Foundation, either version 3 of the
18
 * License, or (at your option) any later version.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License
26
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27
 *
28
 */
29
namespace OCA\DAV\CardDAV;
30
31
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
32
use OCA\Federation\TrustedServers;
33
use OCP\Accounts\IAccountManager;
34
use OCP\IConfig;
35
use OCP\IGroupManager;
36
use OCP\IL10N;
37
use OCP\IRequest;
38
use OCP\IUserSession;
39
use Sabre\CardDAV\Backend\SyncSupport;
40
use Sabre\CardDAV\Backend\BackendInterface;
41
use Sabre\CardDAV\Card;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, OCA\DAV\CardDAV\Card. 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...
42
use Sabre\DAV\Exception\Forbidden;
43
use Sabre\DAV\Exception\NotFound;
44
use Sabre\VObject\Component\VCard;
45
use Sabre\VObject\Reader;
46
use function array_filter;
47
use function array_intersect;
48
use function array_unique;
49
use function in_array;
50
51
class SystemAddressbook extends AddressBook {
52
	public const URI_SHARED = 'z-server-generated--system';
53
	/** @var IConfig */
54
	private $config;
55
	private IUserSession $userSession;
56
	private ?TrustedServers $trustedServers;
57
	private ?IRequest $request;
58
	private ?IGroupManager $groupManager;
59
60
	public function __construct(BackendInterface $carddavBackend,
61
		array $addressBookInfo,
62
		IL10N $l10n,
63
		IConfig $config,
64
		IUserSession $userSession,
65
		?IRequest $request = null,
66
		?TrustedServers $trustedServers = null,
67
		?IGroupManager $groupManager) {
68
		parent::__construct($carddavBackend, $addressBookInfo, $l10n);
69
		$this->config = $config;
70
		$this->userSession = $userSession;
71
		$this->request = $request;
72
		$this->trustedServers = $trustedServers;
73
		$this->groupManager = $groupManager;
74
75
		$this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Accounts');
76
		$this->addressBookInfo['{' . Plugin::NS_CARDDAV . '}addressbook-description'] = $l10n->t('System address book which holds all accounts');
77
	}
78
79
	/**
80
	 * No checkbox checked -> Show only the same user
81
	 * 'Allow username autocompletion in share dialog' -> show everyone
82
	 * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' -> show only users in intersecting groups
83
	 * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users based on phone number integration' -> show only the same user
84
	 * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' + 'Allow username autocompletion to users based on phone number integration' -> show only users in intersecting groups
85
	 */
86
	public function getChildren() {
87
		$shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
88
		$shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
89
		$shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
90
		$user = $this->userSession->getUser();
91
		if (!$user) {
92
			// Should never happen because we don't allow anonymous access
93
			return [];
94
		}
95
		if (!$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
96
			$name = SyncService::getCardUri($user);
97
			try {
98
				return [parent::getChild($name)];
99
			} catch (NotFound $e) {
100
				return [];
101
			}
102
		}
103
		if ($shareEnumerationGroup) {
104
			if ($this->groupManager === null) {
105
				// Group manager is not available, so we can't determine which data is safe
106
				return [];
107
			}
108
			$groups = $this->groupManager->getUserGroups($user);
109
			$names = [];
110
			foreach ($groups as $group) {
111
				$users = $group->getUsers();
112
				foreach ($users as $groupUser) {
113
					if ($groupUser->getBackendClassName() === 'Guests') {
114
						continue;
115
					}
116
					$names[] = SyncService::getCardUri($groupUser);
117
				}
118
			}
119
			return parent::getMultipleChildren(array_unique($names));
120
		}
121
122
		$children = parent::getChildren();
123
		return array_filter($children, function (Card $child) {
124
			// check only for URIs that begin with Guests:
125
			return strpos($child->getName(), 'Guests:') !== 0;
126
		});
127
	}
128
129
	/**
130
	 * @param array $paths
131
	 * @return Card[]
132
	 * @throws NotFound
133
	 */
134
	public function getMultipleChildren($paths): array {
135
		$shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
136
		$shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
137
		$shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
138
		if (!$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
139
			$user = $this->userSession->getUser();
140
			// No user or cards with no access
141
			if ($user === null || !in_array(SyncService::getCardUri($user), $paths, true)) {
142
				return [];
143
			}
144
			// Only return the own card
145
			try {
146
				return [parent::getChild(SyncService::getCardUri($user))];
147
			} catch (NotFound $e) {
148
				return [];
149
			}
150
		}
151
		if ($shareEnumerationGroup) {
152
			$user = $this->userSession->getUser();
153
			if ($this->groupManager === null || $user === null) {
154
				// Group manager or user is not available, so we can't determine which data is safe
155
				return [];
156
			}
157
			$groups = $this->groupManager->getUserGroups($user);
158
			$allowedNames = [];
159
			foreach ($groups as $group) {
160
				$users = $group->getUsers();
161
				foreach ($users as $groupUser) {
162
					if ($groupUser->getBackendClassName() === 'Guests') {
163
						continue;
164
					}
165
					$allowedNames[] = SyncService::getCardUri($groupUser);
166
				}
167
			}
168
			return parent::getMultipleChildren(array_intersect($paths, $allowedNames));
169
		}
170
		if (!$this->isFederation()) {
171
			return parent::getMultipleChildren($paths);
172
		}
173
174
		$objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
175
		$children = [];
176
		/** @var array $obj */
177
		foreach ($objs as $obj) {
178
			if (empty($obj)) {
179
				continue;
180
			}
181
			$carddata = $this->extractCarddata($obj);
182
			if (empty($carddata)) {
183
				continue;
184
			} else {
185
				$obj['carddata'] = $carddata;
186
			}
187
			$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
188
		}
189
		return $children;
190
	}
191
192
	/**
193
	 * @param string $name
194
	 * @return Card
195
	 * @throws NotFound
196
	 * @throws Forbidden
197
	 */
198
	public function getChild($name): Card {
199
		$shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
200
		$shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
201
		$shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
202
		if (!$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
203
			$currentUser = $this->userSession->getUser();
204
			$ownName = $currentUser !== null ? SyncService::getCardUri($currentUser) : null;
205
			if ($ownName === $name) {
206
				return parent::getChild($name);
207
			}
208
			throw new Forbidden();
209
		}
210
		if ($shareEnumerationGroup) {
211
			$user = $this->userSession->getUser();
212
			if ($user === null || $this->groupManager === null) {
213
				// Group manager is not available, so we can't determine which data is safe
214
				throw new Forbidden();
215
			}
216
			$groups = $this->groupManager->getUserGroups($user);
217
			foreach ($groups as $group) {
218
				foreach ($group->getUsers() as $groupUser) {
219
					if ($groupUser->getBackendClassName() === 'Guests') {
220
						continue;
221
					}
222
					$otherName = SyncService::getCardUri($groupUser);
223
					if ($otherName === $name) {
224
						return parent::getChild($name);
225
					}
226
				}
227
			}
228
			throw new Forbidden();
229
		}
230
		if (!$this->isFederation()) {
231
			return parent::getChild($name);
232
		}
233
234
		$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
235
		if (!$obj) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $obj of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
236
			throw new NotFound('Card not found');
237
		}
238
		$carddata = $this->extractCarddata($obj);
239
		if (empty($carddata)) {
240
			throw new Forbidden();
241
		} else {
242
			$obj['carddata'] = $carddata;
243
		}
244
		return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
245
	}
246
247
	/**
248
	 * @throws UnsupportedLimitOnInitialSyncException
249
	 */
250
	public function getChanges($syncToken, $syncLevel, $limit = null) {
251
		if (!$syncToken && $limit) {
252
			throw new UnsupportedLimitOnInitialSyncException();
253
		}
254
255
		if (!$this->carddavBackend instanceof SyncSupport) {
0 ignored issues
show
introduced by
$this->carddavBackend is always a sub-type of Sabre\CardDAV\Backend\SyncSupport.
Loading history...
256
			return null;
257
		}
258
259
		if (!$this->isFederation()) {
260
			return parent::getChanges($syncToken, $syncLevel, $limit);
261
		}
262
263
		$changed = $this->carddavBackend->getChangesForAddressBook(
264
			$this->addressBookInfo['id'],
265
			$syncToken,
266
			$syncLevel,
267
			$limit
268
		);
269
270
		if (empty($changed)) {
271
			return $changed;
272
		}
273
274
		$added = $modified = $deleted = [];
275
		foreach ($changed['added'] as $uri) {
276
			try {
277
				$this->getChild($uri);
278
				$added[] = $uri;
279
			} catch (NotFound | Forbidden $e) {
280
				$deleted[] = $uri;
281
			}
282
		}
283
		foreach ($changed['modified'] as $uri) {
284
			try {
285
				$this->getChild($uri);
286
				$modified[] = $uri;
287
			} catch (NotFound | Forbidden $e) {
288
				$deleted[] = $uri;
289
			}
290
		}
291
		$changed['added'] = $added;
292
		$changed['modified'] = $modified;
293
		$changed['deleted'] = $deleted;
294
		return $changed;
295
	}
296
297
	private function isFederation(): bool {
298
		if ($this->trustedServers === null || $this->request === null) {
299
			return false;
300
		}
301
302
		/** @psalm-suppress NoInterfaceProperties */
303
		$server = $this->request->server;
304
		if (!isset($server['PHP_AUTH_USER']) || $server['PHP_AUTH_USER'] !== 'system') {
305
			return false;
306
		}
307
308
		/** @psalm-suppress NoInterfaceProperties */
309
		$sharedSecret = $server['PHP_AUTH_PW'] ?? null;
310
		if ($sharedSecret === null) {
311
			return false;
312
		}
313
314
		$servers = $this->trustedServers->getServers();
315
		$trusted = array_filter($servers, function ($trustedServer) use ($sharedSecret) {
0 ignored issues
show
Bug introduced by
$servers of type OCA\Federation\list is incompatible with the type array expected by parameter $array of array_filter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

315
		$trusted = array_filter(/** @scrutinizer ignore-type */ $servers, function ($trustedServer) use ($sharedSecret) {
Loading history...
316
			return $trustedServer['shared_secret'] === $sharedSecret;
317
		});
318
		// Authentication is fine, but it's not for a federated share
319
		if (empty($trusted)) {
320
			return false;
321
		}
322
323
		return true;
324
	}
325
326
	/**
327
	 * If the validation doesn't work the card is "not found" so we
328
	 * return empty carddata even if the carddata might exist in the local backend.
329
	 * This can happen when a user sets the required properties
330
	 * FN, N to a local scope only but the request is from
331
	 * a federated share.
332
	 *
333
	 * @see https://github.com/nextcloud/server/issues/38042
334
	 *
335
	 * @param array $obj
336
	 * @return string|null
337
	 */
338
	private function extractCarddata(array $obj): ?string {
339
		$obj['acl'] = $this->getChildACL();
340
		$cardData = $obj['carddata'];
341
		/** @var VCard $vCard */
342
		$vCard = Reader::read($cardData);
343
		foreach ($vCard->children() as $child) {
344
			$scope = $child->offsetGet('X-NC-SCOPE');
345
			if ($scope !== null && $scope->getValue() === IAccountManager::SCOPE_LOCAL) {
346
				$vCard->remove($child);
347
			}
348
		}
349
		$messages = $vCard->validate();
350
		if (!empty($messages)) {
351
			return null;
352
		}
353
354
		return $vCard->serialize();
355
	}
356
357
	/**
358
	 * @return mixed
359
	 * @throws Forbidden
360
	 */
361
	public function delete() {
362
		if ($this->isFederation()) {
363
			parent::delete();
364
		}
365
		throw new Forbidden();
366
	}
367
368
	public function getACL() {
369
		return array_filter(parent::getACL(), function ($acl) {
370
			if (in_array($acl['privilege'], ['{DAV:}write', '{DAV:}all'], true)) {
371
				return false;
372
			}
373
			return true;
374
		});
375
	}
376
}
377