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

Principal::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 17
rs 9.9666
cc 1
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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