Passed
Push — master ( 465e91...f452e2 )
by Roeland
12:18 queued 10s
created

Principal   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 179
dl 0
loc 432
rs 3.2
c 1
b 0
f 0
wmc 65

12 Methods

Rating   Name   Duplication   Size   Complexity  
A searchPrincipals() 0 11 3
A getGroupMembership() 0 27 6
A getPrincipalPrefix() 0 2 1
A updatePrincipal() 0 2 1
A circleToPrincipal() 0 23 6
B getPrincipalByPath() 0 32 9
C searchUserPrincipals() 0 86 15
A getCircleMembership() 0 23 6
A getPrincipalsByPrefix() 0 10 3
A userToPrincipal() 0 15 3
B findByUri() 0 46 11
A __construct() 0 15 1

How to fix   Complexity   

Complex Class

Complex classes like Principal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Principal, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2018, Georg Ehrke
5
 *
6
 * @author Bart Visscher <[email protected]>
7
 * @author Jakob Sack <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Thomas Tanghus <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 * @author Georg Ehrke <[email protected]>
16
 * @author Vinicius Cubas Brand <[email protected]>
17
 * @author Daniel Tygel <[email protected]>
18
 *
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
35
namespace OCA\DAV\Connector\Sabre;
36
37
use OCA\Circles\Exceptions\CircleDoesNotExistException;
0 ignored issues
show
Bug introduced by
The type OCA\Circles\Exceptions\CircleDoesNotExistException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
use OCA\DAV\CalDAV\Proxy\Proxy;
39
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
40
use OCA\DAV\Traits\PrincipalProxyTrait;
41
use OCP\App\IAppManager;
42
use OCP\AppFramework\QueryException;
43
use OCP\IGroup;
44
use OCP\IGroupManager;
45
use OCP\IUser;
46
use OCP\IUserManager;
47
use OCP\IUserSession;
48
use OCP\Share\IManager as IShareManager;
49
use Sabre\DAV\Exception;
50
use Sabre\DAV\PropPatch;
51
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
52
53
class Principal implements BackendInterface {
54
55
	/** @var IUserManager */
56
	private $userManager;
57
58
	/** @var IGroupManager */
59
	private $groupManager;
60
61
	/** @var IShareManager */
62
	private $shareManager;
63
64
	/** @var IUserSession */
65
	private $userSession;
66
67
	/** @var IAppManager */
68
	private $appManager;
69
70
	/** @var string */
71
	private $principalPrefix;
72
73
	/** @var bool */
74
	private $hasGroups;
75
76
	/** @var bool */
77
	private $hasCircles;
78
79
	/** @var ProxyMapper */
80
	private $proxyMapper;
81
82
	/**
83
	 * Principal constructor.
84
	 *
85
	 * @param IUserManager $userManager
86
	 * @param IGroupManager $groupManager
87
	 * @param IShareManager $shareManager
88
	 * @param IUserSession $userSession
89
	 * @param IAppManager $appManager
90
	 * @param ProxyMapper $proxyMapper
91
	 * @param string $principalPrefix
92
	 */
93
	public function __construct(IUserManager $userManager,
94
								IGroupManager $groupManager,
95
								IShareManager $shareManager,
96
								IUserSession $userSession,
97
								IAppManager $appManager,
98
								ProxyMapper $proxyMapper,
99
								string $principalPrefix = 'principals/users/') {
100
		$this->userManager = $userManager;
101
		$this->groupManager = $groupManager;
102
		$this->shareManager = $shareManager;
103
		$this->userSession = $userSession;
104
		$this->appManager = $appManager;
105
		$this->principalPrefix = trim($principalPrefix, '/');
106
		$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
107
		$this->proxyMapper = $proxyMapper;
108
	}
109
110
	use PrincipalProxyTrait {
111
		getGroupMembership as protected traitGetGroupMembership;
112
	}
113
114
	/**
115
	 * Returns a list of principals based on a prefix.
116
	 *
117
	 * This prefix will often contain something like 'principals'. You are only
118
	 * expected to return principals that are in this base path.
119
	 *
120
	 * You are expected to return at least a 'uri' for every user, you can
121
	 * return any additional properties if you wish so. Common properties are:
122
	 *   {DAV:}displayname
123
	 *
124
	 * @param string $prefixPath
125
	 * @return string[]
126
	 */
127
	public function getPrincipalsByPrefix($prefixPath) {
128
		$principals = [];
129
130
		if ($prefixPath === $this->principalPrefix) {
131
			foreach($this->userManager->search('') as $user) {
132
				$principals[] = $this->userToPrincipal($user);
133
			}
134
		}
135
136
		return $principals;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $principals returns an array which contains values of type array<string,string> which are incompatible with the documented value type string.
Loading history...
137
	}
138
139
	/**
140
	 * Returns a specific principal, specified by it's path.
141
	 * The returned structure should be the exact same as from
142
	 * getPrincipalsByPrefix.
143
	 *
144
	 * @param string $path
145
	 * @return array
146
	 */
147
	public function getPrincipalByPath($path) {
148
		list($prefix, $name) = \Sabre\Uri\split($path);
149
150
		if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
151
			list($prefix2, $name2) = \Sabre\Uri\split($prefix);
152
153
			if ($prefix2 === $this->principalPrefix) {
154
				$user = $this->userManager->get($name2);
155
156
				if ($user !== null) {
157
					return [
158
						'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
159
					];
160
				}
161
				return null;
162
			}
163
		}
164
165
		if ($prefix === $this->principalPrefix) {
166
			$user = $this->userManager->get($name);
167
168
			if ($user !== null) {
169
				return $this->userToPrincipal($user);
170
			}
171
		} else if ($prefix === 'principals/circles') {
172
			try {
173
				return $this->circleToPrincipal($name);
174
			} catch (QueryException $e) {
175
				return null;
176
			}
177
		}
178
		return null;
179
	}
