Passed
Push — master ( 033b03...0a5481 )
by Daniel
16:10 queued 13s
created
apps/dav/lib/Connector/Sabre/Principal.php 1 patch
Indentation   +586 added lines, -586 removed lines patch added patch discarded remove patch
@@ -61,590 +61,590 @@
 block discarded – undo
61 61
 
62 62
 class Principal implements BackendInterface {
63 63
 
64
-	/** @var IUserManager */
65
-	private $userManager;
66
-
67
-	/** @var IGroupManager */
68
-	private $groupManager;
69
-
70
-	/** @var IAccountManager */
71
-	private $accountManager;
72
-
73
-	/** @var IShareManager */
74
-	private $shareManager;
75
-
76
-	/** @var IUserSession */
77
-	private $userSession;
78
-
79
-	/** @var IAppManager */
80
-	private $appManager;
81
-
82
-	/** @var string */
83
-	private $principalPrefix;
84
-
85
-	/** @var bool */
86
-	private $hasGroups;
87
-
88
-	/** @var bool */
89
-	private $hasCircles;
90
-
91
-	/** @var ProxyMapper */
92
-	private $proxyMapper;
93
-
94
-	/** @var KnownUserService */
95
-	private $knownUserService;
96
-
97
-	/** @var IConfig */
98
-	private $config;
99
-	/** @var IFactory */
100
-	private $languageFactory;
101
-
102
-	public function __construct(IUserManager $userManager,
103
-								IGroupManager $groupManager,
104
-								IAccountManager $accountManager,
105
-								IShareManager $shareManager,
106
-								IUserSession $userSession,
107
-								IAppManager $appManager,
108
-								ProxyMapper $proxyMapper,
109
-								KnownUserService $knownUserService,
110
-								IConfig $config,
111
-								IFactory $languageFactory,
112
-								string $principalPrefix = 'principals/users/') {
113
-		$this->userManager = $userManager;
114
-		$this->groupManager = $groupManager;
115
-		$this->accountManager = $accountManager;
116
-		$this->shareManager = $shareManager;
117
-		$this->userSession = $userSession;
118
-		$this->appManager = $appManager;
119
-		$this->principalPrefix = trim($principalPrefix, '/');
120
-		$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
121
-		$this->proxyMapper = $proxyMapper;
122
-		$this->knownUserService = $knownUserService;
123
-		$this->config = $config;
124
-		$this->languageFactory = $languageFactory;
125
-	}
126
-
127
-	use PrincipalProxyTrait {
128
-		getGroupMembership as protected traitGetGroupMembership;
129
-	}
130
-
131
-	/**
132
-	 * Returns a list of principals based on a prefix.
133
-	 *
134
-	 * This prefix will often contain something like 'principals'. You are only
135
-	 * expected to return principals that are in this base path.
136
-	 *
137
-	 * You are expected to return at least a 'uri' for every user, you can
138
-	 * return any additional properties if you wish so. Common properties are:
139
-	 *   {DAV:}displayname
140
-	 *
141
-	 * @param string $prefixPath
142
-	 * @return string[]
143
-	 */
144
-	public function getPrincipalsByPrefix($prefixPath) {
145
-		$principals = [];
146
-
147
-		if ($prefixPath === $this->principalPrefix) {
148
-			foreach ($this->userManager->search('') as $user) {
149
-				$principals[] = $this->userToPrincipal($user);
150
-			}
151
-		}
152
-
153
-		return $principals;
154
-	}
155
-
156
-	/**
157
-	 * Returns a specific principal, specified by it's path.
158
-	 * The returned structure should be the exact same as from
159
-	 * getPrincipalsByPrefix.
160
-	 *
161
-	 * @param string $path
162
-	 * @return array
163
-	 */
164
-	public function getPrincipalByPath($path) {
165
-		[$prefix, $name] = \Sabre\Uri\split($path);
166
-		$decodedName = urldecode($name);
167
-
168
-		if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
169
-			[$prefix2, $name2] = \Sabre\Uri\split($prefix);
170
-
171
-			if ($prefix2 === $this->principalPrefix) {
172
-				$user = $this->userManager->get($name2);
173
-
174
-				if ($user !== null) {
175
-					return [
176
-						'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
177
-					];
178
-				}
179
-				return null;
180
-			}
181
-		}
182
-
183
-		if ($prefix === $this->principalPrefix) {
184
-			// Depending on where it is called, it may happen that this function
185
-			// is called either with a urlencoded version of the name or with a non-urlencoded one.
186
-			// The urldecode function replaces %## and +, both of which are forbidden in usernames.
187
-			// Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
188
-			$user = $this->userManager->get($decodedName);
189
-
190
-			if ($user !== null) {
191
-				return $this->userToPrincipal($user);
192
-			}
193
-		} elseif ($prefix === 'principals/circles') {
194
-			if ($this->userSession->getUser() !== null) {
195
-				// At the time of writing - 2021-01-19 — a mixed state is possible.
196
-				// The second condition can be removed when this is fixed.
197
-				return $this->circleToPrincipal($decodedName)
198
-					?: $this->circleToPrincipal($name);
199
-			}
200
-		} elseif ($prefix === 'principals/groups') {
201
-			// At the time of writing - 2021-01-19 — a mixed state is possible.
202
-			// The second condition can be removed when this is fixed.
203
-			$group = $this->groupManager->get($decodedName)
204
-				?: $this->groupManager->get($name);
205
-			if ($group instanceof IGroup) {
206
-				return [
207
-					'uri' => 'principals/groups/' . $name,
208
-					'{DAV:}displayname' => $group->getDisplayName(),
209
-				];
210
-			}
211
-		}
212
-		return null;
213
-	}
214
-
215
-	/**
216
-	 * Returns the list of groups a principal is a member of
217
-	 *
218
-	 * @param string $principal
219
-	 * @param bool $needGroups
220
-	 * @return array
221
-	 * @throws Exception
222
-	 */
223
-	public function getGroupMembership($principal, $needGroups = false) {
224
-		[$prefix, $name] = \Sabre\Uri\split($principal);
225
-
226
-		if ($prefix !== $this->principalPrefix) {
227
-			return [];
228
-		}
229
-
230
-		$user = $this->userManager->get($name);
231
-		if (!$user) {
232
-			throw new Exception('Principal not found');
233
-		}
234
-
235
-		$groups = [];
236
-
237
-		if ($this->hasGroups || $needGroups) {
238
-			$userGroups = $this->groupManager->getUserGroups($user);
239
-			foreach ($userGroups as $userGroup) {
240
-				$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
241
-			}
242
-		}
243
-
244
-		$groups = array_unique(array_merge(
245
-			$groups,
246
-			$this->traitGetGroupMembership($principal, $needGroups)
247
-		));
248
-
249
-		return $groups;
250
-	}
251
-
252
-	/**
253
-	 * @param string $path
254
-	 * @param PropPatch $propPatch
255
-	 * @return int
256
-	 */
257
-	public function updatePrincipal($path, PropPatch $propPatch) {
258
-		return 0;
259
-	}
260
-
261
-	/**
262
-	 * Search user principals
263
-	 *
264
-	 * @param array $searchProperties
265
-	 * @param string $test
266
-	 * @return array
267
-	 */
268
-	protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
269
-		$results = [];
270
-
271
-		// If sharing is disabled, return the empty array
272
-		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
273
-		if (!$shareAPIEnabled) {
274
-			return [];
275
-		}
276
-
277
-		$allowEnumeration = $this->shareManager->allowEnumeration();
278
-		$limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
279
-		$limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
280
-		$allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
281
-		$ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
282
-		$matchEmail = $this->shareManager->matchEmail();
283
-
284
-		// If sharing is restricted to group members only,
285
-		// return only members that have groups in common
286
-		$restrictGroups = false;
287
-		$currentUser = $this->userSession->getUser();
288
-		if ($this->shareManager->shareWithGroupMembersOnly()) {
289
-			if (!$currentUser instanceof IUser) {
290
-				return [];
291
-			}
292
-
293
-			$restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
294
-		}
295
-
296
-		$currentUserGroups = [];
297
-		if ($limitEnumerationGroup) {
298
-			if ($currentUser instanceof IUser) {
299
-				$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
300
-			}
301
-		}
302
-
303
-		$searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
304
-		if ($searchLimit <= 0) {
305
-			$searchLimit = null;
306
-		}
307
-		foreach ($searchProperties as $prop => $value) {
308
-			switch ($prop) {
309
-				case '{http://sabredav.org/ns}email-address':
310
-					if (!$allowEnumeration) {
311
-						if ($allowEnumerationFullMatch && $matchEmail) {
312
-							$users = $this->userManager->getByEmail($value);
313
-						} else {
314
-							$users = [];
315
-						}
316
-					} else {
317
-						$users = $this->userManager->getByEmail($value);
318
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
319
-							if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
320
-								return true;
321
-							}
322
-
323
-							if ($limitEnumerationPhone
324
-								&& $currentUser instanceof IUser
325
-								&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
326
-								// Synced phonebook match
327
-								return true;
328
-							}
329
-
330
-							if (!$limitEnumerationGroup) {
331
-								// No limitation on enumeration, all allowed
332
-								return true;
333
-							}
334
-
335
-							return !empty($currentUserGroups) && !empty(array_intersect(
336
-								$this->groupManager->getUserGroupIds($user),
337
-								$currentUserGroups
338
-							));
339
-						});
340
-					}
341
-
342
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
343
-						// is sharing restricted to groups only?
344
-						if ($restrictGroups !== false) {
345
-							$userGroups = $this->groupManager->getUserGroupIds($user);
346
-							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
347
-								return $carry;
348
-							}
349
-						}
350
-
351
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
352
-						return $carry;
353
-					}, []);
354
-					break;
355
-
356
-				case '{DAV:}displayname':
357
-
358
-					if (!$allowEnumeration) {
359
-						if ($allowEnumerationFullMatch) {
360
-							$lowerSearch = strtolower($value);
361
-							$users = $this->userManager->searchDisplayName($value, $searchLimit);
362
-							$users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
363
-								$lowerDisplayName = strtolower($user->getDisplayName());
364
-								return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
365
-							});
366
-						} else {
367
-							$users = [];
368
-						}
369
-					} else {
370
-						$users = $this->userManager->searchDisplayName($value, $searchLimit);
371
-						$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
372
-							if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
373
-								return true;
374
-							}
375
-
376
-							if ($limitEnumerationPhone
377
-								&& $currentUser instanceof IUser
378
-								&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
379
-								// Synced phonebook match
380
-								return true;
381
-							}
382
-
383
-							if (!$limitEnumerationGroup) {
384
-								// No limitation on enumeration, all allowed
385
-								return true;
386
-							}
387
-
388
-							return !empty($currentUserGroups) && !empty(array_intersect(
389
-								$this->groupManager->getUserGroupIds($user),
390
-								$currentUserGroups
391
-							));
392
-						});
393
-					}
394
-
395
-					$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
396
-						// is sharing restricted to groups only?
397
-						if ($restrictGroups !== false) {
398
-							$userGroups = $this->groupManager->getUserGroupIds($user);
399
-							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
400
-								return $carry;
401
-							}
402
-						}
403
-
404
-						$carry[] = $this->principalPrefix . '/' . $user->getUID();
405
-						return $carry;
406
-					}, []);
407
-					break;
408
-
409
-				case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
410
-					// If you add support for more search properties that qualify as a user-address,
411
-					// please also add them to the array below
412
-					$results[] = $this->searchUserPrincipals([
413
-						// In theory this should also search for principal:principals/users/...
414
-						// but that's used internally only anyway and i don't know of any client querying that
415
-						'{http://sabredav.org/ns}email-address' => $value,
416
-					], 'anyof');
417
-					break;
418
-
419
-				default:
420
-					$results[] = [];
421
-					break;
422
-			}
423
-		}
424
-
425
-		// results is an array of arrays, so this is not the first search result
426
-		// but the results of the first searchProperty
427
-		if (count($results) === 1) {
428
-			return $results[0];
429
-		}
430
-
431
-		switch ($test) {
432
-			case 'anyof':
433
-				return array_values(array_unique(array_merge(...$results)));
434
-
435
-			case 'allof':
436
-			default:
437
-				return array_values(array_intersect(...$results));
438
-		}
439
-	}
440
-
441
-	/**
442
-	 * @param string $prefixPath
443
-	 * @param array $searchProperties
444
-	 * @param string $test
445
-	 * @return array
446
-	 */
447
-	public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
448
-		if (count($searchProperties) === 0) {
449
-			return [];
450
-		}
451
-
452
-		switch ($prefixPath) {
453
-			case 'principals/users':
454
-				return $this->searchUserPrincipals($searchProperties, $test);
455
-
456
-			default:
457
-				return [];
458
-		}
459
-	}
460
-
461
-	/**
462
-	 * @param string $uri
463
-	 * @param string $principalPrefix
464
-	 * @return string
465
-	 */
466
-	public function findByUri($uri, $principalPrefix) {
467
-		// If sharing is disabled, return the empty array
468
-		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
469
-		if (!$shareAPIEnabled) {
470
-			return null;
471
-		}
472
-
473
-		// If sharing is restricted to group members only,
474
-		// return only members that have groups in common
475
-		$restrictGroups = false;
476
-		if ($this->shareManager->shareWithGroupMembersOnly()) {
477
-			$user = $this->userSession->getUser();
478
-			if (!$user) {
479
-				return null;
480
-			}
481
-
482
-			$restrictGroups = $this->groupManager->getUserGroupIds($user);
483
-		}
484
-
485
-		if (strpos($uri, 'mailto:') === 0) {
486
-			if ($principalPrefix === 'principals/users') {
487
-				$users = $this->userManager->getByEmail(substr($uri, 7));
488
-				if (count($users) !== 1) {
489
-					return null;
490
-				}
491
-				$user = $users[0];
492
-
493
-				if ($restrictGroups !== false) {
494
-					$userGroups = $this->groupManager->getUserGroupIds($user);
495
-					if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
496
-						return null;
497
-					}
498
-				}
499
-
500
-				return $this->principalPrefix . '/' . $user->getUID();
501
-			}
502
-		}
503
-		if (substr($uri, 0, 10) === 'principal:') {
504
-			$principal = substr($uri, 10);
505
-			$principal = $this->getPrincipalByPath($principal);
506
-			if ($principal !== null) {
507
-				return $principal['uri'];
508
-			}
509
-		}
510
-
511
-		return null;
512
-	}
513
-
514
-	/**
515
-	 * @param IUser $user
516
-	 * @return array
517
-	 * @throws PropertyDoesNotExistException
518
-	 */
519
-	protected function userToPrincipal($user) {
520
-		$userId = $user->getUID();
521
-		$displayName = $user->getDisplayName();
522
-		$principal = [
523
-			'uri' => $this->principalPrefix . '/' . $userId,
524
-			'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
525
-			'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
526
-			'{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
527
-		];
528
-
529
-		$account = $this->accountManager->getAccount($user);
530
-		$alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
531
-
532
-		$email = $user->getSystemEMailAddress();
533
-		if (!empty($email)) {
534
-			$principal['{http://sabredav.org/ns}email-address'] = $email;
535
-		}
536
-
537
-		if (!empty($alternativeEmails)) {
538
-			$principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
539
-		}
540
-
541
-		return $principal;
542
-	}
543
-
544
-	public function getPrincipalPrefix() {
545
-		return $this->principalPrefix;
546
-	}
547
-
548
-	/**
549
-	 * @param string $circleUniqueId
550
-	 * @return array|null
551
-	 */
552
-	protected function circleToPrincipal($circleUniqueId) {
553
-		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
554
-			return null;
555
-		}
556
-
557
-		try {
558
-			$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
559
-		} catch (QueryException $ex) {
560
-			return null;
561
-		} catch (CircleNotFoundException $ex) {
562
-			return null;
563
-		}
564
-
565
-		if (!$circle) {
566
-			return null;
567
-		}
568
-
569
-		$principal = [
570
-			'uri' => 'principals/circles/' . $circleUniqueId,
571
-			'{DAV:}displayname' => $circle->getDisplayName(),
572
-		];
573
-
574
-		return $principal;
575
-	}
576
-
577
-	/**
578
-	 * Returns the list of circles a principal is a member of
579
-	 *
580
-	 * @param string $principal
581
-	 * @return array
582
-	 * @throws Exception
583
-	 * @throws \OCP\AppFramework\QueryException
584
-	 * @suppress PhanUndeclaredClassMethod
585
-	 */
586
-	public function getCircleMembership($principal):array {
587
-		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
588
-			return [];
589
-		}
590
-
591
-		[$prefix, $name] = \Sabre\Uri\split($principal);
592
-		if ($this->hasCircles && $prefix === $this->principalPrefix) {
593
-			$user = $this->userManager->get($name);
594
-			if (!$user) {
595
-				throw new Exception('Principal not found');
596
-			}
597
-
598
-			$circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
599
-
600
-			$circles = array_map(function ($circle) {
601
-				/** @var \OCA\Circles\Model\Circle $circle */
602
-				return 'principals/circles/' . urlencode($circle->getSingleId());
603
-			}, $circles);
604
-
605
-			return $circles;
606
-		}
607
-
608
-		return [];
609
-	}
610
-
611
-	/**
612
-	 * Get all email addresses associated to a principal.
613
-	 *
614
-	 * @param array $principal Data from getPrincipal*()
615
-	 * @return string[] All email addresses without the mailto: prefix
616
-	 */
617
-	public function getEmailAddressesOfPrincipal(array $principal): array {
618
-		$emailAddresses = [];
619
-
620
-		if (isset($principal['{http://sabredav.org/ns}email-address'])) {
621
-			$emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
622
-		}
623
-
624
-		if (isset($principal['{DAV:}alternate-URI-set'])) {
625
-			foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
626
-				if (str_starts_with($address, 'mailto:')) {
627
-					$emailAddresses[] = substr($address, 7);
628
-				}
629
-			}
630
-		}
631
-
632
-		if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
633
-			foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
634
-				if (str_starts_with($address, 'mailto:')) {
635
-					$emailAddresses[] = substr($address, 7);
636
-				}
637
-			}
638
-		}
639
-
640
-		if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
641
-			foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
642
-				if (str_starts_with($address, 'mailto:')) {
643
-					$emailAddresses[] = substr($address, 7);
644
-				}
645
-			}
646
-		}
647
-
648
-		return array_values(array_unique($emailAddresses));
649
-	}
64
+    /** @var IUserManager */
65
+    private $userManager;
66
+
67
+    /** @var IGroupManager */
68
+    private $groupManager;
69
+
70
+    /** @var IAccountManager */
71
+    private $accountManager;
72
+
73
+    /** @var IShareManager */
74
+    private $shareManager;
75
+
76
+    /** @var IUserSession */
77
+    private $userSession;
78
+
79
+    /** @var IAppManager */
80
+    private $appManager;
81
+
82
+    /** @var string */
83
+    private $principalPrefix;
84
+
85
+    /** @var bool */
86
+    private $hasGroups;
87
+
88
+    /** @var bool */
89
+    private $hasCircles;
90
+
91
+    /** @var ProxyMapper */
92
+    private $proxyMapper;
93
+
94
+    /** @var KnownUserService */
95
+    private $knownUserService;
96
+
97
+    /** @var IConfig */
98
+    private $config;
99
+    /** @var IFactory */
100
+    private $languageFactory;
101
+
102
+    public function __construct(IUserManager $userManager,
103
+                                IGroupManager $groupManager,
104
+                                IAccountManager $accountManager,
105
+                                IShareManager $shareManager,
106
+                                IUserSession $userSession,
107
+                                IAppManager $appManager,
108
+                                ProxyMapper $proxyMapper,
109
+                                KnownUserService $knownUserService,
110
+                                IConfig $config,
111
+                                IFactory $languageFactory,
112
+                                string $principalPrefix = 'principals/users/') {
113
+        $this->userManager = $userManager;
114
+        $this->groupManager = $groupManager;
115
+        $this->accountManager = $accountManager;
116
+        $this->shareManager = $shareManager;
117
+        $this->userSession = $userSession;
118
+        $this->appManager = $appManager;
119
+        $this->principalPrefix = trim($principalPrefix, '/');
120
+        $this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
121
+        $this->proxyMapper = $proxyMapper;
122
+        $this->knownUserService = $knownUserService;
123
+        $this->config = $config;
124
+        $this->languageFactory = $languageFactory;
125
+    }
126
+
127
+    use PrincipalProxyTrait {
128
+        getGroupMembership as protected traitGetGroupMembership;
129
+    }
130
+
131
+    /**
132
+     * Returns a list of principals based on a prefix.
133
+     *
134
+     * This prefix will often contain something like 'principals'. You are only
135
+     * expected to return principals that are in this base path.
136
+     *
137
+     * You are expected to return at least a 'uri' for every user, you can
138
+     * return any additional properties if you wish so. Common properties are:
139
+     *   {DAV:}displayname
140
+     *
141
+     * @param string $prefixPath
142
+     * @return string[]
143
+     */
144
+    public function getPrincipalsByPrefix($prefixPath) {
145
+        $principals = [];
146
+
147
+        if ($prefixPath === $this->principalPrefix) {
148
+            foreach ($this->userManager->search('') as $user) {
149
+                $principals[] = $this->userToPrincipal($user);
150
+            }
151
+        }
152
+
153
+        return $principals;
154
+    }
155
+
156
+    /**
157
+     * Returns a specific principal, specified by it's path.
158
+     * The returned structure should be the exact same as from
159
+     * getPrincipalsByPrefix.
160
+     *
161
+     * @param string $path
162
+     * @return array
163
+     */
164
+    public function getPrincipalByPath($path) {
165
+        [$prefix, $name] = \Sabre\Uri\split($path);
166
+        $decodedName = urldecode($name);
167
+
168
+        if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
169
+            [$prefix2, $name2] = \Sabre\Uri\split($prefix);
170
+
171
+            if ($prefix2 === $this->principalPrefix) {
172
+                $user = $this->userManager->get($name2);
173
+
174
+                if ($user !== null) {
175
+                    return [
176
+                        'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
177
+                    ];
178
+                }
179
+                return null;
180
+            }
181
+        }
182
+
183
+        if ($prefix === $this->principalPrefix) {
184
+            // Depending on where it is called, it may happen that this function
185
+            // is called either with a urlencoded version of the name or with a non-urlencoded one.
186
+            // The urldecode function replaces %## and +, both of which are forbidden in usernames.
187
+            // Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
188
+            $user = $this->userManager->get($decodedName);
189
+
190
+            if ($user !== null) {
191
+                return $this->userToPrincipal($user);
192
+            }
193
+        } elseif ($prefix === 'principals/circles') {
194
+            if ($this->userSession->getUser() !== null) {
195
+                // At the time of writing - 2021-01-19 — a mixed state is possible.
196
+                // The second condition can be removed when this is fixed.
197
+                return $this->circleToPrincipal($decodedName)
198
+                    ?: $this->circleToPrincipal($name);
199
+            }
200
+        } elseif ($prefix === 'principals/groups') {
201
+            // At the time of writing - 2021-01-19 — a mixed state is possible.
202
+            // The second condition can be removed when this is fixed.
203
+            $group = $this->groupManager->get($decodedName)
204
+                ?: $this->groupManager->get($name);
205
+            if ($group instanceof IGroup) {
206
+                return [
207
+                    'uri' => 'principals/groups/' . $name,
208
+                    '{DAV:}displayname' => $group->getDisplayName(),
209
+                ];
210
+            }
211
+        }
212
+        return null;
213
+    }
214
+
215
+    /**
216
+     * Returns the list of groups a principal is a member of
217
+     *
218
+     * @param string $principal
219
+     * @param bool $needGroups
220
+     * @return array
221
+     * @throws Exception
222
+     */
223
+    public function getGroupMembership($principal, $needGroups = false) {
224
+        [$prefix, $name] = \Sabre\Uri\split($principal);
225
+
226
+        if ($prefix !== $this->principalPrefix) {
227
+            return [];
228
+        }
229
+
230
+        $user = $this->userManager->get($name);
231
+        if (!$user) {
232
+            throw new Exception('Principal not found');
233
+        }
234
+
235
+        $groups = [];
236
+
237
+        if ($this->hasGroups || $needGroups) {
238
+            $userGroups = $this->groupManager->getUserGroups($user);
239
+            foreach ($userGroups as $userGroup) {
240
+                $groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
241
+            }
242
+        }
243
+
244
+        $groups = array_unique(array_merge(
245
+            $groups,
246
+            $this->traitGetGroupMembership($principal, $needGroups)
247
+        ));
248
+
249
+        return $groups;
250
+    }
251
+
252
+    /**
253
+     * @param string $path
254
+     * @param PropPatch $propPatch
255
+     * @return int
256
+     */
257
+    public function updatePrincipal($path, PropPatch $propPatch) {
258
+        return 0;
259
+    }
260
+
261
+    /**
262
+     * Search user principals
263
+     *
264
+     * @param array $searchProperties
265
+     * @param string $test
266
+     * @return array
267
+     */
268
+    protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
269
+        $results = [];
270
+
271
+        // If sharing is disabled, return the empty array
272
+        $shareAPIEnabled = $this->shareManager->shareApiEnabled();
273
+        if (!$shareAPIEnabled) {
274
+            return [];
275
+        }
276
+
277
+        $allowEnumeration = $this->shareManager->allowEnumeration();
278
+        $limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
279
+        $limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
280
+        $allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
281
+        $ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
282
+        $matchEmail = $this->shareManager->matchEmail();
283
+
284
+        // If sharing is restricted to group members only,
285
+        // return only members that have groups in common
286
+        $restrictGroups = false;
287
+        $currentUser = $this->userSession->getUser();
288
+        if ($this->shareManager->shareWithGroupMembersOnly()) {
289
+            if (!$currentUser instanceof IUser) {
290
+                return [];
291
+            }
292
+
293
+            $restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
294
+        }
295
+
296
+        $currentUserGroups = [];
297
+        if ($limitEnumerationGroup) {
298
+            if ($currentUser instanceof IUser) {
299
+                $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
300
+            }
301
+        }
302
+
303
+        $searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
304
+        if ($searchLimit <= 0) {
305
+            $searchLimit = null;
306
+        }
307
+        foreach ($searchProperties as $prop => $value) {
308
+            switch ($prop) {
309
+                case '{http://sabredav.org/ns}email-address':
310
+                    if (!$allowEnumeration) {
311
+                        if ($allowEnumerationFullMatch && $matchEmail) {
312
+                            $users = $this->userManager->getByEmail($value);
313
+                        } else {
314
+                            $users = [];
315
+                        }
316
+                    } else {
317
+                        $users = $this->userManager->getByEmail($value);
318
+                        $users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
319
+                            if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
320
+                                return true;
321
+                            }
322
+
323
+                            if ($limitEnumerationPhone
324
+                                && $currentUser instanceof IUser
325
+                                && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
326
+                                // Synced phonebook match
327
+                                return true;
328
+                            }
329
+
330
+                            if (!$limitEnumerationGroup) {
331
+                                // No limitation on enumeration, all allowed
332
+                                return true;
333
+                            }
334
+
335
+                            return !empty($currentUserGroups) && !empty(array_intersect(
336
+                                $this->groupManager->getUserGroupIds($user),
337
+                                $currentUserGroups
338
+                            ));
339
+                        });
340
+                    }
341
+
342
+                    $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
343
+                        // is sharing restricted to groups only?
344
+                        if ($restrictGroups !== false) {
345
+                            $userGroups = $this->groupManager->getUserGroupIds($user);
346
+                            if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
347
+                                return $carry;
348
+                            }
349
+                        }
350
+
351
+                        $carry[] = $this->principalPrefix . '/' . $user->getUID();
352
+                        return $carry;
353
+                    }, []);
354
+                    break;
355
+
356
+                case '{DAV:}displayname':
357
+
358
+                    if (!$allowEnumeration) {
359
+                        if ($allowEnumerationFullMatch) {
360
+                            $lowerSearch = strtolower($value);
361
+                            $users = $this->userManager->searchDisplayName($value, $searchLimit);
362
+                            $users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
363
+                                $lowerDisplayName = strtolower($user->getDisplayName());
364
+                                return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
365
+                            });
366
+                        } else {
367
+                            $users = [];
368
+                        }
369
+                    } else {
370
+                        $users = $this->userManager->searchDisplayName($value, $searchLimit);
371
+                        $users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
372
+                            if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
373
+                                return true;
374
+                            }
375
+
376
+                            if ($limitEnumerationPhone
377
+                                && $currentUser instanceof IUser
378
+                                && $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
379
+                                // Synced phonebook match
380
+                                return true;
381
+                            }
382
+
383
+                            if (!$limitEnumerationGroup) {
384
+                                // No limitation on enumeration, all allowed
385
+                                return true;
386
+                            }
387
+
388
+                            return !empty($currentUserGroups) && !empty(array_intersect(
389
+                                $this->groupManager->getUserGroupIds($user),
390
+                                $currentUserGroups
391
+                            ));
392
+                        });
393
+                    }
394
+
395
+                    $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
396
+                        // is sharing restricted to groups only?
397
+                        if ($restrictGroups !== false) {
398
+                            $userGroups = $this->groupManager->getUserGroupIds($user);
399
+                            if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
400
+                                return $carry;
401
+                            }
402
+                        }
403
+
404
+                        $carry[] = $this->principalPrefix . '/' . $user->getUID();
405
+                        return $carry;
406
+                    }, []);
407
+                    break;
408
+
409
+                case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
410
+                    // If you add support for more search properties that qualify as a user-address,
411
+                    // please also add them to the array below
412
+                    $results[] = $this->searchUserPrincipals([
413
+                        // In theory this should also search for principal:principals/users/...
414
+                        // but that's used internally only anyway and i don't know of any client querying that
415
+                        '{http://sabredav.org/ns}email-address' => $value,
416
+                    ], 'anyof');
417
+                    break;
418
+
419
+                default:
420
+                    $results[] = [];
421
+                    break;
422
+            }
423
+        }
424
+
425
+        // results is an array of arrays, so this is not the first search result
426
+        // but the results of the first searchProperty
427
+        if (count($results) === 1) {
428
+            return $results[0];
429
+        }
430
+
431
+        switch ($test) {
432
+            case 'anyof':
433
+                return array_values(array_unique(array_merge(...$results)));
434
+
435
+            case 'allof':
436
+            default:
437
+                return array_values(array_intersect(...$results));
438
+        }
439
+    }
440
+
441
+    /**
442
+     * @param string $prefixPath
443
+     * @param array $searchProperties
444
+     * @param string $test
445
+     * @return array
446
+     */
447
+    public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
448
+        if (count($searchProperties) === 0) {
449
+            return [];
450
+        }
451
+
452
+        switch ($prefixPath) {
453
+            case 'principals/users':
454
+                return $this->searchUserPrincipals($searchProperties, $test);
455
+
456
+            default:
457
+                return [];
458
+        }
459
+    }
460
+
461
+    /**
462
+     * @param string $uri
463
+     * @param string $principalPrefix
464
+     * @return string
465
+     */
466
+    public function findByUri($uri, $principalPrefix) {
467
+        // If sharing is disabled, return the empty array
468
+        $shareAPIEnabled = $this->shareManager->shareApiEnabled();
469
+        if (!$shareAPIEnabled) {
470
+            return null;
471
+        }
472
+
473
+        // If sharing is restricted to group members only,
474
+        // return only members that have groups in common
475
+        $restrictGroups = false;
476
+        if ($this->shareManager->shareWithGroupMembersOnly()) {
477
+            $user = $this->userSession->getUser();
478
+            if (!$user) {
479
+                return null;
480
+            }
481
+
482
+            $restrictGroups = $this->groupManager->getUserGroupIds($user);
483
+        }
484
+
485
+        if (strpos($uri, 'mailto:') === 0) {
486
+            if ($principalPrefix === 'principals/users') {
487
+                $users = $this->userManager->getByEmail(substr($uri, 7));
488
+                if (count($users) !== 1) {
489
+                    return null;
490
+                }
491
+                $user = $users[0];
492
+
493
+                if ($restrictGroups !== false) {
494
+                    $userGroups = $this->groupManager->getUserGroupIds($user);
495
+                    if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
496
+                        return null;
497
+                    }
498
+                }
499
+
500
+                return $this->principalPrefix . '/' . $user->getUID();
501
+            }
502
+        }
503
+        if (substr($uri, 0, 10) === 'principal:') {
504
+            $principal = substr($uri, 10);
505
+            $principal = $this->getPrincipalByPath($principal);
506
+            if ($principal !== null) {
507
+                return $principal['uri'];
508
+            }
509
+        }
510
+
511
+        return null;
512
+    }
513
+
514
+    /**
515
+     * @param IUser $user
516
+     * @return array
517
+     * @throws PropertyDoesNotExistException
518
+     */
519
+    protected function userToPrincipal($user) {
520
+        $userId = $user->getUID();
521
+        $displayName = $user->getDisplayName();
522
+        $principal = [
523
+            'uri' => $this->principalPrefix . '/' . $userId,
524
+            '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
525
+            '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
526
+            '{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
527
+        ];
528
+
529
+        $account = $this->accountManager->getAccount($user);
530
+        $alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
531
+
532
+        $email = $user->getSystemEMailAddress();
533
+        if (!empty($email)) {
534
+            $principal['{http://sabredav.org/ns}email-address'] = $email;
535
+        }
536
+
537
+        if (!empty($alternativeEmails)) {
538
+            $principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
539
+        }
540
+
541
+        return $principal;
542
+    }
543
+
544
+    public function getPrincipalPrefix() {
545
+        return $this->principalPrefix;
546
+    }
547
+
548
+    /**
549
+     * @param string $circleUniqueId
550
+     * @return array|null
551
+     */
552
+    protected function circleToPrincipal($circleUniqueId) {
553
+        if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
554
+            return null;
555
+        }
556
+
557
+        try {
558
+            $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
559
+        } catch (QueryException $ex) {
560
+            return null;
561
+        } catch (CircleNotFoundException $ex) {
562
+            return null;
563
+        }
564
+
565
+        if (!$circle) {
566
+            return null;
567
+        }
568
+
569
+        $principal = [
570
+            'uri' => 'principals/circles/' . $circleUniqueId,
571
+            '{DAV:}displayname' => $circle->getDisplayName(),
572
+        ];
573
+
574
+        return $principal;
575
+    }
576
+
577
+    /**
578
+     * Returns the list of circles a principal is a member of
579
+     *
580
+     * @param string $principal
581
+     * @return array
582
+     * @throws Exception
583
+     * @throws \OCP\AppFramework\QueryException
584
+     * @suppress PhanUndeclaredClassMethod
585
+     */
586
+    public function getCircleMembership($principal):array {
587
+        if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
588
+            return [];
589
+        }
590
+
591
+        [$prefix, $name] = \Sabre\Uri\split($principal);
592
+        if ($this->hasCircles && $prefix === $this->principalPrefix) {
593
+            $user = $this->userManager->get($name);
594
+            if (!$user) {
595
+                throw new Exception('Principal not found');
596
+            }
597
+
598
+            $circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
599
+
600
+            $circles = array_map(function ($circle) {
601
+                /** @var \OCA\Circles\Model\Circle $circle */
602
+                return 'principals/circles/' . urlencode($circle->getSingleId());
603
+            }, $circles);
604
+
605
+            return $circles;
606
+        }
607
+
608
+        return [];
609
+    }
610
+
611
+    /**
612
+     * Get all email addresses associated to a principal.
613
+     *
614
+     * @param array $principal Data from getPrincipal*()
615
+     * @return string[] All email addresses without the mailto: prefix
616
+     */
617
+    public function getEmailAddressesOfPrincipal(array $principal): array {
618
+        $emailAddresses = [];
619
+
620
+        if (isset($principal['{http://sabredav.org/ns}email-address'])) {
621
+            $emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
622
+        }
623
+
624
+        if (isset($principal['{DAV:}alternate-URI-set'])) {
625
+            foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
626
+                if (str_starts_with($address, 'mailto:')) {
627
+                    $emailAddresses[] = substr($address, 7);
628
+                }
629
+            }
630
+        }
631
+
632
+        if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
633
+            foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
634
+                if (str_starts_with($address, 'mailto:')) {
635
+                    $emailAddresses[] = substr($address, 7);
636
+                }
637
+            }
638
+        }
639
+
640
+        if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
641
+            foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
642
+                if (str_starts_with($address, 'mailto:')) {
643
+                    $emailAddresses[] = substr($address, 7);
644
+                }
645
+            }
646
+        }
647
+
648
+        return array_values(array_unique($emailAddresses));
649
+    }
650 650
 }
Please login to merge, or discard this patch.