Passed
Push — master ( 581704...2efa00 )
by Roeland
11:12 queued 11s
created

Principal::searchUserPrincipals()   F

Complexity

Conditions 23
Paths 266

Size

Total Lines 127
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 73
nc 266
nop 2
dl 0
loc 127
rs 2.5083
c 0
b 0
f 0

How to fix   Long Method    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
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2018, Georg Ehrke
5
 *
6
 * @author Bart Visscher <[email protected]>
7
 * @author Christoph Seitz <[email protected]>
8
 * @author Georg Ehrke <[email protected]>
9
 * @author Jakob Sack <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 * @author Vinicius Cubas Brand <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OCA\DAV\Connector\Sabre;
34
35
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...
36
use OCA\DAV\CalDAV\Proxy\Proxy;
37
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
38
use OCA\DAV\Traits\PrincipalProxyTrait;
39
use OCP\App\IAppManager;
40
use OCP\AppFramework\QueryException;
41
use OCP\IConfig;
42
use OCP\IGroup;
43
use OCP\IGroupManager;
44
use OCP\IUser;
45
use OCP\IUserManager;
46
use OCP\IUserSession;
47
use OCP\Share\IManager as IShareManager;
48
use Sabre\DAV\Exception;
49
use Sabre\DAV\PropPatch;
50
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
51
52
class Principal implements BackendInterface {
53
54
	/** @var IUserManager */
55
	private $userManager;
56
57
	/** @var IGroupManager */
58
	private $groupManager;
59
60
	/** @var IShareManager */
61
	private $shareManager;
62
63
	/** @var IUserSession */
64
	private $userSession;
65
66
	/** @var IAppManager */
67
	private $appManager;
68
69
	/** @var string */
70
	private $principalPrefix;
71
72
	/** @var bool */
73
	private $hasGroups;
74
75
	/** @var bool */
76
	private $hasCircles;
77
78
	/** @var ProxyMapper */
79
	private $proxyMapper;
80
81
	/** @var IConfig */
82
	private $config;
83
84
	/**
85
	 * Principal constructor.
86
	 *
87
	 * @param IUserManager $userManager
88
	 * @param IGroupManager $groupManager
89
	 * @param IShareManager $shareManager
90
	 * @param IUserSession $userSession
91
	 * @param IAppManager $appManager
92
	 * @param ProxyMapper $proxyMapper
93
	 * @param IConfig $config
94
	 * @param string $principalPrefix
95
	 */
96
	public function __construct(IUserManager $userManager,
97
								IGroupManager $groupManager,
98
								IShareManager $shareManager,
99
								IUserSession $userSession,
100
								IAppManager $appManager,
101
								ProxyMapper $proxyMapper,
102
								IConfig $config,
103
								string $principalPrefix = 'principals/users/') {
104
		$this->userManager = $userManager;
105
		$this->groupManager = $groupManager;
106
		$this->shareManager = $shareManager;
107
		$this->userSession = $userSession;
108
		$this->appManager = $appManager;
109
		$this->principalPrefix = trim($principalPrefix, '/');
110
		$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
111
		$this->proxyMapper = $proxyMapper;
112
		$this->config = $config;
113
	}
114
115
	use PrincipalProxyTrait {
116
		getGroupMembership as protected traitGetGroupMembership;
117
	}
118
119
	/**
120
	 * Returns a list of principals based on a prefix.
121
	 *
122
	 * This prefix will often contain something like 'principals'. You are only
123
	 * expected to return principals that are in this base path.
124
	 *
125
	 * You are expected to return at least a 'uri' for every user, you can
126
	 * return any additional properties if you wish so. Common properties are:
127
	 *   {DAV:}displayname
128
	 *
129
	 * @param string $prefixPath
130
	 * @return string[]
131
	 */
132
	public function getPrincipalsByPrefix($prefixPath) {
133
		$principals = [];
134
135
		if ($prefixPath === $this->principalPrefix) {
136
			foreach($this->userManager->search('') as $user) {
137
				$principals[] = $this->userToPrincipal($user);
138
			}
139
		}
140
141
		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...
142
	}
143
144
	/**
145
	 * Returns a specific principal, specified by it's path.
146
	 * The returned structure should be the exact same as from
147
	 * getPrincipalsByPrefix.
148
	 *
149
	 * @param string $path
150
	 * @return array
151
	 */
152
	public function getPrincipalByPath($path) {
153
		list($prefix, $name) = \Sabre\Uri\split($path);
154
155
		if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
156
			list($prefix2, $name2) = \Sabre\Uri\split($prefix);
157
158
			if ($prefix2 === $this->principalPrefix) {
159
				$user = $this->userManager->get($name2);
160
161
				if ($user !== null) {
162
					return [
163
						'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
164
					];
165
				}
166
				return null;
167
			}
168
		}
169
170
		if ($prefix === $this->principalPrefix) {
171
			$user = $this->userManager->get($name);
172
173
			if ($user !== null) {
174
				return $this->userToPrincipal($user);
175
			}
176
		} else if ($prefix === 'principals/circles') {
177
			try {
178
				return $this->circleToPrincipal($name);
179
			} catch (QueryException $e) {
180
				return null;
181
			}
182
		}
183
		return null;
184
	}
