Passed
Push — master ( 978047...cd5bd1 )
by Blizzz
16:35 queued 13s
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 ($user->getBackendClassName() === 'Guests' || !$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
		$user = $this->userSession->getUser();
139
		if (($user !== null && $user->getBackendClassName() === 'Guests') || !$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
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
			if ($this->groupManager === null || $user === null) {
153
				// Group manager or user is not available, so we can't determine which data is safe
154
				return [];
155
			}
156
			$groups = $this->groupManager->getUserGroups($user);
157
			$allowedNames = [];
158
			foreach ($groups as $group) {
159
				$users = $group->getUsers();
160
				foreach ($users as $groupUser) {
161
					if ($groupUser->getBackendClassName() === 'Guests') {
162
						continue;
163
					}
164
					$allowedNames[] = SyncService::getCardUri($groupUser);
165
				}
166
			}
167
			return parent::getMultipleChildren(array_intersect($paths, $allowedNames));
168
		}
169
		if (!$this->isFederation()) {
170
			return parent::getMultipleChildren($paths);
171
		}
172
173
		$objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
174
		$children = [];
175
		/** @var array $obj */
176
		foreach ($objs as $obj) {
177
			if (empty($obj)) {
178
				continue;
179
			}
180
			$carddata = $this->extractCarddata($obj);
181
			if (empty($carddata)) {
182
				continue;
183
			} else {
184
				$obj['carddata'] = $carddata;
185
			}
186
			$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
187
		}
188
		return $children;
189
	}
190
191
	/**
192
	 * @param string $name
193
	 * @return Card
194
	 * @throws NotFound
195
	 * @throws Forbidden
196
	 */
197
	public function getChild($name): Card {
198
		$user = $this->userSession->getUser();
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 (($user !== null && $user->getBackendClassName() === 'Guests') || !$shareEnumeration || (!$shareEnumerationGroup && $shareEnumerationPhone)) {
203
			$ownName = $user !== null ? SyncService::getCardUri($user) : null;
204
			if ($ownName === $name) {
205
				return parent::getChild($name);
206
			}
207
			throw new Forbidden();
208
		}
209
		if ($shareEnumerationGroup) {
210
			if ($user === null || $this->groupManager === null) {
211
				// Group manager is not available, so we can't determine which data is safe
212
				throw new Forbidden();
213
			}
214
			$groups = $this->groupManager->getUserGroups($user);
215
			foreach ($groups as $group) {
216
				foreach ($group->getUsers() as $groupUser) {
217
					if ($groupUser->getBackendClassName() === 'Guests') {
218
						continue;
219
					}
220
					$otherName = SyncService::getCardUri($groupUser);
221
					if ($otherName === $name) {
222
						return parent::getChild($name);
223
					}
224
				}
225
			}
226
			throw new Forbidden();
227
		}
228
		if (!$this->isFederation()) {
229
			return parent::getChild($name);
230
		}
231
232
		$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
233
		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...
234
			throw new NotFound('Card not found');
235
		}
236
		$carddata = $this->extractCarddata($obj);
237
		if (empty($carddata)) {
238
			throw new Forbidden();
239
		} else {
240
			$obj['carddata'] = $carddata;
241
		}
242
		return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
243
	}
244
245
	/**
246
	 * @throws UnsupportedLimitOnInitialSyncException
247
	 */
248
	public function getChanges($syncToken, $syncLevel, $limit = null) {
249
		if (!$syncToken && $limit) {
250
			throw new UnsupportedLimitOnInitialSyncException();
251
		}
252
253
		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...
254
			return null;
255
		}
256
257
		if (!$this->isFederation()) {
258
			return parent::getChanges($syncToken, $syncLevel, $limit);
259
		}
260
261
		$changed = $this->carddavBackend->getChangesForAddressBook(
262
			$this->addressBookInfo['id'],
263
			$syncToken,
264
			$syncLevel,
265
			$limit
266
		);
267
268
		if (empty($changed)) {
269
			return $changed;
270
		}
271
272
		$added = $modified = $deleted = [];
273
		foreach ($changed['added'] as $uri) {
274
			try {
275
				$this->getChild($uri);
276
				$added[] = $uri;
277
			} catch (NotFound | Forbidden $e) {
278
				$deleted[] = $uri;
279
			}
280
		}
281
		foreach ($changed['modified'] as $uri) {
282
			try {
283
				$this->getChild($uri);
284
				$modified[] = $uri;
285
			} catch (NotFound | Forbidden $e) {
286
				$deleted[] = $uri;
287
			}
288
		}
289
		$changed['added'] = $added;
290
		$changed['modified'] = $modified;
291
		$changed['deleted'] = $deleted;
292
		return $changed;
293
	}
294
295
	private function isFederation(): bool {
296
		if ($this->trustedServers === null || $this->request === null) {
297
			return false;
298
		}
299
300
		/** @psalm-suppress NoInterfaceProperties */
301
		$server = $this->request->server;
302
		if (!isset($server['PHP_AUTH_USER']) || $server['PHP_AUTH_USER'] !== 'system') {
303
			return false;
304
		}
305
306
		/** @psalm-suppress NoInterfaceProperties */
307
		$sharedSecret = $server['PHP_AUTH_PW'] ?? null;
308
		if ($sharedSecret === null) {
309
			return false;
310
		}
311
312
		$servers = $this->trustedServers->getServers();
313
		$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

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