Passed
Push — master ( b85c2d...d5c2e7 )
by Christoph
15:18 queued 13s
created

SystemAddressbook::getChanges()   B

Complexity

Conditions 10
Paths 20

Size

Total Lines 45
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 30
nc 20
nop 3
dl 0
loc 45
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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