Passed
Push — master ( 6a3321...65b6b4 )
by Christoph
13:21 queued 14s
created

Principal::getPrincipalByPath()   C

Complexity

Conditions 13
Paths 16

Size

Total Lines 49
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 26
c 1
b 0
f 0
nc 16
nop 1
dl 0
loc 49
rs 6.6166

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
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2018, Georg Ehrke
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bart Visscher <[email protected]>
8
 * @author Christoph Seitz <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Daniel Kesselberg <[email protected]>
11
 * @author Georg Ehrke <[email protected]>
12
 * @author Jakob Sack <[email protected]>
13
 * @author Julius Härtl <[email protected]>
14
 * @author Lukas Reschke <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Thomas Müller <[email protected]>
18
 * @author Vincent Petry <[email protected]>
19
 * @author Vinicius Cubas Brand <[email protected]>
20
 *
21
 * @license AGPL-3.0
22
 *
23
 * This code is free software: you can redistribute it and/or modify
24
 * it under the terms of the GNU Affero General Public License, version 3,
25
 * as published by the Free Software Foundation.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
 * GNU Affero General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
33
 * along with this program. If not, see <http://www.gnu.org/licenses/>
34
 *
35
 */
36
37
namespace OCA\DAV\Connector\Sabre;
38
39
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...
40
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
41
use OCA\DAV\Traits\PrincipalProxyTrait;
42
use OCP\App\IAppManager;
43
use OCP\AppFramework\QueryException;
44
use OCP\Constants;
45
use OCP\IConfig;
46
use OCP\IGroup;
47
use OCP\IGroupManager;
48
use OCP\IUser;
49
use OCP\IUserManager;
50
use OCP\IUserSession;
51
use OCP\Share\IManager as IShareManager;
52
use Sabre\DAV\Exception;
53
use Sabre\DAV\PropPatch;
54
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
55
56
class Principal implements BackendInterface {
57
58
	/** @var IUserManager */
59
	private $userManager;
60
61
	/** @var IGroupManager */
62
	private $groupManager;
63
64
	/** @var IShareManager */
65
	private $shareManager;
66
67
	/** @var IUserSession */
68
	private $userSession;
69
70
	/** @var IAppManager */
71
	private $appManager;
72
73
	/** @var string */
74
	private $principalPrefix;
75
76
	/** @var bool */
77
	private $hasGroups;
78
79
	/** @var bool */
80
	private $hasCircles;
81
82
	/** @var ProxyMapper */
83
	private $proxyMapper;
84
85
	/** @var IConfig */
86
	private $config;
87
88
	/**
89
	 * Principal constructor.
90
	 *
91
	 * @param IUserManager $userManager
92
	 * @param IGroupManager $groupManager
93
	 * @param IShareManager $shareManager
94
	 * @param IUserSession $userSession
95
	 * @param IAppManager $appManager
96
	 * @param ProxyMapper $proxyMapper
97
	 * @param IConfig $config
98
	 * @param string $principalPrefix
99
	 */
100
	public function __construct(IUserManager $userManager,
101
								IGroupManager $groupManager,
102
								IShareManager $shareManager,
103
								IUserSession $userSession,
104
								IAppManager $appManager,
105
								ProxyMapper $proxyMapper,
106
								IConfig $config,
107
								string $principalPrefix = 'principals/users/') {
108
		$this->userManager = $userManager;
109
		$this->groupManager = $groupManager;
110
		$this->shareManager = $shareManager;
111
		$this->userSession = $userSession;
112
		$this->appManager = $appManager;
113
		$this->principalPrefix = trim($principalPrefix, '/');
114
		$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
115
		$this->proxyMapper = $proxyMapper;
116
		$this->config = $config;
117
	}
118
119
	use PrincipalProxyTrait {
120
		getGroupMembership as protected traitGetGroupMembership;
121
	}
122
123
	/**
124
	 * Returns a list of principals based on a prefix.
125
	 *
126
	 * This prefix will often contain something like 'principals'. You are only
127
	 * expected to return principals that are in this base path.
128
	 *
129
	 * You are expected to return at least a 'uri' for every user, you can
130
	 * return any additional properties if you wish so. Common properties are:
131
	 *   {DAV:}displayname
132
	 *
133
	 * @param string $prefixPath
134
	 * @return string[]
135
	 */
136
	public function getPrincipalsByPrefix($prefixPath) {
137
		$principals = [];
138
139
		if ($prefixPath === $this->principalPrefix) {
140
			foreach ($this->userManager->search('') as $user) {
141
				$principals[] = $this->userToPrincipal($user);
142
			}
143
		}
144
145
		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...
146
	}
147
148
	/**
149
	 * Returns a specific principal, specified by it's path.
150
	 * The returned structure should be the exact same as from
151
	 * getPrincipalsByPrefix.
152
	 *
153
	 * @param string $path
154
	 * @return array
155
	 */
156
	public function getPrincipalByPath($path) {
157
		list($prefix, $name) = \Sabre\Uri\split($path);
158
		$decodedName = urldecode($name);
159
160
		if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
161
			list($prefix2, $name2) = \Sabre\Uri\split($prefix);
162
163
			if ($prefix2 === $this->principalPrefix) {
164
				$user = $this->userManager->get($name2);
165
166
				if ($user !== null) {
167
					return [
168
						'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
169
					];
170
				}
171
				return null;
172
			}
173
		}
174
175
		if ($prefix === $this->principalPrefix) {
176
			// Depending on where it is called, it may happen that this function
177
			// is called either with a urlencoded version of the name or with a non-urlencoded one.
178
			// The urldecode function replaces %## and +, both of which are forbidden in usernames.
179
			// Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
180
			$user = $this->userManager->get($decodedName);
181
182
			if ($user !== null) {
183
				return $this->userToPrincipal($user);
184
			}
185
		} elseif ($prefix === 'principals/circles') {
186
			if ($this->userSession->getUser() !== null) {
187
				// At the time of writing - 2021-01-19 — a mixed state is possible.
188
				// The second condition can be removed when this is fixed.
189
				return $this->circleToPrincipal($decodedName)
190
					?: $this->circleToPrincipal($name);
191
			}
192
		} elseif ($prefix === 'principals/groups') {
193
			// At the time of writing - 2021-01-19 — a mixed state is possible.
194
			// The second condition can be removed when this is fixed.
195
			$group = $this->groupManager->get($decodedName)
196
				?: $this->groupManager->get($name);
197
			if ($group instanceof IGroup) {
198
				return [
199
					'uri' => 'principals/groups/' . $name,
200
					'{DAV:}displayname' => $group->getDisplayName(),
201
				];
202
			}
203
		}
204
		return null;
205
	}
