Passed
Push — master ( 5993a4...1fd8f4 )
by Christoph
17:32 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
 *
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License
25
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
namespace OCA\DAV\CardDAV;
29
30
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
31
use OCA\Federation\TrustedServers;
32
use OCP\Accounts\IAccountManager;
33
use OCP\IConfig;
34
use OCP\IL10N;
35
use OCP\IRequest;
36
use Sabre\CardDAV\Backend\SyncSupport;
37
use Sabre\CardDAV\Backend\BackendInterface;
38
use Sabre\CardDAV\Card;
39
use Sabre\DAV\Exception\Forbidden;
40
use Sabre\DAV\Exception\NotFound;
41
use Sabre\VObject\Component\VCard;
42
use Sabre\VObject\Reader;
43
44
class SystemAddressbook extends AddressBook {
45
	/** @var IConfig */
46
	private $config;
47
	private ?TrustedServers $trustedServers;
48
	private ?IRequest $request;
49
50
	public function __construct(BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n, IConfig $config, ?IRequest $request = null, ?TrustedServers $trustedServers = null) {
51
		parent::__construct($carddavBackend, $addressBookInfo, $l10n);
52
		$this->config = $config;
53
		$this->request = $request;
54
		$this->trustedServers = $trustedServers;
55
	}
56
57
	public function getChildren(): array {
58
		$shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
59
		$shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
60
		$shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
61
		if (!$shareEnumeration || $shareEnumerationGroup || $shareEnumerationPhone) {
62
			return [];
63
		}
64
65
		return parent::getChildren();
66
	}
67
68
	/**
69
	 * @param array $paths
70
	 * @return Card[]
71
	 * @throws NotFound
72
	 */
73
	public function getMultipleChildren($paths): array {
74
		if (!$this->isFederation()) {
75
			return parent::getMultipleChildren($paths);
76
		}
77
78
		$objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
79
		$children = [];
80
		/** @var array $obj */
81
		foreach ($objs as $obj) {
82
			if (empty($obj)) {
83
				continue;
84
			}
85
			$carddata = $this->extractCarddata($obj);
86
			if (empty($carddata)) {
87
				continue;
88
			} else {
89
				$obj['carddata'] = $carddata;
90
			}
91
			$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
92
		}
93
		return $children;
94
	}
95
96
	/**
97
	 * @param string $name
98
	 * @return Card
99
	 * @throws NotFound
100
	 * @throws Forbidden
101
	 */
102
	public function getChild($name): Card {
103
		if (!$this->isFederation()) {
104
			return parent::getChild($name);
105
		}
106
107
		$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
108
		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...
109
			throw new NotFound('Card not found');
110
		}
111
		$carddata = $this->extractCarddata($obj);
112
		if (empty($carddata)) {
113
			throw new Forbidden();
114
		} else {
115
			$obj['carddata'] = $carddata;
116
		}
117
		return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
118
	}
119
120
	/**
121
	 * @throws UnsupportedLimitOnInitialSyncException
122
	 */
123
	public function getChanges($syncToken, $syncLevel, $limit = null) {
124
		if (!$syncToken && $limit) {
125
			throw new UnsupportedLimitOnInitialSyncException();
126
		}
127
128
		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...
129
			return null;
130
		}
131
132
		if (!$this->isFederation()) {
133
			return parent::getChanges($syncToken, $syncLevel, $limit);
134
		}
135
136
		$changed = $this->carddavBackend->getChangesForAddressBook(
137
			$this->addressBookInfo['id'],
138
			$syncToken,
139
			$syncLevel,
140
			$limit
141
		);
142
143
		if (empty($changed)) {
144
			return $changed;
145
		}
146
147
		$added = $modified = $deleted = [];
148
		foreach ($changed['added'] as $uri) {
149
			try {
150
				$this->getChild($uri);
151
				$added[] = $uri;
152
			} catch (NotFound | Forbidden $e) {
153
				$deleted[] = $uri;
154
			}
155
		}
156
		foreach ($changed['modified'] as $uri) {
157
			try {
158
				$this->getChild($uri);
159
				$modified[] = $uri;
160
			} catch (NotFound | Forbidden $e) {
161
				$deleted[] = $uri;
162
			}
163
		}
164
		$changed['added'] = $added;
165
		$changed['modified'] = $modified;
166
		$changed['deleted'] = $deleted;
167
		return $changed;
168
	}
169
170
	private function isFederation(): bool {
171
		if ($this->trustedServers === null || $this->request === null) {
172
			return false;
173
		}
174
175
		/** @psalm-suppress NoInterfaceProperties */
176
		if ($this->request->server['PHP_AUTH_USER'] !== 'system') {
177
			return false;
178
		}
179
180
		/** @psalm-suppress NoInterfaceProperties */
181
		$sharedSecret = $this->request->server['PHP_AUTH_PW'];
182
		if ($sharedSecret === null) {
0 ignored issues
show
introduced by
The condition $sharedSecret === null is always false.
Loading history...
183
			return false;
184
		}
185
186
		$servers = $this->trustedServers->getServers();
187
		$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

187
		$trusted = array_filter(/** @scrutinizer ignore-type */ $servers, function ($trustedServer) use ($sharedSecret) {
Loading history...
188
			return $trustedServer['shared_secret'] === $sharedSecret;
189
		});
190
		// Authentication is fine, but it's not for a federated share
191
		if (empty($trusted)) {
192
			return false;
193
		}
194
195
		return true;
196
	}
197
198
	/**
199
	 * If the validation doesn't work the card is "not found" so we
200
	 * return empty carddata even if the carddata might exist in the local backend.
201
	 * This can happen when a user sets the required properties
202
	 * FN, N to a local scope only but the request is from
203
	 * a federated share.
204
	 *
205
	 * @see https://github.com/nextcloud/server/issues/38042
206
	 *
207
	 * @param array $obj
208
	 * @return string|null
209
	 */
210
	private function extractCarddata(array $obj): ?string {
211
		$obj['acl'] = $this->getChildACL();
212
		$cardData = $obj['carddata'];
213
		/** @var VCard $vCard */
214
		$vCard = Reader::read($cardData);
215
		foreach ($vCard->children() as $child) {
216
			$scope = $child->offsetGet('X-NC-SCOPE');
217
			if ($scope !== null && $scope->getValue() === IAccountManager::SCOPE_LOCAL) {
218
				$vCard->remove($child);
219
			}
220
		}
221
		$messages = $vCard->validate();
222
		if (!empty($messages)) {
223
			return null;
224
		}
225
226
		return $vCard->serialize();
227
	}
228
}
229