180
181
	/**
182
	 * Returns the list of groups a principal is a member of
183
	 *
184
	 * @param string $principal
185
	 * @param bool $needGroups
186
	 * @return array
187
	 * @throws Exception
188
	 */
189
	public function getGroupMembership($principal, $needGroups = false) {
190
		list($prefix, $name) = \Sabre\Uri\split($principal);
191
192
		if ($prefix !== $this->principalPrefix) {
193
			return [];
194
		}
195
196
		$user = $this->userManager->get($name);
197
		if (!$user) {
198
			throw new Exception('Principal not found');
199
		}
200
201
		$groups = [];
202
203
		if ($this->hasGroups || $needGroups) {
204
			$userGroups = $this->groupManager->getUserGroups($user);
205
			foreach($userGroups as $userGroup) {
206
				$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
207
			}
208
		}
209
210
		$groups = array_unique(array_merge(
211
			$groups,
212
			$this->traitGetGroupMembership($principal, $needGroups)
213
		));
214
215
		return $groups;
216
	}
217
218
	/**
219
	 * @param string $path
220
	 * @param PropPatch $propPatch
221
	 * @return int
222
	 */
223
	function updatePrincipal($path, PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
224
		return 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return 0 returns the type integer which is incompatible with the return type mandated by Sabre\DAVACL\PrincipalBa...face::updatePrincipal() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
225
	}
226
227
	/**
228
	 * Search user principals
229
	 *
230
	 * @param array $searchProperties
231
	 * @param string $test
232
	 * @return array
233
	 */
234
	protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
235
		$results = [];
236
237
		// If sharing is disabled, return the empty array
238
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
239
		if (!$shareAPIEnabled) {
240
			return [];
241
		}
242
243
		// If sharing is restricted to group members only,
244
		// return only members that have groups in common
245
		$restrictGroups = false;
246
		if ($this->shareManager->shareWithGroupMembersOnly()) {
247
			$user = $this->userSession->getUser();
248
			if (!$user) {
249
				return [];
250
			}
251
252
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
253
		}
254
255
		foreach ($searchProperties as $prop => $value) {
256
			switch ($prop) {
257
				case '{http://sabredav.org/ns}email-address':
258
					$users = $this->userManager->getByEmail($value);
259
260
					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
261
						// is sharing restricted to groups only?
262
						if ($restrictGroups !== false) {
263
							$userGroups = $this->groupManager->getUserGroupIds($user);
264
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
265
								return $carry;
266
							}
267
						}
268
269
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
270
						return $carry;
271
					}, []);
272
					break;
273
274
				case '{DAV:}displayname':
275
					$users = $this->userManager->searchDisplayName($value);
276
277
					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
278
						// is sharing restricted to groups only?
279
						if ($restrictGroups !== false) {
280
							$userGroups = $this->groupManager->getUserGroupIds($user);
281
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
282
								return $carry;
283
							}
284
						}
285
286
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
287
						return $carry;
288
					}, []);
289
					break;
290
291
				case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
292
					// If you add support for more search properties that qualify as a user-address,
293
					// please also add them to the array below
294
					$results[] = $this->searchUserPrincipals([
295
						// In theory this should also search for principal:principals/users/...
296
						// but that's used internally only anyway and i don't know of any client querying that
297
						'{http://sabredav.org/ns}email-address' => $value,
298
					], 'anyof');
299
					break;
300
301
				default:
302
					$results[] = [];
303
					break;
304
			}
305
		}
306
307
		// results is an array of arrays, so this is not the first search result
308
		// but the results of the first searchProperty
309
		if (count($results) === 1) {
310
			return $results[0];
311
		}
312
313
		switch ($test) {
314
			case 'anyof':
315
				return array_values(array_unique(array_merge(...$results)));
316
317
			case 'allof':
318
			default:
319
				return array_values(array_intersect(...$results));
0 ignored issues
show
Bug introduced by
The call to array_intersect() has too few arguments starting with array2. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

319
				return array_values(/** @scrutinizer ignore-call */ array_intersect(...$results));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
320
		}