206
207
	/**
208
	 * Returns the list of groups a principal is a member of
209
	 *
210
	 * @param string $principal
211
	 * @param bool $needGroups
212
	 * @return array
213
	 * @throws Exception
214
	 */
215
	public function getGroupMembership($principal, $needGroups = false) {
216
		list($prefix, $name) = \Sabre\Uri\split($principal);
217
218
		if ($prefix !== $this->principalPrefix) {
219
			return [];
220
		}
221
222
		$user = $this->userManager->get($name);
223
		if (!$user) {
224
			throw new Exception('Principal not found');
225
		}
226
227
		$groups = [];
228
229
		if ($this->hasGroups || $needGroups) {
230
			$userGroups = $this->groupManager->getUserGroups($user);
231
			foreach ($userGroups as $userGroup) {
232
				$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
233
			}
234
		}
235
236
		$groups = array_unique(array_merge(
237
			$groups,
238
			$this->traitGetGroupMembership($principal, $needGroups)
239
		));
240
241
		return $groups;
242
	}
243
244
	/**
245
	 * @param string $path
246
	 * @param PropPatch $propPatch
247
	 * @return int
248
	 */
249
	public function updatePrincipal($path, PropPatch $propPatch) {
250
		return 0;
251
	}
252
253
	/**
254
	 * Search user principals
255
	 *
256
	 * @param array $searchProperties
257
	 * @param string $test
258
	 * @return array
259
	 */
260
	protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
261
		$results = [];
