Passed
Push — master ( 24690a...b9026a )
by Christoph
20:57 queued 17s
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\IUser;
39
use OCP\IUserSession;
40
use Sabre\CardDAV\Backend\SyncSupport;
41
use Sabre\CardDAV\Backend\BackendInterface;
42
use Sabre\CardDAV\Card;
43
use Sabre\DAV\Exception\Forbidden;
44
use Sabre\DAV\Exception\NotFound;
45
use Sabre\DAV\ICollection;
46
use Sabre\VObject\Component\VCard;
47
use Sabre\VObject\Reader;
48
use function array_filter;
49
use function array_unique;
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
		if (!$this->isFederation()) {
136
			return parent::getMultipleChildren($paths);
137
		}
138
139
		$objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
140
		$children = [];
141
		/** @var array $obj */
142
		foreach ($objs as $obj) {
143
			if (empty($obj)) {
144
				continue;
145
			}
146
			$carddata = $this->extractCarddata($obj);
147
			if (empty($carddata)) {
148
				continue;
149
			} else {
150
				$obj['carddata'] = $carddata;
151
			}
152
			$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
153
		}
154
		return $children;
155
	}
156
157
	/**
158
	 * @param string $name
159
	 * @return Card
160
	 * @throws NotFound
161
	 * @throws Forbidden
162
	 */
163
	public function getChild($name): Card {
164
		if (!$this->isFederation()) {
165
			return parent::getChild($name);
166
		}
167
168
		$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
169
		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...
170
			throw new NotFound('Card not found');
171
		}
172
		$carddata = $this->extractCarddata($obj);
173
		if (empty($carddata)) {
174
			throw new Forbidden();
175
		} else {
176
			$obj['carddata'] = $carddata;
177
		}
178
		return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
179
	}
180
181
	/**
182
	 * @throws UnsupportedLimitOnInitialSyncException
183
	 */
184
	public function getChanges($syncToken, $syncLevel, $limit = null) {
185
		if (!$syncToken && $limit) {
186
			throw new UnsupportedLimitOnInitialSyncException();
187
		}
188
189
		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...
190
			return null;
191
		}
192
193
		if (!$this->isFederation()) {
194
			return parent::getChanges($syncToken, $syncLevel, $limit);
195
		}
196
197
		$changed = $this->carddavBackend->getChangesForAddressBook(
198
			$this->addressBookInfo['id'],
199
			$syncToken,
200
			$syncLevel,
201
			$limit
202
		);
203
204
		if (empty($changed)) {
205
			return $changed;
206
		}
207
208
		$added = $modified = $deleted = [];
209
		foreach ($changed['added'] as $uri) {
210
			try {
211
				$this->getChild($uri);
212
				$added[] = $uri;
213
			} catch (NotFound | Forbidden $e) {
214
				$deleted[] = $uri;
215
			}
216
		}
217
		foreach ($changed['modified'] as $uri) {
218
			try {
219
				$this->getChild($uri);
220
				$modified[] = $uri;
221
			} catch (NotFound | Forbidden $e) {
222
				$deleted[] = $uri;
223
			}
224
		}
225
		$changed['added'] = $added;
226
		$changed['modified'] = $modified;
227
		$changed['deleted'] = $deleted;
228
		return $changed;
229
	}
230
231
	private function isFederation(): bool {
232
		if ($this->trustedServers === null || $this->request === null) {
233
			return false;
234
		}
235
236
		/** @psalm-suppress NoInterfaceProperties */
237
		if ($this->request->server['PHP_AUTH_USER'] !== 'system') {
238
			return false;
239
		}
240
241
		/** @psalm-suppress NoInterfaceProperties */
242
		$sharedSecret = $this->request->server['PHP_AUTH_PW'];
243
		if ($sharedSecret === null) {
0 ignored issues
show
introduced by
The condition $sharedSecret === null is always false.
Loading history...
244
			return false;
245
		}
246
247
		$servers = $this->trustedServers->getServers();
248
		$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

248
		$trusted = array_filter(/** @scrutinizer ignore-type */ $servers, function ($trustedServer) use ($sharedSecret) {
Loading history...
249
			return $trustedServer['shared_secret'] === $sharedSecret;
250
		});
251
		// Authentication is fine, but it's not for a federated share
252
		if (empty($trusted)) {
253
			return false;
254
		}
255
256
		return true;
257
	}
258
259
	/**
260
	 * If the validation doesn't work the card is "not found" so we
261
	 * return empty carddata even if the carddata might exist in the local backend.
262
	 * This can happen when a user sets the required properties
263
	 * FN, N to a local scope only but the request is from
264
	 * a federated share.
265
	 *
266
	 * @see https://github.com/nextcloud/server/issues/38042
267
	 *
268
	 * @param array $obj
269
	 * @return string|null
270
	 */
271
	private function extractCarddata(array $obj): ?string {
272
		$obj['acl'] = $this->getChildACL();
273
		$cardData = $obj['carddata'];
274
		/** @var VCard $vCard */
275
		$vCard = Reader::read($cardData);
276
		foreach ($vCard->children() as $child) {
277
			$scope = $child->offsetGet('X-NC-SCOPE');
278
			if ($scope !== null && $scope->getValue() === IAccountManager::SCOPE_LOCAL) {
279
				$vCard->remove($child);
280
			}
281
		}
282
		$messages = $vCard->validate();
283
		if (!empty($messages)) {
284
			return null;
285
		}
286
287
		return $vCard->serialize();
288
	}
289
290
	/**
291
	 * @return mixed
292
	 * @throws Forbidden
293
	 */
294
	public function delete() {
295
		if ($this->isFederation()) {
296
			parent::delete();
297
		}
298
		throw new Forbidden();
299
	}
300
301
	public function getACL() {
302
		return array_filter(parent::getACL(), function($acl) {
303
			if (in_array($acl['privilege'], ['{DAV:}write', '{DAV:}all'], true)) {
304
				return false;
305
			}
306
			return true;
307
		});
308
	}
309
}
310