321
	}
322
323
	/**
324
	 * @param string $prefixPath
325
	 * @param array $searchProperties
326
	 * @param string $test
327
	 * @return array
328
	 */
329
	function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
330
		if (count($searchProperties) === 0) {
331
			return [];
332
		}
333
334
		switch ($prefixPath) {
335
			case 'principals/users':
336
				return $this->searchUserPrincipals($searchProperties, $test);
337
338
			default:
339
				return [];
340
		}
341
	}
342
343
	/**
344
	 * @param string $uri
345
	 * @param string $principalPrefix
346
	 * @return string
347
	 */
348
	function findByUri($uri, $principalPrefix) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
349
		// If sharing is disabled, return the empty array
350
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
351
		if (!$shareAPIEnabled) {
352
			return null;
353
		}
354
355
		// If sharing is restricted to group members only,
356
		// return only members that have groups in common
357
		$restrictGroups = false;
358
		if ($this->shareManager->shareWithGroupMembersOnly()) {
359
			$user = $this->userSession->getUser();
360
			if (!$user) {
361
				return null;
362
			}
363
364
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
365
		}
366
367
		if (strpos($uri, 'mailto:') === 0) {
368
			if ($principalPrefix === 'principals/users') {
369
				$users = $this->userManager->getByEmail(substr($uri, 7));
370
				if (count($users) !== 1) {
371
					return null;
372
				}
373
				$user = $users[0];
374
375
				if ($restrictGroups !== false) {
376
					$userGroups = $this->groupManager->getUserGroupIds($user);
377
					if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
378
						return null;
379
					}
380
				}
381
382
				return $this->principalPrefix . '/' . $user->getUID();
383
			}
384
		}
385
		if (substr($uri, 0, 10) === 'principal:') {
386
			$principal = substr($uri, 10);
387
			$principal = $this->getPrincipalByPath($principal);
388
			if ($principal !== null) {
0 ignored issues
show
introduced by
The condition $principal !== null is always true.
Loading history...
389
				return $principal['uri'];
390
			}
391
		}
392
393
		return null;
394
	}
395
396
	/**
397
	 * @param IUser $user
398
	 * @return array
399
	 */
400
	protected function userToPrincipal($user) {
401
		$userId = $user->getUID();
402
		$displayName = $user->getDisplayName();
403
		$principal = [
404
				'uri' => $this->principalPrefix . '/' . $userId,
405
				'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
0 ignored issues
show
introduced by
The condition is_null($displayName) is always false.
Loading history...
406
				'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
407
		];
408
409
		$email = $user->getEMailAddress();
410
		if (!empty($email)) {
411
			$principal['{http://sabredav.org/ns}email-address'] = $email;
412
		}
413
414
		return $principal;
415
	}
416
417
	public function getPrincipalPrefix() {
418
		return $this->principalPrefix;
419
	}
420
421
	/**
422
	 * @param string $circleUniqueId
423
	 * @return array|null
424
	 * @throws \OCP\AppFramework\QueryException
425
	 * @suppress PhanUndeclaredClassMethod
426
	 * @suppress PhanUndeclaredClassCatch
427
	 */
428
	protected function circleToPrincipal($circleUniqueId) {
429
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
430
			return null;
431
		}
432
433
		try {
434
			$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
0 ignored issues
show
Bug introduced by
The type OCA\Circles\Api\v1\Circles was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
435
		} catch(QueryException $ex) {
436
			return null;
437
		} catch(CircleDoesNotExistException $ex) {
438
			return null;
439
		}
440
441
		if (!$circle) {
442
			return null;
443
		}
444
445
		$principal = [
446
			'uri' => 'principals/circles/' . $circleUniqueId,
447
			'{DAV:}displayname' => $circle->getName(),
448
		];
449
450
		return $principal;
451
	}
452
453
	/**
454
	 * Returns the list of circles a principal is a member of
455
	 *
456
	 * @param string $principal
457
	 * @return array
458
	 * @throws Exception
459
	 * @throws \OCP\AppFramework\QueryException
460
	 * @suppress PhanUndeclaredClassMethod
461
	 */
462
	public function getCircleMembership($principal):array {
463
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
464
			return [];
465
		}
466
467
		list($prefix, $name) = \Sabre\Uri\split($principal);
468
		if ($this->hasCircles && $prefix === $this->principalPrefix) {
469
			$user = $this->userManager->get($name);
470
			if (!$user) {
471
				throw new Exception('Principal not found');
472
			}
473
474
			$circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
475
476
			$circles = array_map(function($circle) {
477
				/** @var \OCA\Circles\Model\Circle $circle */
478
				return 'principals/circles/' . urlencode($circle->getUniqueId());
479
			}, $circles);
480
481
			return $circles;
482
		}
483
484
		return [];
485
	}
486
}
487