262
263
		// If sharing is disabled, return the empty array
264
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
265
		if (!$shareAPIEnabled) {
266
			return [];
267
		}
268
269
		$allowEnumeration = $this->shareManager->allowEnumeration();
270
		$limitEnumeration = $this->shareManager->limitEnumerationToGroups();
271
272
		// If sharing is restricted to group members only,
273
		// return only members that have groups in common
274
		$restrictGroups = false;
275
		if ($this->shareManager->shareWithGroupMembersOnly()) {
276
			$user = $this->userSession->getUser();
277
			if (!$user) {
278
				return [];
279
			}
280
281
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
282
		}
283
284
		$currentUserGroups = [];
285
		if ($limitEnumeration) {
286
			$currentUser = $this->userSession->getUser();
287
			if ($currentUser) {
288
				$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
289
			}
290
		}
291
292
		$searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
293
		if ($searchLimit <= 0) {
294
			$searchLimit = null;
295
		}
296
		foreach ($searchProperties as $prop => $value) {
297
			switch ($prop) {
298
				case '{http://sabredav.org/ns}email-address':
299
					$users = $this->userManager->getByEmail($value);
300
301
					if (!$allowEnumeration) {
302
						$users = \array_filter($users, static function (IUser $user) use ($value) {
303
							return $user->getEMailAddress() === $value;
304
						});
305
					}
306
307
					if ($limitEnumeration) {
308
						$users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) {
309
							return !empty(array_intersect(
310
									$this->groupManager->getUserGroupIds($user),
311
									$currentUserGroups
312
								)) || $user->getEMailAddress() === $value;
313
						});
314
					}
315
316
					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
317
						// is sharing restricted to groups only?
318
						if ($restrictGroups !== false) {
319
							$userGroups = $this->groupManager->getUserGroupIds($user);
320
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
321
								return $carry;
322
							}
323
						}
324
325
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
326
						return $carry;
327
					}, []);
328
					break;
329
330
				case '{DAV:}displayname':
331
					$users = $this->userManager->searchDisplayName($value, $searchLimit);
332
333
					if (!$allowEnumeration) {
334
						$users = \array_filter($users, static function (IUser $user) use ($value) {
335
							return $user->getDisplayName() === $value;
336
						});
337
					}
338
339
					if ($limitEnumeration) {
340
						$users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) {
341
							return !empty(array_intersect(
342
									$this->groupManager->getUserGroupIds($user),
343
									$currentUserGroups
344
								)) || $user->getDisplayName() === $value;
345
						});
346
					}
347
348
					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
349
						// is sharing restricted to groups only?
350
						if ($restrictGroups !== false) {
351
							$userGroups = $this->groupManager->getUserGroupIds($user);
352
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
353
								return $carry;
354
							}
355
						}
356
357
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
358
						return $carry;
359
					}, []);
360
					break;
361
362
				case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
363
					// If you add support for more search properties that qualify as a user-address,
364
					// please also add them to the array below
365
					$results[] = $this->searchUserPrincipals([
366
						// In theory this should also search for principal:principals/users/...
367
						// but that's used internally only anyway and i don't know of any client querying that
368
						'{http://sabredav.org/ns}email-address' => $value,
369
					], 'anyof');
370
					break;
371
372
				default:
373
					$results[] = [];
374
					break;
375
			}
376
		}
377
378
		// results is an array of arrays, so this is not the first search result
379
		// but the results of the first searchProperty
380
		if (count($results) === 1) {
381
			return $results[0];
382
		}
383
384
		switch ($test) {
385
			case 'anyof':
386
				return array_values(array_unique(array_merge(...$results)));
387
388
			case 'allof':
389
			default:
390
				return array_values(array_intersect(...$results));
391
		}
392
	}
393
394
	/**
395
	 * @param string $prefixPath
396
	 * @param array $searchProperties
397
	 * @param string $test
398
	 * @return array
399
	 */
400
	public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
401
		if (count($searchProperties) === 0) {
402
			return [];
403
		}
404
405
		switch ($prefixPath) {
406
			case 'principals/users':
407
				return $this->searchUserPrincipals($searchProperties, $test);
408
409
			default:
410
				return [];
411
		}