185
186
	/**
187
	 * Returns the list of groups a principal is a member of
188
	 *
189
	 * @param string $principal
190
	 * @param bool $needGroups
191
	 * @return array
192
	 * @throws Exception
193
	 */
194
	public function getGroupMembership($principal, $needGroups = false) {
195
		list($prefix, $name) = \Sabre\Uri\split($principal);
196
197
		if ($prefix !== $this->principalPrefix) {
198
			return [];
199
		}
200
201
		$user = $this->userManager->get($name);
202
		if (!$user) {
203
			throw new Exception('Principal not found');
204
		}
205
206
		$groups = [];
207
208
		if ($this->hasGroups || $needGroups) {
209
			$userGroups = $this->groupManager->getUserGroups($user);
210
			foreach($userGroups as $userGroup) {
211
				$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
212
			}
213
		}
214
215
		$groups = array_unique(array_merge(
216
			$groups,
217
			$this->traitGetGroupMembership($principal, $needGroups)
218
		));
219
220
		return $groups;
221
	}
222
223
	/**
224
	 * @param string $path
225
	 * @param PropPatch $propPatch
226
	 * @return int
227
	 */
228
	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...
229
		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...
230
	}
231
232
	/**
233
	 * Search user principals
234
	 *
235
	 * @param array $searchProperties
236
	 * @param string $test
237
	 * @return array
238
	 */
239
	protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
240
		$results = [];
241
242
		// If sharing is disabled, return the empty array
243
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
244
		if (!$shareAPIEnabled) {
245
			return [];
246
		}
247
248
		$allowEnumeration = $this->shareManager->allowEnumeration();
249
		$limitEnumeration = $this->shareManager->limitEnumerationToGroups();
250
251
		// If sharing is restricted to group members only,
252
		// return only members that have groups in common
253
		$restrictGroups = false;
254
		if ($this->shareManager->shareWithGroupMembersOnly()) {
255
			$user = $this->userSession->getUser();
256
			if (!$user) {
257
				return [];
258
			}
259
260
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
261
		}
262
263
		$currentUserGroups = [];
264
		if ($limitEnumeration) {
265
			$currentUser = $this->userSession->getUser();
266
			if ($currentUser) {
267
				$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
268
			}
269
		}
270
271
		foreach ($searchProperties as $prop => $value) {
272
			switch ($prop) {
273
				case '{http://sabredav.org/ns}email-address':
274
					$users = $this->userManager->getByEmail($value);
275
276
					if (!$allowEnumeration) {
277
						$users = \array_filter($users, static function(IUser $user) use ($value) {
278
							return $user->getEMailAddress() === $value;
279
						});
280
					}
281
282
					if ($limitEnumeration) {
283
						$users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) {
284
							return !empty(array_intersect(
285
									$this->groupManager->getUserGroupIds($user),
286
									$currentUserGroups
287
								)) || $user->getEMailAddress() === $value;
288
						});
289
					}
290
291
					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
292
						// is sharing restricted to groups only?
293
						if ($restrictGroups !== false) {
294
							$userGroups = $this->groupManager->getUserGroupIds($user);
295
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
296
								return $carry;
297
							}
298
						}
299
300
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
301
						return $carry;
302
					}, []);
303
					break;
304
305
				case '{DAV:}displayname':
306
					$users = $this->userManager->searchDisplayName($value);
307
308
					if (!$allowEnumeration) {
309
						$users = \array_filter($users, static function(IUser $user) use ($value) {
310
							return $user->getDisplayName() === $value;
311
						});
312
					}
313
314
					if ($limitEnumeration) {
315
						$users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) {
316
							return !empty(array_intersect(
317
									$this->groupManager->getUserGroupIds($user),
318
									$currentUserGroups
319
								)) || $user->getDisplayName() === $value;
320
						});
321
					}
322
323
					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
324
						// is sharing restricted to groups only?
325
						if ($restrictGroups !== false) {
326
							$userGroups = $this->groupManager->getUserGroupIds($user);
327
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
328
								return $carry;
329
							}
330
						}
331
332
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
333
						return $carry;
334
					}, []);
335
					break;
336
337
				case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
338
					// If you add support for more search properties that qualify as a user-address,
339
					// please also add them to the array below
340
					$results[] = $this->searchUserPrincipals([
341
						// In theory this should also search for principal:principals/users/...
342
						// but that's used internally only anyway and i don't know of any client querying that
343
						'{http://sabredav.org/ns}email-address' => $value,
344
					], 'anyof');
345
					break;
346
347
				default:
348
					$results[] = [];
349
					break;
350
			}
351
		}
352
353
		// results is an array of arrays, so this is not the first search result
354
		// but the results of the first searchProperty
