Passed
Push — master ( b85c2d...d5c2e7 )
by Christoph
15:18 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\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_unique;
49
50
class SystemAddressbook extends AddressBook {
51
	public const URI_SHARED = 'z-server-generated--system';
52
	/** @var IConfig */
53
	private $config;
54
	private IUserSession $userSession;
55
	private ?TrustedServers $trustedServers;
56
	private ?IRequest $request;
57
	private ?IGroupManager $groupManager;
58
59
	public function __construct(BackendInterface $carddavBackend,
60
		array $addressBookInfo,
61
		IL10N $l10n,
62
		IConfig $config,
63
		IUserSession $userSession,
64
		?IRequest $request = null,
65
		?TrustedServers $trustedServers = null,
66
		?IGroupManager $groupManager) {
67
		parent::__construct($carddavBackend, $addressBookInfo, $l10n);
68
		$this->config = $config;
69
		$this->userSession = $userSession;
70
		$this->request = $request;
71
		$this->trustedServers = $trustedServers;
72
		$this->groupManager = $groupManager;
73
74
		$this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Accounts');
75
		$this->addressBookInfo['{' . Plugin::NS_CARDDAV . '}addressbook-description'] = $l10n->t('System address book which holds all accounts');
76
	}
77
78
	/**
79
	 * No checkbox checked -> Show only the same user
80
	 * 'Allow username autocompletion in share dialog' -> show everyone
81
	 * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' -> show only users in intersecting groups
82
	 * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users based on phone number integration' -> show only the same user
83
	 * '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
84
	 */
85
	public function getChildren() {
86
		$shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
87
		$shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
88
		$shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
89
		$user = $this->userSession->getUser();
90
		if (!$user) {
91
			// Should never happen because we don't allow anonymous access
92
			return [];
93
		}
94
		if (!$shareEnumeration || !$shareEnumerationGroup && $shareEnumerationPhone) {
95
			$name = SyncService::getCardUri($user);
96
			try {
97
				return [parent::getChild($name)];
98
			} catch (NotFound $e) {
99
				return [];
100
			}
101
		}
102
		if ($shareEnumerationGroup) {
103
			if ($this->groupManager === null) {
104
				// Group manager is not available, so we can't determine which data is safe
105
				return [];
106
			}
107
			$groups = $this->groupManager->getUserGroups($user);
108
			$names = [];
109
			foreach ($groups as $group) {
110
				$users = $group->getUsers();
111
				foreach ($users as $groupUser) {
112
					if ($groupUser->getBackendClassName() === 'Guests') {
113
						continue;
114
					}
115
					$names[] = SyncService::getCardUri($groupUser);
116
				}
117
			}
118
			return parent::getMultipleChildren(array_unique($names));
119
		}
120
121
		$children = parent::getChildren();
122
		return array_filter($children, function (Card $child) {
123
			// check only for URIs that begin with Guests:
124
			return strpos($child->getName(), 'Guests:') !== 0;
125
		});
126
	}
127
128
	/**
129
	 * @param array $paths
130
	 * @return Card[]
131
	 * @throws NotFound
132
	 */
133
	public function getMultipleChildren($paths): array {
134
		if (!$this->isFederation()) {
135
			return parent::getMultipleChildren($paths);
136
		}
137
138
		$objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
139
		$children = [];
140
		/** @var array $obj */
141
		foreach ($objs as $obj) {
142
			if (empty($obj)) {
143
				continue;
144
			}
145
			$carddata = $this->extractCarddata($obj);
146
			if (empty($carddata)) {
147
				continue;
148
			} else {
149
				$obj['carddata'] = $carddata;
150
			}
151
			$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
152
		}
153
		return $children;
154
	}
155
156
	/**
157
	 * @param string $name
158
	 * @return Card
159
	 * @throws NotFound
160
	 * @throws Forbidden
161
	 */
162
	public function getChild($name): Card {
163
		if (!$this->isFederation()) {
164
			return parent::getChild($name);
165
		}
166
167
		$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
168
		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...
169
			throw new NotFound('Card not found');
170
		}
171
		$carddata = $this->extractCarddata($obj);
172
		if (empty($carddata)) {
173
			throw new Forbidden();
174
		} else {
175
			$obj['carddata'] = $carddata;
176
		}
177
		return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
178
	}
179
180
	/**
181
	 * @throws UnsupportedLimitOnInitialSyncException
182
	 */
183
	public function getChanges($syncToken, $syncLevel, $limit = null) {
184
		if (!$syncToken && $limit) {
185
			throw new UnsupportedLimitOnInitialSyncException();
186
		}
187
188
		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...
189
			return null;
190
		}
191
192
		if (!$this->isFederation()) {
193
			return parent::getChanges($syncToken, $syncLevel, $limit);
194
		}
195
196
		$changed = $this->carddavBackend->getChangesForAddressBook(
197
			$this->addressBookInfo['id'],
198
			$syncToken,
199
			$syncLevel,
200
			$limit
201
		);
202
203
		if (empty($changed)) {
204
			return $changed;
205
		}
206
207
		$added = $modified = $deleted = [];
208
		foreach ($changed['added'] as $uri) {
209
			try {
210
				$this->getChild($uri);
211
				$added[] = $uri;
212
			} catch (NotFound | Forbidden $e) {
213
				$deleted[] = $uri;
214
			}
215
		}
216
		foreach ($changed['modified'] as $uri) {
217
			try {
218
				$this->getChild($uri);
219
				$modified[] = $uri;
220
			} catch (NotFound | Forbidden $e) {
221
				$deleted[] = $uri;
222
			}
223
		}
224
		$changed['added'] = $added;
225
		$changed['modified'] = $modified;
226
		$changed['deleted'] = $deleted;
227
		return $changed;
228
	}
229
230
	private function isFederation(): bool {
231
		if ($this->trustedServers === null || $this->request === null) {
232
			return false;
233
		}
234
235
		/** @psalm-suppress NoInterfaceProperties */
236
		if ($this->request->server['PHP_AUTH_USER'] !== 'system') {
237
			return false;
238
		}
239
240
		/** @psalm-suppress NoInterfaceProperties */
241
		$sharedSecret = $this->request->server['PHP_AUTH_PW'];
242
		if ($sharedSecret === null) {
0 ignored issues
show
introduced by
The condition $sharedSecret === null is always false.
Loading history...
243
			return false;
244
		}
245
246
		$servers = $this->trustedServers->getServers();
247
		$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

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