412
	}
413
414
	/**
415
	 * @param string $uri
416
	 * @param string $principalPrefix
417
	 * @return string
418
	 */
419
	public function findByUri($uri, $principalPrefix) {
420
		// If sharing is disabled, return the empty array
421
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
422
		if (!$shareAPIEnabled) {
423
			return null;
424
		}
425
426
		// If sharing is restricted to group members only,
427
		// return only members that have groups in common
428
		$restrictGroups = false;
429
		if ($this->shareManager->shareWithGroupMembersOnly()) {
430
			$user = $this->userSession->getUser();
431
			if (!$user) {
432
				return null;
433
			}
434
435
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
436
		}
437
438
		if (strpos($uri, 'mailto:') === 0) {
439
			if ($principalPrefix === 'principals/users') {
440
				$users = $this->userManager->getByEmail(substr($uri, 7));
441
				if (count($users) !== 1) {
442
					return null;
443
				}
444
				$user = $users[0];
445
446
				if ($restrictGroups !== false) {
447
					$userGroups = $this->groupManager->getUserGroupIds($user);
448
					if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
449
						return null;
450
					}
451
				}
452
453
				return $this->principalPrefix . '/' . $user->getUID();
454
			}
455
		}
456
		if (substr($uri, 0, 10) === 'principal:') {
457
			$principal = substr($uri, 10);
458
			$principal = $this->getPrincipalByPath($principal);
459
			if ($principal !== null) {
0 ignored issues
show
introduced by
The condition $principal !== null is always true.
Loading history...
460
				return $principal['uri'];
461
			}
462
		}
463
464
		return null;
465
	}
466
467
	/**
468
	 * @param IUser $user
469
	 * @return array
470
	 */
471
	protected function userToPrincipal($user) {
472
		$userId = $user->getUID();
473
		$displayName = $user->getDisplayName();
474
		$principal = [
475
			'uri' => $this->principalPrefix . '/' . $userId,
476
			'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
0 ignored issues
show
introduced by
The condition is_null($displayName) is always false.
Loading history...
477
			'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
478
		];
479
480
		$email = $user->getEMailAddress();
481
		if (!empty($email)) {
482
			$principal['{http://sabredav.org/ns}email-address'] = $email;
483
		}
484
485
		return $principal;
486
	}
487
488
	public function getPrincipalPrefix() {
489
		return $this->principalPrefix;
490
	}
491
492
	/**
493
	 * @param string $circleUniqueId
494
	 * @return array|null
495
	 */
496
	protected function circleToPrincipal($circleUniqueId) {
497
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
498
			return null;
499
		}
500
501
		try {
502
			$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...
503
		} catch (QueryException $ex) {
504
			return null;
505
		} catch (CircleDoesNotExistException $ex) {
506
			return null;
507
		}
508
509
		if (!$circle) {
510
			return null;
511
		}
512
513
		$principal = [
514
			'uri' => 'principals/circles/' . $circleUniqueId,
515
			'{DAV:}displayname' => $circle->getName(),
516
		];
517
518
		return $principal;
519
	}
520
521
	/**
522
	 * Returns the list of circles a principal is a member of
523
	 *
524
	 * @param string $principal
525
	 * @return array
526
	 * @throws Exception
527
	 * @throws \OCP\AppFramework\QueryException
528
	 * @suppress PhanUndeclaredClassMethod
529
	 */
530
	public function getCircleMembership($principal):array {
531
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
532
			return [];
533
		}
534
535
		list($prefix, $name) = \Sabre\Uri\split($principal);
536
		if ($this->hasCircles && $prefix === $this->principalPrefix) {
537
			$user = $this->userManager->get($name);
538
			if (!$user) {
539
				throw new Exception('Principal not found');
540
			}
541
542
			$circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
543
544
			$circles = array_map(function ($circle) {
545
				/** @var \OCA\Circles\Model\Circle $circle */
546
				return 'principals/circles/' . urlencode($circle->getUniqueId());
547
			}, $circles);
548
549
			return $circles;
550
		}
551
552
		return [];
553
	}
554
}
555