355
		if (count($results) === 1) {
356
			return $results[0];
357
		}
358
359
		switch ($test) {
360
			case 'anyof':
361
				return array_values(array_unique(array_merge(...$results)));
362
363
			case 'allof':
364
			default:
365
				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

365
				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...
366
		}
367
	}
368
369
	/**
370
	 * @param string $prefixPath
371
	 * @param array $searchProperties
372
	 * @param string $test
373
	 * @return array
374
	 */
375
	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...
376
		if (count($searchProperties) === 0) {
377
			return [];
378
		}
379
380
		switch ($prefixPath) {
381
			case 'principals/users':
382
				return $this->searchUserPrincipals($searchProperties, $test);
383
384
			default:
385
				return [];
386
		}
387
	}
388
389
	/**
390
	 * @param string $uri
391
	 * @param string $principalPrefix
392
	 * @return string
393
	 */
394
	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...
395
		// If sharing is disabled, return the empty array
396
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
397
		if (!$shareAPIEnabled) {
398
			return null;
399
		}
400
401
		// If sharing is restricted to group members only,
402
		// return only members that have groups in common
403
		$restrictGroups = false;
404
		if ($this->shareManager->shareWithGroupMembersOnly()) {
405
			$user = $this->userSession->getUser();
406
			if (!$user) {
407
				return null;
408
			}
409
410
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
411
		}
412
413
		if (strpos($uri, 'mailto:') === 0) {
414
			if ($principalPrefix === 'principals/users') {
415
				$users = $this->userManager->getByEmail(substr($uri, 7));
416
				if (count($users) !== 1) {
417
					return null;
418
				}
419
				$user = $users[0];
420
421
				if ($restrictGroups !== false) {
422
					$userGroups = $this->groupManager->getUserGroupIds($user);
423
					if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
424
						return null;
425
					}
426
				}
427
428
				return $this->principalPrefix . '/' . $user->getUID();
429
			}
430
		}
431
		if (substr($uri, 0, 10) === 'principal:') {
432
			$principal = substr($uri, 10);
433
			$principal = $this->getPrincipalByPath($principal);
434
			if ($principal !== null) {
0 ignored issues
show
introduced by
The condition $principal !== null is always true.
Loading history...
435
				return $principal['uri'];
436
			}
437
		}
438
439
		return null;
440
	}
441
442
	/**
443
	 * @param IUser $user
444
	 * @return array
445
	 */
446
	protected function userToPrincipal($user) {
447
		$userId = $user->getUID();
448
		$displayName = $user->getDisplayName();
449
		$principal = [
450
				'uri' => $this->principalPrefix . '/' . $userId,
451
				'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
0 ignored issues
show
introduced by
The condition is_null($displayName) is always false.
Loading history...
452
				'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
453
		];
454
455
		$email = $user->getEMailAddress();
456
		if (!empty($email)) {
457
			$principal['{http://sabredav.org/ns}email-address'] = $email;
458
		}
459
460
		return $principal;
461
	}
462
463
	public function getPrincipalPrefix() {
464
		return $this->principalPrefix;
465
	}
466
467
	/**
468
	 * @param string $circleUniqueId
469
	 * @return array|null
470
	 * @throws \OCP\AppFramework\QueryException
471
	 * @suppress PhanUndeclaredClassMethod
472
	 * @suppress PhanUndeclaredClassCatch
473
	 */
474
	protected function circleToPrincipal($circleUniqueId) {
475
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
476
			return null;
477
		}
478
479
		try {
480
			$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...
481
		} catch(QueryException $ex) {
482
			return null;
483
		} catch(CircleDoesNotExistException $ex) {
484
			return null;
485
		}
486
487
		if (!$circle) {
488
			return null;
489
		}
490
491
		$principal = [
492
			'uri' => 'principals/circles/' . $circleUniqueId,
493
			'{DAV:}displayname' => $circle->getName(),
494
		];
495
496
		return $principal;
497
	}
498
499
	/**
500
	 * Returns the list of circles a principal is a member of
501
	 *
502
	 * @param string $principal
503
	 * @return array
504
	 * @throws Exception
505
	 * @throws \OCP\AppFramework\QueryException
506
	 * @suppress PhanUndeclaredClassMethod
507
	 */
508
	public function getCircleMembership($principal):array {
509
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
510
			return [];
511
		}
512
513
		list($prefix, $name) = \Sabre\Uri\split($principal);
514
		if ($this->hasCircles && $prefix === $this->principalPrefix) {
515
			$user = $this->userManager->get($name);
516
			if (!$user) {
517
				throw new Exception('Principal not found');
518
			}
519
520
			$circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
521
522
			$circles = array_map(function($circle) {
523
				/** @var \OCA\Circles\Model\Circle $circle */
524
				return 'principals/circles/' . urlencode($circle->getUniqueId());
525
			}, $circles);
526
527
			return $circles;
528
		}
529
530
		return [];
531
	}
532
}
533