Passed
Push — master ( 56ece0...90a628 )
by Joas
18:03 queued 13s
created
lib/private/Accounts/AccountManager.php 1 patch
Indentation   +764 added lines, -764 removed lines patch added patch discarded remove patch
@@ -79,768 +79,768 @@
 block discarded – undo
79 79
  * @package OC\Accounts
80 80
  */
81 81
 class AccountManager implements IAccountManager {
82
-	use TAccountsHelper;
83
-
84
-	use TProfileHelper;
85
-
86
-	/** @var  IDBConnection database connection */
87
-	private $connection;
88
-
89
-	/** @var IConfig */
90
-	private $config;
91
-
92
-	/** @var string table name */
93
-	private $table = 'accounts';
94
-
95
-	/** @var string table name */
96
-	private $dataTable = 'accounts_data';
97
-
98
-	/** @var EventDispatcherInterface */
99
-	private $eventDispatcher;
100
-
101
-	/** @var IJobList */
102
-	private $jobList;
103
-
104
-	/** @var LoggerInterface */
105
-	private $logger;
106
-	/** @var IVerificationToken */
107
-	private $verificationToken;
108
-	/** @var IMailer */
109
-	private $mailer;
110
-	/** @var Defaults */
111
-	private $defaults;
112
-	/** @var IL10N */
113
-	private $l10n;
114
-	/** @var IURLGenerator */
115
-	private $urlGenerator;
116
-	/** @var ICrypto */
117
-	private $crypto;
118
-	/** @var IFactory */
119
-	private $l10nfactory;
120
-	private CappedMemoryCache $internalCache;
121
-
122
-	/**
123
-	 * The list of default scopes for each property.
124
-	 */
125
-	public const DEFAULT_SCOPES = [
126
-		self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
127
-		self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
128
-		self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
129
-		self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
130
-		self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
131
-		self::PROPERTY_PHONE => self::SCOPE_LOCAL,
132
-		self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
133
-		self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
134
-		self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
135
-		self::PROPERTY_ROLE => self::SCOPE_LOCAL,
136
-		self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
137
-		self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
138
-	];
139
-
140
-	public function __construct(
141
-		IDBConnection            $connection,
142
-		IConfig                  $config,
143
-		EventDispatcherInterface $eventDispatcher,
144
-		IJobList                 $jobList,
145
-		LoggerInterface          $logger,
146
-		IVerificationToken       $verificationToken,
147
-		IMailer                  $mailer,
148
-		Defaults                 $defaults,
149
-		IFactory                 $factory,
150
-		IURLGenerator            $urlGenerator,
151
-		ICrypto                  $crypto
152
-	) {
153
-		$this->connection = $connection;
154
-		$this->config = $config;
155
-		$this->eventDispatcher = $eventDispatcher;
156
-		$this->jobList = $jobList;
157
-		$this->logger = $logger;
158
-		$this->verificationToken = $verificationToken;
159
-		$this->mailer = $mailer;
160
-		$this->defaults = $defaults;
161
-		$this->urlGenerator = $urlGenerator;
162
-		$this->crypto = $crypto;
163
-		// DIing IL10N results in a dependency loop
164
-		$this->l10nfactory = $factory;
165
-		$this->internalCache = new CappedMemoryCache();
166
-	}
167
-
168
-	/**
169
-	 * @param string $input
170
-	 * @return string Provided phone number in E.164 format when it was a valid number
171
-	 * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
172
-	 */
173
-	protected function parsePhoneNumber(string $input): string {
174
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
175
-
176
-		if ($defaultRegion === '') {
177
-			// When no default region is set, only +49… numbers are valid
178
-			if (strpos($input, '+') !== 0) {
179
-				throw new InvalidArgumentException(self::PROPERTY_PHONE);
180
-			}
181
-
182
-			$defaultRegion = 'EN';
183
-		}
184
-
185
-		$phoneUtil = PhoneNumberUtil::getInstance();
186
-		try {
187
-			$phoneNumber = $phoneUtil->parse($input, $defaultRegion);
188
-			if ($phoneUtil->isValidNumber($phoneNumber)) {
189
-				return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
190
-			}
191
-		} catch (NumberParseException $e) {
192
-		}
193
-
194
-		throw new InvalidArgumentException(self::PROPERTY_PHONE);
195
-	}
196
-
197
-	/**
198
-	 *
199
-	 * @param string $input
200
-	 * @return string
201
-	 * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
202
-	 */
203
-	protected function parseWebsite(string $input): string {
204
-		$parts = parse_url($input);
205
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
206
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
207
-		}
208
-
209
-		if (!isset($parts['host']) || $parts['host'] === '') {
210
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
211
-		}
212
-
213
-		return $input;
214
-	}
215
-
216
-	/**
217
-	 * @param IAccountProperty[] $properties
218
-	 */
219
-	protected function testValueLengths(array $properties, bool $throwOnData = false): void {
220
-		foreach ($properties as $property) {
221
-			if (strlen($property->getValue()) > 2048) {
222
-				if ($throwOnData) {
223
-					throw new InvalidArgumentException($property->getName());
224
-				} else {
225
-					$property->setValue('');
226
-				}
227
-			}
228
-		}
229
-	}
230
-
231
-	protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
232
-		if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
233
-			throw new InvalidArgumentException('scope');
234
-		}
235
-
236
-		if (
237
-			$property->getScope() === self::SCOPE_PRIVATE
238
-			&& in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
239
-		) {
240
-			if ($throwOnData) {
241
-				// v2-private is not available for these fields
242
-				throw new InvalidArgumentException('scope');
243
-			} else {
244
-				// default to local
245
-				$property->setScope(self::SCOPE_LOCAL);
246
-			}
247
-		} else {
248
-			// migrate scope values to the new format
249
-			// invalid scopes are mapped to a default value
250
-			$property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
251
-		}
252
-	}
253
-
254
-	protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) {
255
-		if ($property->getName() !== self::PROPERTY_PHONE) {
256
-			if ($throwOnData) {
257
-				throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName()));
258
-			}
259
-			return;
260
-		}
261
-		if ($property->getValue() === '') {
262
-			return;
263
-		}
264
-		try {
265
-			$property->setValue($this->parsePhoneNumber($property->getValue()));
266
-		} catch (InvalidArgumentException $e) {
267
-			if ($throwOnData) {
268
-				throw $e;
269
-			}
270
-			$property->setValue('');
271
-		}
272
-	}
273
-
274
-	protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) {
275
-		if ($property->getName() !== self::PROPERTY_WEBSITE) {
276
-			if ($throwOnData) {
277
-				throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName()));
278
-			}
279
-		}
280
-		try {
281
-			$property->setValue($this->parseWebsite($property->getValue()));
282
-		} catch (InvalidArgumentException $e) {
283
-			if ($throwOnData) {
284
-				throw $e;
285
-			}
286
-			$property->setValue('');
287
-		}
288
-	}
289
-
290
-	protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
291
-		if ($oldUserData === null) {
292
-			$oldUserData = $this->getUser($user, false);
293
-		}
294
-
295
-		$updated = true;
296
-
297
-		if ($oldUserData !== $data) {
298
-			$this->updateExistingUser($user, $data, $oldUserData);
299
-		} else {
300
-			// nothing needs to be done if new and old data set are the same
301
-			$updated = false;
302
-		}
303
-
304
-		if ($updated) {
305
-			$this->eventDispatcher->dispatch(
306
-				'OC\AccountManager::userUpdated',
307
-				new GenericEvent($user, $data)
308
-			);
309
-		}
310
-
311
-		return $data;
312
-	}
313
-
314
-	/**
315
-	 * delete user from accounts table
316
-	 *
317
-	 * @param IUser $user
318
-	 */
319
-	public function deleteUser(IUser $user) {
320
-		$uid = $user->getUID();
321
-		$query = $this->connection->getQueryBuilder();
322
-		$query->delete($this->table)
323
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
324
-			->execute();
325
-
326
-		$this->deleteUserData($user);
327
-	}
328
-
329
-	/**
330
-	 * delete user from accounts table
331
-	 *
332
-	 * @param IUser $user
333
-	 */
334
-	public function deleteUserData(IUser $user): void {
335
-		$uid = $user->getUID();
336
-		$query = $this->connection->getQueryBuilder();
337
-		$query->delete($this->dataTable)
338
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
339
-			->execute();
340
-	}
341
-
342
-	/**
343
-	 * get stored data from a given user
344
-	 */
345
-	protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
346
-		$uid = $user->getUID();
347
-		$query = $this->connection->getQueryBuilder();
348
-		$query->select('data')
349
-			->from($this->table)
350
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
351
-			->setParameter('uid', $uid);
352
-		$result = $query->executeQuery();
353
-		$accountData = $result->fetchAll();
354
-		$result->closeCursor();
355
-
356
-		if (empty($accountData)) {
357
-			$userData = $this->buildDefaultUserRecord($user);
358
-			if ($insertIfNotExists) {
359
-				$this->insertNewUser($user, $userData);
360
-			}
361
-			return $userData;
362
-		}
363
-
364
-		$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
365
-		if ($userDataArray === null || $userDataArray === []) {
366
-			return $this->buildDefaultUserRecord($user);
367
-		}
368
-
369
-		return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
370
-	}
371
-
372
-	public function searchUsers(string $property, array $values): array {
373
-		// the value col is limited to 255 bytes. It is used for searches only.
374
-		$values = array_map(function (string $value) {
375
-			return Util::shortenMultibyteString($value, 255);
376
-		}, $values);
377
-		$chunks = array_chunk($values, 500);
378
-		$query = $this->connection->getQueryBuilder();
379
-		$query->select('*')
380
-			->from($this->dataTable)
381
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
382
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
383
-
384
-		$matches = [];
385
-		foreach ($chunks as $chunk) {
386
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
387
-			$result = $query->executeQuery();
388
-
389
-			while ($row = $result->fetch()) {
390
-				$matches[$row['uid']] = $row['value'];
391
-			}
392
-			$result->closeCursor();
393
-		}
394
-
395
-		$result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
396
-
397
-		return array_flip($result);
398
-	}
399
-
400
-	protected function searchUsersForRelatedCollection(string $property, array $values): array {
401
-		switch ($property) {
402
-			case IAccountManager::PROPERTY_EMAIL:
403
-				return array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values));
404
-			default:
405
-				return [];
406
-		}
407
-	}
408
-
409
-	/**
410
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
411
-	 */
412
-	protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
413
-		try {
414
-			$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
415
-		} catch (PropertyDoesNotExistException $e) {
416
-			return;
417
-		}
418
-
419
-		$oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
420
-		$oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
421
-
422
-		if ($oldMail !== $property->getValue()) {
423
-			$this->jobList->add(
424
-				VerifyUserData::class,
425
-				[
426
-					'verificationCode' => '',
427
-					'data' => $property->getValue(),
428
-					'type' => self::PROPERTY_EMAIL,
429
-					'uid' => $updatedAccount->getUser()->getUID(),
430
-					'try' => 0,
431
-					'lastRun' => time()
432
-				]
433
-			);
434
-
435
-			$property->setVerified(self::VERIFICATION_IN_PROGRESS);
436
-		}
437
-	}
438
-
439
-	protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
440
-		$mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
441
-		foreach ($mailCollection->getProperties() as $property) {
442
-			if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
443
-				continue;
444
-			}
445
-			if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
446
-				$property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
447
-			}
448
-		}
449
-	}
450
-
451
-	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
452
-		$ref = \substr(hash('sha256', $email), 0, 8);
453
-		$key = $this->crypto->encrypt($email);
454
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
455
-
456
-		$link = $this->urlGenerator->linkToRouteAbsolute(
457
-			'provisioning_api.Verification.verifyMail',
458
-			[
459
-				'userId' => $user->getUID(),
460
-				'token' => $token,
461
-				'key' => $key
462
-			]
463
-		);
464
-
465
-		$emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
466
-			'link' => $link,
467
-		]);
468
-
469
-		if (!$this->l10n) {
470
-			$this->l10n = $this->l10nfactory->get('core');
471
-		}
472
-
473
-		$emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
474
-		$emailTemplate->addHeader();
475
-		$emailTemplate->addHeading($this->l10n->t('Email verification'));
476
-
477
-		$emailTemplate->addBodyText(
478
-			htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
479
-			$this->l10n->t('Click the following link to confirm your email.')
480
-		);
481
-
482
-		$emailTemplate->addBodyButton(
483
-			htmlspecialchars($this->l10n->t('Confirm your email')),
484
-			$link,
485
-			false
486
-		);
487
-		$emailTemplate->addFooter();
488
-
489
-		try {
490
-			$message = $this->mailer->createMessage();
491
-			$message->setTo([$email => $user->getDisplayName()]);
492
-			$message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
493
-			$message->useTemplate($emailTemplate);
494
-			$this->mailer->send($message);
495
-		} catch (Exception $e) {
496
-			// Log the exception and continue
497
-			$this->logger->info('Failed to send verification mail', [
498
-				'app' => 'core',
499
-				'exception' => $e
500
-			]);
501
-			return false;
502
-		}
503
-		return true;
504
-	}
505
-
506
-	/**
507
-	 * Make sure that all expected data are set
508
-	 */
509
-	protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
510
-		foreach ($defaultUserData as $defaultDataItem) {
511
-			// If property does not exist, initialize it
512
-			$userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
513
-			if ($userDataIndex === false) {
514
-				$userData[] = $defaultDataItem;
515
-				continue;
516
-			}
517
-
518
-			// Merge and extend default missing values
519
-			$userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
520
-		}
521
-
522
-		return $userData;
523
-	}
524
-
525
-	protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
526
-		static $propertiesVerifiableByLookupServer = [
527
-			self::PROPERTY_TWITTER,
528
-			self::PROPERTY_FEDIVERSE,
529
-			self::PROPERTY_WEBSITE,
530
-			self::PROPERTY_EMAIL,
531
-		];
532
-
533
-		foreach ($propertiesVerifiableByLookupServer as $propertyName) {
534
-			try {
535
-				$property = $updatedAccount->getProperty($propertyName);
536
-			} catch (PropertyDoesNotExistException $e) {
537
-				continue;
538
-			}
539
-			$wasVerified = isset($oldData[$propertyName])
540
-				&& isset($oldData[$propertyName]['verified'])
541
-				&& $oldData[$propertyName]['verified'] === self::VERIFIED;
542
-			if ((!isset($oldData[$propertyName])
543
-					|| !isset($oldData[$propertyName]['value'])
544
-					|| $property->getValue() !== $oldData[$propertyName]['value'])
545
-				&& ($property->getVerified() !== self::NOT_VERIFIED
546
-					|| $wasVerified)
547
-			) {
548
-				$property->setVerified(self::NOT_VERIFIED);
549
-			}
550
-		}
551
-	}
552
-
553
-	/**
554
-	 * add new user to accounts table
555
-	 *
556
-	 * @param IUser $user
557
-	 * @param array $data
558
-	 */
559
-	protected function insertNewUser(IUser $user, array $data): void {
560
-		$uid = $user->getUID();
561
-		$jsonEncodedData = $this->prepareJson($data);
562
-		$query = $this->connection->getQueryBuilder();
563
-		$query->insert($this->table)
564
-			->values(
565
-				[
566
-					'uid' => $query->createNamedParameter($uid),
567
-					'data' => $query->createNamedParameter($jsonEncodedData),
568
-				]
569
-			)
570
-			->executeStatement();
571
-
572
-		$this->deleteUserData($user);
573
-		$this->writeUserData($user, $data);
574
-	}
575
-
576
-	protected function prepareJson(array $data): string {
577
-		$preparedData = [];
578
-		foreach ($data as $dataRow) {
579
-			$propertyName = $dataRow['name'];
580
-			unset($dataRow['name']);
581
-
582
-			if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
583
-				// do not write default value, save DB space
584
-				unset($dataRow['locallyVerified']);
585
-			}
586
-
587
-			if (!$this->isCollection($propertyName)) {
588
-				$preparedData[$propertyName] = $dataRow;
589
-				continue;
590
-			}
591
-			if (!isset($preparedData[$propertyName])) {
592
-				$preparedData[$propertyName] = [];
593
-			}
594
-			$preparedData[$propertyName][] = $dataRow;
595
-		}
596
-		return json_encode($preparedData);
597
-	}
598
-
599
-	protected function importFromJson(string $json, string $userId): ?array {
600
-		$result = [];
601
-		$jsonArray = json_decode($json, true);
602
-		$jsonError = json_last_error();
603
-		if ($jsonError !== JSON_ERROR_NONE) {
604
-			$this->logger->critical(
605
-				'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
606
-				[
607
-					'uid' => $userId,
608
-					'json_error' => $jsonError
609
-				]
610
-			);
611
-			return null;
612
-		}
613
-		foreach ($jsonArray as $propertyName => $row) {
614
-			if (!$this->isCollection($propertyName)) {
615
-				$result[] = array_merge($row, ['name' => $propertyName]);
616
-				continue;
617
-			}
618
-			foreach ($row as $singleRow) {
619
-				$result[] = array_merge($singleRow, ['name' => $propertyName]);
620
-			}
621
-		}
622
-		return $result;
623
-	}
624
-
625
-	/**
626
-	 * Update existing user in accounts table
627
-	 */
628
-	protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
629
-		$uid = $user->getUID();
630
-		$jsonEncodedData = $this->prepareJson($data);
631
-		$query = $this->connection->getQueryBuilder();
632
-		$query->update($this->table)
633
-			->set('data', $query->createNamedParameter($jsonEncodedData))
634
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
635
-			->executeStatement();
636
-
637
-		$this->deleteUserData($user);
638
-		$this->writeUserData($user, $data);
639
-	}
640
-
641
-	protected function writeUserData(IUser $user, array $data): void {
642
-		$query = $this->connection->getQueryBuilder();
643
-		$query->insert($this->dataTable)
644
-			->values(
645
-				[
646
-					'uid' => $query->createNamedParameter($user->getUID()),
647
-					'name' => $query->createParameter('name'),
648
-					'value' => $query->createParameter('value'),
649
-				]
650
-			);
651
-		$this->writeUserDataProperties($query, $data);
652
-	}
653
-
654
-	protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
655
-		foreach ($data as $property) {
656
-			if ($property['name'] === self::PROPERTY_AVATAR) {
657
-				continue;
658
-			}
659
-
660
-			// the value col is limited to 255 bytes. It is used for searches only.
661
-			$value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
662
-
663
-			$query->setParameter('name', $property['name'])
664
-				->setParameter('value', $value);
665
-			$query->executeStatement();
666
-		}
667
-	}
668
-
669
-	/**
670
-	 * build default user record in case not data set exists yet
671
-	 */
672
-	protected function buildDefaultUserRecord(IUser $user): array {
673
-		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
674
-			return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
675
-		}, ARRAY_FILTER_USE_BOTH));
676
-
677
-		return [
678
-			[
679
-				'name' => self::PROPERTY_DISPLAYNAME,
680
-				'value' => $user->getDisplayName(),
681
-				// Display name must be at least SCOPE_LOCAL
682
-				'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
683
-				'verified' => self::NOT_VERIFIED,
684
-			],
685
-
686
-			[
687
-				'name' => self::PROPERTY_ADDRESS,
688
-				'value' => '',
689
-				'scope' => $scopes[self::PROPERTY_ADDRESS],
690
-				'verified' => self::NOT_VERIFIED,
691
-			],
692
-
693
-			[
694
-				'name' => self::PROPERTY_WEBSITE,
695
-				'value' => '',
696
-				'scope' => $scopes[self::PROPERTY_WEBSITE],
697
-				'verified' => self::NOT_VERIFIED,
698
-			],
699
-
700
-			[
701
-				'name' => self::PROPERTY_EMAIL,
702
-				'value' => $user->getEMailAddress(),
703
-				// Email must be at least SCOPE_LOCAL
704
-				'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
705
-				'verified' => self::NOT_VERIFIED,
706
-			],
707
-
708
-			[
709
-				'name' => self::PROPERTY_AVATAR,
710
-				'scope' => $scopes[self::PROPERTY_AVATAR],
711
-			],
712
-
713
-			[
714
-				'name' => self::PROPERTY_PHONE,
715
-				'value' => '',
716
-				'scope' => $scopes[self::PROPERTY_PHONE],
717
-				'verified' => self::NOT_VERIFIED,
718
-			],
719
-
720
-			[
721
-				'name' => self::PROPERTY_TWITTER,
722
-				'value' => '',
723
-				'scope' => $scopes[self::PROPERTY_TWITTER],
724
-				'verified' => self::NOT_VERIFIED,
725
-			],
726
-
727
-			[
728
-				'name' => self::PROPERTY_FEDIVERSE,
729
-				'value' => '',
730
-				'scope' => $scopes[self::PROPERTY_FEDIVERSE],
731
-				'verified' => self::NOT_VERIFIED,
732
-			],
733
-
734
-			[
735
-				'name' => self::PROPERTY_ORGANISATION,
736
-				'value' => '',
737
-				'scope' => $scopes[self::PROPERTY_ORGANISATION],
738
-			],
739
-
740
-			[
741
-				'name' => self::PROPERTY_ROLE,
742
-				'value' => '',
743
-				'scope' => $scopes[self::PROPERTY_ROLE],
744
-			],
745
-
746
-			[
747
-				'name' => self::PROPERTY_HEADLINE,
748
-				'value' => '',
749
-				'scope' => $scopes[self::PROPERTY_HEADLINE],
750
-			],
751
-
752
-			[
753
-				'name' => self::PROPERTY_BIOGRAPHY,
754
-				'value' => '',
755
-				'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
756
-			],
757
-
758
-			[
759
-				'name' => self::PROPERTY_PROFILE_ENABLED,
760
-				'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
761
-			],
762
-		];
763
-	}
764
-
765
-	private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
766
-		$collection = $account->getPropertyCollection($data['name']);
767
-
768
-		$p = new AccountProperty(
769
-			$data['name'],
770
-			$data['value'] ?? '',
771
-			$data['scope'] ?? self::SCOPE_LOCAL,
772
-			$data['verified'] ?? self::NOT_VERIFIED,
773
-			''
774
-		);
775
-		$p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
776
-		$collection->addProperty($p);
777
-
778
-		return $collection;
779
-	}
780
-
781
-	private function parseAccountData(IUser $user, $data): Account {
782
-		$account = new Account($user);
783
-		foreach ($data as $accountData) {
784
-			if ($this->isCollection($accountData['name'])) {
785
-				$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
786
-			} else {
787
-				$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
788
-				if (isset($accountData['locallyVerified'])) {
789
-					$property = $account->getProperty($accountData['name']);
790
-					$property->setLocallyVerified($accountData['locallyVerified']);
791
-				}
792
-			}
793
-		}
794
-		return $account;
795
-	}
796
-
797
-	public function getAccount(IUser $user): IAccount {
798
-		if ($this->internalCache->hasKey($user->getUID())) {
799
-			return $this->internalCache->get($user->getUID());
800
-		}
801
-		$account = $this->parseAccountData($user, $this->getUser($user));
802
-		$this->internalCache->set($user->getUID(), $account);
803
-		return $account;
804
-	}
805
-
806
-	public function updateAccount(IAccount $account): void {
807
-		$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
808
-		try {
809
-			$property = $account->getProperty(self::PROPERTY_PHONE);
810
-			$this->sanitizePhoneNumberValue($property);
811
-		} catch (PropertyDoesNotExistException $e) {
812
-			//  valid case, nothing to do
813
-		}
814
-
815
-		try {
816
-			$property = $account->getProperty(self::PROPERTY_WEBSITE);
817
-			$this->sanitizeWebsite($property);
818
-		} catch (PropertyDoesNotExistException $e) {
819
-			//  valid case, nothing to do
820
-		}
821
-
822
-		foreach ($account->getAllProperties() as $property) {
823
-			$this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
824
-		}
825
-
826
-		$oldData = $this->getUser($account->getUser(), false);
827
-		$this->updateVerificationStatus($account, $oldData);
828
-		$this->checkEmailVerification($account, $oldData);
829
-		$this->checkLocalEmailVerification($account, $oldData);
830
-
831
-		$data = [];
832
-		foreach ($account->getAllProperties() as $property) {
833
-			/** @var IAccountProperty $property */
834
-			$data[] = [
835
-				'name' => $property->getName(),
836
-				'value' => $property->getValue(),
837
-				'scope' => $property->getScope(),
838
-				'verified' => $property->getVerified(),
839
-				'locallyVerified' => $property->getLocallyVerified(),
840
-			];
841
-		}
842
-
843
-		$this->updateUser($account->getUser(), $data, $oldData, true);
844
-		$this->internalCache->set($account->getUser()->getUID(), $account);
845
-	}
82
+    use TAccountsHelper;
83
+
84
+    use TProfileHelper;
85
+
86
+    /** @var  IDBConnection database connection */
87
+    private $connection;
88
+
89
+    /** @var IConfig */
90
+    private $config;
91
+
92
+    /** @var string table name */
93
+    private $table = 'accounts';
94
+
95
+    /** @var string table name */
96
+    private $dataTable = 'accounts_data';
97
+
98
+    /** @var EventDispatcherInterface */
99
+    private $eventDispatcher;
100
+
101
+    /** @var IJobList */
102
+    private $jobList;
103
+
104
+    /** @var LoggerInterface */
105
+    private $logger;
106
+    /** @var IVerificationToken */
107
+    private $verificationToken;
108
+    /** @var IMailer */
109
+    private $mailer;
110
+    /** @var Defaults */
111
+    private $defaults;
112
+    /** @var IL10N */
113
+    private $l10n;
114
+    /** @var IURLGenerator */
115
+    private $urlGenerator;
116
+    /** @var ICrypto */
117
+    private $crypto;
118
+    /** @var IFactory */
119
+    private $l10nfactory;
120
+    private CappedMemoryCache $internalCache;
121
+
122
+    /**
123
+     * The list of default scopes for each property.
124
+     */
125
+    public const DEFAULT_SCOPES = [
126
+        self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
127
+        self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
128
+        self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
129
+        self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
130
+        self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
131
+        self::PROPERTY_PHONE => self::SCOPE_LOCAL,
132
+        self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
133
+        self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
134
+        self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
135
+        self::PROPERTY_ROLE => self::SCOPE_LOCAL,
136
+        self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
137
+        self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
138
+    ];
139
+
140
+    public function __construct(
141
+        IDBConnection            $connection,
142
+        IConfig                  $config,
143
+        EventDispatcherInterface $eventDispatcher,
144
+        IJobList                 $jobList,
145
+        LoggerInterface          $logger,
146
+        IVerificationToken       $verificationToken,
147
+        IMailer                  $mailer,
148
+        Defaults                 $defaults,
149
+        IFactory                 $factory,
150
+        IURLGenerator            $urlGenerator,
151
+        ICrypto                  $crypto
152
+    ) {
153
+        $this->connection = $connection;
154
+        $this->config = $config;
155
+        $this->eventDispatcher = $eventDispatcher;
156
+        $this->jobList = $jobList;
157
+        $this->logger = $logger;
158
+        $this->verificationToken = $verificationToken;
159
+        $this->mailer = $mailer;
160
+        $this->defaults = $defaults;
161
+        $this->urlGenerator = $urlGenerator;
162
+        $this->crypto = $crypto;
163
+        // DIing IL10N results in a dependency loop
164
+        $this->l10nfactory = $factory;
165
+        $this->internalCache = new CappedMemoryCache();
166
+    }
167
+
168
+    /**
169
+     * @param string $input
170
+     * @return string Provided phone number in E.164 format when it was a valid number
171
+     * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
172
+     */
173
+    protected function parsePhoneNumber(string $input): string {
174
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
175
+
176
+        if ($defaultRegion === '') {
177
+            // When no default region is set, only +49… numbers are valid
178
+            if (strpos($input, '+') !== 0) {
179
+                throw new InvalidArgumentException(self::PROPERTY_PHONE);
180
+            }
181
+
182
+            $defaultRegion = 'EN';
183
+        }
184
+
185
+        $phoneUtil = PhoneNumberUtil::getInstance();
186
+        try {
187
+            $phoneNumber = $phoneUtil->parse($input, $defaultRegion);
188
+            if ($phoneUtil->isValidNumber($phoneNumber)) {
189
+                return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
190
+            }
191
+        } catch (NumberParseException $e) {
192
+        }
193
+
194
+        throw new InvalidArgumentException(self::PROPERTY_PHONE);
195
+    }
196
+
197
+    /**
198
+     *
199
+     * @param string $input
200
+     * @return string
201
+     * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
202
+     */
203
+    protected function parseWebsite(string $input): string {
204
+        $parts = parse_url($input);
205
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
206
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
207
+        }
208
+
209
+        if (!isset($parts['host']) || $parts['host'] === '') {
210
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
211
+        }
212
+
213
+        return $input;
214
+    }
215
+
216
+    /**
217
+     * @param IAccountProperty[] $properties
218
+     */
219
+    protected function testValueLengths(array $properties, bool $throwOnData = false): void {
220
+        foreach ($properties as $property) {
221
+            if (strlen($property->getValue()) > 2048) {
222
+                if ($throwOnData) {
223
+                    throw new InvalidArgumentException($property->getName());
224
+                } else {
225
+                    $property->setValue('');
226
+                }
227
+            }
228
+        }
229
+    }
230
+
231
+    protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
232
+        if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
233
+            throw new InvalidArgumentException('scope');
234
+        }
235
+
236
+        if (
237
+            $property->getScope() === self::SCOPE_PRIVATE
238
+            && in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
239
+        ) {
240
+            if ($throwOnData) {
241
+                // v2-private is not available for these fields
242
+                throw new InvalidArgumentException('scope');
243
+            } else {
244
+                // default to local
245
+                $property->setScope(self::SCOPE_LOCAL);
246
+            }
247
+        } else {
248
+            // migrate scope values to the new format
249
+            // invalid scopes are mapped to a default value
250
+            $property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
251
+        }
252
+    }
253
+
254
+    protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) {
255
+        if ($property->getName() !== self::PROPERTY_PHONE) {
256
+            if ($throwOnData) {
257
+                throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName()));
258
+            }
259
+            return;
260
+        }
261
+        if ($property->getValue() === '') {
262
+            return;
263
+        }
264
+        try {
265
+            $property->setValue($this->parsePhoneNumber($property->getValue()));
266
+        } catch (InvalidArgumentException $e) {
267
+            if ($throwOnData) {
268
+                throw $e;
269
+            }
270
+            $property->setValue('');
271
+        }
272
+    }
273
+
274
+    protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) {
275
+        if ($property->getName() !== self::PROPERTY_WEBSITE) {
276
+            if ($throwOnData) {
277
+                throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName()));
278
+            }
279
+        }
280
+        try {
281
+            $property->setValue($this->parseWebsite($property->getValue()));
282
+        } catch (InvalidArgumentException $e) {
283
+            if ($throwOnData) {
284
+                throw $e;
285
+            }
286
+            $property->setValue('');
287
+        }
288
+    }
289
+
290
+    protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
291
+        if ($oldUserData === null) {
292
+            $oldUserData = $this->getUser($user, false);
293
+        }
294
+
295
+        $updated = true;
296
+
297
+        if ($oldUserData !== $data) {
298
+            $this->updateExistingUser($user, $data, $oldUserData);
299
+        } else {
300
+            // nothing needs to be done if new and old data set are the same
301
+            $updated = false;
302
+        }
303
+
304
+        if ($updated) {
305
+            $this->eventDispatcher->dispatch(
306
+                'OC\AccountManager::userUpdated',
307
+                new GenericEvent($user, $data)
308
+            );
309
+        }
310
+
311
+        return $data;
312
+    }
313
+
314
+    /**
315
+     * delete user from accounts table
316
+     *
317
+     * @param IUser $user
318
+     */
319
+    public function deleteUser(IUser $user) {
320
+        $uid = $user->getUID();
321
+        $query = $this->connection->getQueryBuilder();
322
+        $query->delete($this->table)
323
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
324
+            ->execute();
325
+
326
+        $this->deleteUserData($user);
327
+    }
328
+
329
+    /**
330
+     * delete user from accounts table
331
+     *
332
+     * @param IUser $user
333
+     */
334
+    public function deleteUserData(IUser $user): void {
335
+        $uid = $user->getUID();
336
+        $query = $this->connection->getQueryBuilder();
337
+        $query->delete($this->dataTable)
338
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
339
+            ->execute();
340
+    }
341
+
342
+    /**
343
+     * get stored data from a given user
344
+     */
345
+    protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
346
+        $uid = $user->getUID();
347
+        $query = $this->connection->getQueryBuilder();
348
+        $query->select('data')
349
+            ->from($this->table)
350
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
351
+            ->setParameter('uid', $uid);
352
+        $result = $query->executeQuery();
353
+        $accountData = $result->fetchAll();
354
+        $result->closeCursor();
355
+
356
+        if (empty($accountData)) {
357
+            $userData = $this->buildDefaultUserRecord($user);
358
+            if ($insertIfNotExists) {
359
+                $this->insertNewUser($user, $userData);
360
+            }
361
+            return $userData;
362
+        }
363
+
364
+        $userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
365
+        if ($userDataArray === null || $userDataArray === []) {
366
+            return $this->buildDefaultUserRecord($user);
367
+        }
368
+
369
+        return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
370
+    }
371
+
372
+    public function searchUsers(string $property, array $values): array {
373
+        // the value col is limited to 255 bytes. It is used for searches only.
374
+        $values = array_map(function (string $value) {
375
+            return Util::shortenMultibyteString($value, 255);
376
+        }, $values);
377
+        $chunks = array_chunk($values, 500);
378
+        $query = $this->connection->getQueryBuilder();
379
+        $query->select('*')
380
+            ->from($this->dataTable)
381
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
382
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
383
+
384
+        $matches = [];
385
+        foreach ($chunks as $chunk) {
386
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
387
+            $result = $query->executeQuery();
388
+
389
+            while ($row = $result->fetch()) {
390
+                $matches[$row['uid']] = $row['value'];
391
+            }
392
+            $result->closeCursor();
393
+        }
394
+
395
+        $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
396
+
397
+        return array_flip($result);
398
+    }
399
+
400
+    protected function searchUsersForRelatedCollection(string $property, array $values): array {
401
+        switch ($property) {
402
+            case IAccountManager::PROPERTY_EMAIL:
403
+                return array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values));
404
+            default:
405
+                return [];
406
+        }
407
+    }
408
+
409
+    /**
410
+     * check if we need to ask the server for email verification, if yes we create a cronjob
411
+     */
412
+    protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
413
+        try {
414
+            $property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
415
+        } catch (PropertyDoesNotExistException $e) {
416
+            return;
417
+        }
418
+
419
+        $oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
420
+        $oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
421
+
422
+        if ($oldMail !== $property->getValue()) {
423
+            $this->jobList->add(
424
+                VerifyUserData::class,
425
+                [
426
+                    'verificationCode' => '',
427
+                    'data' => $property->getValue(),
428
+                    'type' => self::PROPERTY_EMAIL,
429
+                    'uid' => $updatedAccount->getUser()->getUID(),
430
+                    'try' => 0,
431
+                    'lastRun' => time()
432
+                ]
433
+            );
434
+
435
+            $property->setVerified(self::VERIFICATION_IN_PROGRESS);
436
+        }
437
+    }
438
+
439
+    protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
440
+        $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
441
+        foreach ($mailCollection->getProperties() as $property) {
442
+            if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
443
+                continue;
444
+            }
445
+            if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
446
+                $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
447
+            }
448
+        }
449
+    }
450
+
451
+    protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
452
+        $ref = \substr(hash('sha256', $email), 0, 8);
453
+        $key = $this->crypto->encrypt($email);
454
+        $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
455
+
456
+        $link = $this->urlGenerator->linkToRouteAbsolute(
457
+            'provisioning_api.Verification.verifyMail',
458
+            [
459
+                'userId' => $user->getUID(),
460
+                'token' => $token,
461
+                'key' => $key
462
+            ]
463
+        );
464
+
465
+        $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
466
+            'link' => $link,
467
+        ]);
468
+
469
+        if (!$this->l10n) {
470
+            $this->l10n = $this->l10nfactory->get('core');
471
+        }
472
+
473
+        $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
474
+        $emailTemplate->addHeader();
475
+        $emailTemplate->addHeading($this->l10n->t('Email verification'));
476
+
477
+        $emailTemplate->addBodyText(
478
+            htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
479
+            $this->l10n->t('Click the following link to confirm your email.')
480
+        );
481
+
482
+        $emailTemplate->addBodyButton(
483
+            htmlspecialchars($this->l10n->t('Confirm your email')),
484
+            $link,
485
+            false
486
+        );
487
+        $emailTemplate->addFooter();
488
+
489
+        try {
490
+            $message = $this->mailer->createMessage();
491
+            $message->setTo([$email => $user->getDisplayName()]);
492
+            $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
493
+            $message->useTemplate($emailTemplate);
494
+            $this->mailer->send($message);
495
+        } catch (Exception $e) {
496
+            // Log the exception and continue
497
+            $this->logger->info('Failed to send verification mail', [
498
+                'app' => 'core',
499
+                'exception' => $e
500
+            ]);
501
+            return false;
502
+        }
503
+        return true;
504
+    }
505
+
506
+    /**
507
+     * Make sure that all expected data are set
508
+     */
509
+    protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
510
+        foreach ($defaultUserData as $defaultDataItem) {
511
+            // If property does not exist, initialize it
512
+            $userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
513
+            if ($userDataIndex === false) {
514
+                $userData[] = $defaultDataItem;
515
+                continue;
516
+            }
517
+
518
+            // Merge and extend default missing values
519
+            $userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
520
+        }
521
+
522
+        return $userData;
523
+    }
524
+
525
+    protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
526
+        static $propertiesVerifiableByLookupServer = [
527
+            self::PROPERTY_TWITTER,
528
+            self::PROPERTY_FEDIVERSE,
529
+            self::PROPERTY_WEBSITE,
530
+            self::PROPERTY_EMAIL,
531
+        ];
532
+
533
+        foreach ($propertiesVerifiableByLookupServer as $propertyName) {
534
+            try {
535
+                $property = $updatedAccount->getProperty($propertyName);
536
+            } catch (PropertyDoesNotExistException $e) {
537
+                continue;
538
+            }
539
+            $wasVerified = isset($oldData[$propertyName])
540
+                && isset($oldData[$propertyName]['verified'])
541
+                && $oldData[$propertyName]['verified'] === self::VERIFIED;
542
+            if ((!isset($oldData[$propertyName])
543
+                    || !isset($oldData[$propertyName]['value'])
544
+                    || $property->getValue() !== $oldData[$propertyName]['value'])
545
+                && ($property->getVerified() !== self::NOT_VERIFIED
546
+                    || $wasVerified)
547
+            ) {
548
+                $property->setVerified(self::NOT_VERIFIED);
549
+            }
550
+        }
551
+    }
552
+
553
+    /**
554
+     * add new user to accounts table
555
+     *
556
+     * @param IUser $user
557
+     * @param array $data
558
+     */
559
+    protected function insertNewUser(IUser $user, array $data): void {
560
+        $uid = $user->getUID();
561
+        $jsonEncodedData = $this->prepareJson($data);
562
+        $query = $this->connection->getQueryBuilder();
563
+        $query->insert($this->table)
564
+            ->values(
565
+                [
566
+                    'uid' => $query->createNamedParameter($uid),
567
+                    'data' => $query->createNamedParameter($jsonEncodedData),
568
+                ]
569
+            )
570
+            ->executeStatement();
571
+
572
+        $this->deleteUserData($user);
573
+        $this->writeUserData($user, $data);
574
+    }
575
+
576
+    protected function prepareJson(array $data): string {
577
+        $preparedData = [];
578
+        foreach ($data as $dataRow) {
579
+            $propertyName = $dataRow['name'];
580
+            unset($dataRow['name']);
581
+
582
+            if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
583
+                // do not write default value, save DB space
584
+                unset($dataRow['locallyVerified']);
585
+            }
586
+
587
+            if (!$this->isCollection($propertyName)) {
588
+                $preparedData[$propertyName] = $dataRow;
589
+                continue;
590
+            }
591
+            if (!isset($preparedData[$propertyName])) {
592
+                $preparedData[$propertyName] = [];
593
+            }
594
+            $preparedData[$propertyName][] = $dataRow;
595
+        }
596
+        return json_encode($preparedData);
597
+    }
598
+
599
+    protected function importFromJson(string $json, string $userId): ?array {
600
+        $result = [];
601
+        $jsonArray = json_decode($json, true);
602
+        $jsonError = json_last_error();
603
+        if ($jsonError !== JSON_ERROR_NONE) {
604
+            $this->logger->critical(
605
+                'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
606
+                [
607
+                    'uid' => $userId,
608
+                    'json_error' => $jsonError
609
+                ]
610
+            );
611
+            return null;
612
+        }
613
+        foreach ($jsonArray as $propertyName => $row) {
614
+            if (!$this->isCollection($propertyName)) {
615
+                $result[] = array_merge($row, ['name' => $propertyName]);
616
+                continue;
617
+            }
618
+            foreach ($row as $singleRow) {
619
+                $result[] = array_merge($singleRow, ['name' => $propertyName]);
620
+            }
621
+        }
622
+        return $result;
623
+    }
624
+
625
+    /**
626
+     * Update existing user in accounts table
627
+     */
628
+    protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
629
+        $uid = $user->getUID();
630
+        $jsonEncodedData = $this->prepareJson($data);
631
+        $query = $this->connection->getQueryBuilder();
632
+        $query->update($this->table)
633
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
634
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
635
+            ->executeStatement();
636
+
637
+        $this->deleteUserData($user);
638
+        $this->writeUserData($user, $data);
639
+    }
640
+
641
+    protected function writeUserData(IUser $user, array $data): void {
642
+        $query = $this->connection->getQueryBuilder();
643
+        $query->insert($this->dataTable)
644
+            ->values(
645
+                [
646
+                    'uid' => $query->createNamedParameter($user->getUID()),
647
+                    'name' => $query->createParameter('name'),
648
+                    'value' => $query->createParameter('value'),
649
+                ]
650
+            );
651
+        $this->writeUserDataProperties($query, $data);
652
+    }
653
+
654
+    protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
655
+        foreach ($data as $property) {
656
+            if ($property['name'] === self::PROPERTY_AVATAR) {
657
+                continue;
658
+            }
659
+
660
+            // the value col is limited to 255 bytes. It is used for searches only.
661
+            $value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
662
+
663
+            $query->setParameter('name', $property['name'])
664
+                ->setParameter('value', $value);
665
+            $query->executeStatement();
666
+        }
667
+    }
668
+
669
+    /**
670
+     * build default user record in case not data set exists yet
671
+     */
672
+    protected function buildDefaultUserRecord(IUser $user): array {
673
+        $scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
674
+            return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
675
+        }, ARRAY_FILTER_USE_BOTH));
676
+
677
+        return [
678
+            [
679
+                'name' => self::PROPERTY_DISPLAYNAME,
680
+                'value' => $user->getDisplayName(),
681
+                // Display name must be at least SCOPE_LOCAL
682
+                'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
683
+                'verified' => self::NOT_VERIFIED,
684
+            ],
685
+
686
+            [
687
+                'name' => self::PROPERTY_ADDRESS,
688
+                'value' => '',
689
+                'scope' => $scopes[self::PROPERTY_ADDRESS],
690
+                'verified' => self::NOT_VERIFIED,
691
+            ],
692
+
693
+            [
694
+                'name' => self::PROPERTY_WEBSITE,
695
+                'value' => '',
696
+                'scope' => $scopes[self::PROPERTY_WEBSITE],
697
+                'verified' => self::NOT_VERIFIED,
698
+            ],
699
+
700
+            [
701
+                'name' => self::PROPERTY_EMAIL,
702
+                'value' => $user->getEMailAddress(),
703
+                // Email must be at least SCOPE_LOCAL
704
+                'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
705
+                'verified' => self::NOT_VERIFIED,
706
+            ],
707
+
708
+            [
709
+                'name' => self::PROPERTY_AVATAR,
710
+                'scope' => $scopes[self::PROPERTY_AVATAR],
711
+            ],
712
+
713
+            [
714
+                'name' => self::PROPERTY_PHONE,
715
+                'value' => '',
716
+                'scope' => $scopes[self::PROPERTY_PHONE],
717
+                'verified' => self::NOT_VERIFIED,
718
+            ],
719
+
720
+            [
721
+                'name' => self::PROPERTY_TWITTER,
722
+                'value' => '',
723
+                'scope' => $scopes[self::PROPERTY_TWITTER],
724
+                'verified' => self::NOT_VERIFIED,
725
+            ],
726
+
727
+            [
728
+                'name' => self::PROPERTY_FEDIVERSE,
729
+                'value' => '',
730
+                'scope' => $scopes[self::PROPERTY_FEDIVERSE],
731
+                'verified' => self::NOT_VERIFIED,
732
+            ],
733
+
734
+            [
735
+                'name' => self::PROPERTY_ORGANISATION,
736
+                'value' => '',
737
+                'scope' => $scopes[self::PROPERTY_ORGANISATION],
738
+            ],
739
+
740
+            [
741
+                'name' => self::PROPERTY_ROLE,
742
+                'value' => '',
743
+                'scope' => $scopes[self::PROPERTY_ROLE],
744
+            ],
745
+
746
+            [
747
+                'name' => self::PROPERTY_HEADLINE,
748
+                'value' => '',
749
+                'scope' => $scopes[self::PROPERTY_HEADLINE],
750
+            ],
751
+
752
+            [
753
+                'name' => self::PROPERTY_BIOGRAPHY,
754
+                'value' => '',
755
+                'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
756
+            ],
757
+
758
+            [
759
+                'name' => self::PROPERTY_PROFILE_ENABLED,
760
+                'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
761
+            ],
762
+        ];
763
+    }
764
+
765
+    private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
766
+        $collection = $account->getPropertyCollection($data['name']);
767
+
768
+        $p = new AccountProperty(
769
+            $data['name'],
770
+            $data['value'] ?? '',
771
+            $data['scope'] ?? self::SCOPE_LOCAL,
772
+            $data['verified'] ?? self::NOT_VERIFIED,
773
+            ''
774
+        );
775
+        $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
776
+        $collection->addProperty($p);
777
+
778
+        return $collection;
779
+    }
780
+
781
+    private function parseAccountData(IUser $user, $data): Account {
782
+        $account = new Account($user);
783
+        foreach ($data as $accountData) {
784
+            if ($this->isCollection($accountData['name'])) {
785
+                $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
786
+            } else {
787
+                $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
788
+                if (isset($accountData['locallyVerified'])) {
789
+                    $property = $account->getProperty($accountData['name']);
790
+                    $property->setLocallyVerified($accountData['locallyVerified']);
791
+                }
792
+            }
793
+        }
794
+        return $account;
795
+    }
796
+
797
+    public function getAccount(IUser $user): IAccount {
798
+        if ($this->internalCache->hasKey($user->getUID())) {
799
+            return $this->internalCache->get($user->getUID());
800
+        }
801
+        $account = $this->parseAccountData($user, $this->getUser($user));
802
+        $this->internalCache->set($user->getUID(), $account);
803
+        return $account;
804
+    }
805
+
806
+    public function updateAccount(IAccount $account): void {
807
+        $this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
808
+        try {
809
+            $property = $account->getProperty(self::PROPERTY_PHONE);
810
+            $this->sanitizePhoneNumberValue($property);
811
+        } catch (PropertyDoesNotExistException $e) {
812
+            //  valid case, nothing to do
813
+        }
814
+
815
+        try {
816
+            $property = $account->getProperty(self::PROPERTY_WEBSITE);
817
+            $this->sanitizeWebsite($property);
818
+        } catch (PropertyDoesNotExistException $e) {
819
+            //  valid case, nothing to do
820
+        }
821
+
822
+        foreach ($account->getAllProperties() as $property) {
823
+            $this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
824
+        }
825
+
826
+        $oldData = $this->getUser($account->getUser(), false);
827
+        $this->updateVerificationStatus($account, $oldData);
828
+        $this->checkEmailVerification($account, $oldData);
829
+        $this->checkLocalEmailVerification($account, $oldData);
830
+
831
+        $data = [];
832
+        foreach ($account->getAllProperties() as $property) {
833
+            /** @var IAccountProperty $property */
834
+            $data[] = [
835
+                'name' => $property->getName(),
836
+                'value' => $property->getValue(),
837
+                'scope' => $property->getScope(),
838
+                'verified' => $property->getVerified(),
839
+                'locallyVerified' => $property->getLocallyVerified(),
840
+            ];
841
+        }
842
+
843
+        $this->updateUser($account->getUser(), $data, $oldData, true);
844
+        $this->internalCache->set($account->getUser()->getUID(), $account);
845
+    }
846 846
 }
Please login to merge, or discard this patch.
apps/provisioning_api/lib/Controller/UsersController.php 1 patch
Indentation   +1359 added lines, -1359 removed lines patch added patch discarded remove patch
@@ -77,1363 +77,1363 @@
 block discarded – undo
77 77
 use Psr\Log\LoggerInterface;
78 78
 
79 79
 class UsersController extends AUserData {
80
-	/** @var IURLGenerator */
81
-	protected $urlGenerator;
82
-	/** @var LoggerInterface */
83
-	private $logger;
84
-	/** @var IFactory */
85
-	protected $l10nFactory;
86
-	/** @var NewUserMailHelper */
87
-	private $newUserMailHelper;
88
-	/** @var ISecureRandom */
89
-	private $secureRandom;
90
-	/** @var RemoteWipe */
91
-	private $remoteWipe;
92
-	/** @var KnownUserService */
93
-	private $knownUserService;
94
-	/** @var IEventDispatcher */
95
-	private $eventDispatcher;
96
-
97
-	public function __construct(
98
-		string $appName,
99
-		IRequest $request,
100
-		IUserManager $userManager,
101
-		IConfig $config,
102
-		IGroupManager $groupManager,
103
-		IUserSession $userSession,
104
-		IAccountManager $accountManager,
105
-		IURLGenerator $urlGenerator,
106
-		LoggerInterface $logger,
107
-		IFactory $l10nFactory,
108
-		NewUserMailHelper $newUserMailHelper,
109
-		ISecureRandom $secureRandom,
110
-		RemoteWipe $remoteWipe,
111
-		KnownUserService $knownUserService,
112
-		IEventDispatcher $eventDispatcher
113
-	) {
114
-		parent::__construct(
115
-			$appName,
116
-			$request,
117
-			$userManager,
118
-			$config,
119
-			$groupManager,
120
-			$userSession,
121
-			$accountManager,
122
-			$l10nFactory
123
-		);
124
-
125
-		$this->urlGenerator = $urlGenerator;
126
-		$this->logger = $logger;
127
-		$this->l10nFactory = $l10nFactory;
128
-		$this->newUserMailHelper = $newUserMailHelper;
129
-		$this->secureRandom = $secureRandom;
130
-		$this->remoteWipe = $remoteWipe;
131
-		$this->knownUserService = $knownUserService;
132
-		$this->eventDispatcher = $eventDispatcher;
133
-	}
134
-
135
-	/**
136
-	 * @NoAdminRequired
137
-	 *
138
-	 * returns a list of users
139
-	 *
140
-	 * @param string $search
141
-	 * @param int $limit
142
-	 * @param int $offset
143
-	 * @return DataResponse
144
-	 */
145
-	public function getUsers(string $search = '', int $limit = null, int $offset = 0): DataResponse {
146
-		$user = $this->userSession->getUser();
147
-		$users = [];
148
-
149
-		// Admin? Or SubAdmin?
150
-		$uid = $user->getUID();
151
-		$subAdminManager = $this->groupManager->getSubAdmin();
152
-		if ($this->groupManager->isAdmin($uid)) {
153
-			$users = $this->userManager->search($search, $limit, $offset);
154
-		} elseif ($subAdminManager->isSubAdmin($user)) {
155
-			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
156
-			foreach ($subAdminOfGroups as $key => $group) {
157
-				$subAdminOfGroups[$key] = $group->getGID();
158
-			}
159
-
160
-			$users = [];
161
-			foreach ($subAdminOfGroups as $group) {
162
-				$users = array_merge($users, $this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
163
-			}
164
-		}
165
-
166
-		$users = array_keys($users);
167
-
168
-		return new DataResponse([
169
-			'users' => $users
170
-		]);
171
-	}
172
-
173
-	/**
174
-	 * @NoAdminRequired
175
-	 *
176
-	 * returns a list of users and their data
177
-	 */
178
-	public function getUsersDetails(string $search = '', int $limit = null, int $offset = 0): DataResponse {
179
-		$currentUser = $this->userSession->getUser();
180
-		$users = [];
181
-
182
-		// Admin? Or SubAdmin?
183
-		$uid = $currentUser->getUID();
184
-		$subAdminManager = $this->groupManager->getSubAdmin();
185
-		if ($this->groupManager->isAdmin($uid)) {
186
-			$users = $this->userManager->search($search, $limit, $offset);
187
-			$users = array_keys($users);
188
-		} elseif ($subAdminManager->isSubAdmin($currentUser)) {
189
-			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
190
-			foreach ($subAdminOfGroups as $key => $group) {
191
-				$subAdminOfGroups[$key] = $group->getGID();
192
-			}
193
-
194
-			$users = [];
195
-			foreach ($subAdminOfGroups as $group) {
196
-				$users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
197
-			}
198
-			$users = array_merge(...$users);
199
-		}
200
-
201
-		$usersDetails = [];
202
-		foreach ($users as $userId) {
203
-			$userId = (string) $userId;
204
-			$userData = $this->getUserData($userId);
205
-			// Do not insert empty entry
206
-			if (!empty($userData)) {
207
-				$usersDetails[$userId] = $userData;
208
-			} else {
209
-				// Logged user does not have permissions to see this user
210
-				// only showing its id
211
-				$usersDetails[$userId] = ['id' => $userId];
212
-			}
213
-		}
214
-
215
-		return new DataResponse([
216
-			'users' => $usersDetails
217
-		]);
218
-	}
219
-
220
-
221
-	/**
222
-	 * @NoAdminRequired
223
-	 * @NoSubAdminRequired
224
-	 *
225
-	 * @param string $location
226
-	 * @param array $search
227
-	 * @return DataResponse
228
-	 */
229
-	public function searchByPhoneNumbers(string $location, array $search): DataResponse {
230
-		$phoneUtil = PhoneNumberUtil::getInstance();
231
-
232
-		if ($phoneUtil->getCountryCodeForRegion($location) === 0) {
233
-			// Not a valid region code
234
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
235
-		}
236
-
237
-		/** @var IUser $user */
238
-		$user = $this->userSession->getUser();
239
-		$knownTo = $user->getUID();
240
-		$defaultPhoneRegion = $this->config->getSystemValueString('default_phone_region');
241
-
242
-		$normalizedNumberToKey = [];
243
-		foreach ($search as $key => $phoneNumbers) {
244
-			foreach ($phoneNumbers as $phone) {
245
-				try {
246
-					$phoneNumber = $phoneUtil->parse($phone, $location);
247
-					if ($phoneUtil->isValidNumber($phoneNumber)) {
248
-						$normalizedNumber = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
249
-						$normalizedNumberToKey[$normalizedNumber] = (string) $key;
250
-					}
251
-				} catch (NumberParseException $e) {
252
-				}
253
-
254
-				if ($defaultPhoneRegion !== '' && $defaultPhoneRegion !== $location && strpos($phone, '0') === 0) {
255
-					// If the number has a leading zero (no country code),
256
-					// we also check the default phone region of the instance,
257
-					// when it's different to the user's given region.
258
-					try {
259
-						$phoneNumber = $phoneUtil->parse($phone, $defaultPhoneRegion);
260
-						if ($phoneUtil->isValidNumber($phoneNumber)) {
261
-							$normalizedNumber = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
262
-							$normalizedNumberToKey[$normalizedNumber] = (string) $key;
263
-						}
264
-					} catch (NumberParseException $e) {
265
-					}
266
-				}
267
-			}
268
-		}
269
-
270
-		$phoneNumbers = array_keys($normalizedNumberToKey);
271
-
272
-		if (empty($phoneNumbers)) {
273
-			return new DataResponse();
274
-		}
275
-
276
-		// Cleanup all previous entries and only allow new matches
277
-		$this->knownUserService->deleteKnownTo($knownTo);
278
-
279
-		$userMatches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, $phoneNumbers);
280
-
281
-		if (empty($userMatches)) {
282
-			return new DataResponse();
283
-		}
284
-
285
-		$cloudUrl = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
286
-		if (strpos($cloudUrl, 'http://') === 0) {
287
-			$cloudUrl = substr($cloudUrl, strlen('http://'));
288
-		} elseif (strpos($cloudUrl, 'https://') === 0) {
289
-			$cloudUrl = substr($cloudUrl, strlen('https://'));
290
-		}
291
-
292
-		$matches = [];
293
-		foreach ($userMatches as $phone => $userId) {
294
-			// Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book
295
-			$matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl;
296
-			$this->knownUserService->storeIsKnownToUser($knownTo, $userId);
297
-		}
298
-
299
-		return new DataResponse($matches);
300
-	}
301
-
302
-	/**
303
-	 * @throws OCSException
304
-	 */
305
-	private function createNewUserId(): string {
306
-		$attempts = 0;
307
-		do {
308
-			$uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
309
-			if (!$this->userManager->userExists($uidCandidate)) {
310
-				return $uidCandidate;
311
-			}
312
-			$attempts++;
313
-		} while ($attempts < 10);
314
-		throw new OCSException('Could not create non-existing user id', 111);
315
-	}
316
-
317
-	/**
318
-	 * @PasswordConfirmationRequired
319
-	 * @NoAdminRequired
320
-	 *
321
-	 * @param string $userid
322
-	 * @param string $password
323
-	 * @param string $displayName
324
-	 * @param string $email
325
-	 * @param array $groups
326
-	 * @param array $subadmin
327
-	 * @param string $quota
328
-	 * @param string $language
329
-	 * @return DataResponse
330
-	 * @throws OCSException
331
-	 */
332
-	public function addUser(
333
-		string $userid,
334
-		string $password = '',
335
-		string $displayName = '',
336
-		string $email = '',
337
-		array $groups = [],
338
-		array $subadmin = [],
339
-		string $quota = '',
340
-		string $language = '',
341
-		?string $manager = null,
342
-	): DataResponse {
343
-		$user = $this->userSession->getUser();
344
-		$isAdmin = $this->groupManager->isAdmin($user->getUID());
345
-		$subAdminManager = $this->groupManager->getSubAdmin();
346
-
347
-		if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
348
-			$userid = $this->createNewUserId();
349
-		}
350
-
351
-		if ($this->userManager->userExists($userid)) {
352
-			$this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
353
-			throw new OCSException($this->l10nFactory->get('provisioning_api')->t('User already exists'), 102);
354
-		}
355
-
356
-		if ($groups !== []) {
357
-			foreach ($groups as $group) {
358
-				if (!$this->groupManager->groupExists($group)) {
359
-					throw new OCSException('group ' . $group . ' does not exist', 104);
360
-				}
361
-				if (!$isAdmin && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
362
-					throw new OCSException('insufficient privileges for group ' . $group, 105);
363
-				}
364
-			}
365
-		} else {
366
-			if (!$isAdmin) {
367
-				throw new OCSException('no group specified (required for subadmins)', 106);
368
-			}
369
-		}
370
-
371
-		$subadminGroups = [];
372
-		if ($subadmin !== []) {
373
-			foreach ($subadmin as $groupid) {
374
-				$group = $this->groupManager->get($groupid);
375
-				// Check if group exists
376
-				if ($group === null) {
377
-					throw new OCSException('Subadmin group does not exist', 102);
378
-				}
379
-				// Check if trying to make subadmin of admin group
380
-				if ($group->getGID() === 'admin') {
381
-					throw new OCSException('Cannot create subadmins for admin group', 103);
382
-				}
383
-				// Check if has permission to promote subadmins
384
-				if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin) {
385
-					throw new OCSForbiddenException('No permissions to promote subadmins');
386
-				}
387
-				$subadminGroups[] = $group;
388
-			}
389
-		}
390
-
391
-		$generatePasswordResetToken = false;
392
-		if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
393
-			throw new OCSException('Invalid password value', 101);
394
-		}
395
-		if ($password === '') {
396
-			if ($email === '') {
397
-				throw new OCSException('To send a password link to the user an email address is required.', 108);
398
-			}
399
-
400
-			$passwordEvent = new GenerateSecurePasswordEvent();
401
-			$this->eventDispatcher->dispatchTyped($passwordEvent);
402
-
403
-			$password = $passwordEvent->getPassword();
404
-			if ($password === null) {
405
-				// Fallback: ensure to pass password_policy in any case
406
-				$password = $this->secureRandom->generate(10)
407
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_UPPER)
408
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_LOWER)
409
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_DIGITS)
410
-					. $this->secureRandom->generate(1, ISecureRandom::CHAR_SYMBOLS);
411
-			}
412
-			$generatePasswordResetToken = true;
413
-		}
414
-
415
-		if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
416
-			throw new OCSException('Required email address was not provided', 110);
417
-		}
418
-
419
-		try {
420
-			$newUser = $this->userManager->createUser($userid, $password);
421
-			$this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
422
-
423
-			foreach ($groups as $group) {
424
-				$this->groupManager->get($group)->addUser($newUser);
425
-				$this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
426
-			}
427
-			foreach ($subadminGroups as $group) {
428
-				$subAdminManager->createSubAdmin($newUser, $group);
429
-			}
430
-
431
-			if ($displayName !== '') {
432
-				try {
433
-					$this->editUser($userid, self::USER_FIELD_DISPLAYNAME, $displayName);
434
-				} catch (OCSException $e) {
435
-					if ($newUser instanceof IUser) {
436
-						$newUser->delete();
437
-					}
438
-					throw $e;
439
-				}
440
-			}
441
-
442
-			if ($quota !== '') {
443
-				$this->editUser($userid, self::USER_FIELD_QUOTA, $quota);
444
-			}
445
-
446
-			if ($language !== '') {
447
-				$this->editUser($userid, self::USER_FIELD_LANGUAGE, $language);
448
-			}
449
-
450
-			/**
451
-			 * null -> nothing sent
452
-			 * '' -> unset manager
453
-			 * else -> set manager
454
-			 */
455
-			if ($manager !== null) {
456
-				$this->editUser($userid, self::USER_FIELD_MANAGER, $manager);
457
-			}
458
-
459
-			// Send new user mail only if a mail is set
460
-			if ($email !== '') {
461
-				$newUser->setEMailAddress($email);
462
-				if ($this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes') {
463
-					try {
464
-						$emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken);
465
-						$this->newUserMailHelper->sendMail($newUser, $emailTemplate);
466
-					} catch (\Exception $e) {
467
-						// Mail could be failing hard or just be plain not configured
468
-						// Logging error as it is the hardest of the two
469
-						$this->logger->error(
470
-							"Unable to send the invitation mail to $email",
471
-							[
472
-								'app' => 'ocs_api',
473
-								'exception' => $e,
474
-							]
475
-						);
476
-					}
477
-				}
478
-			}
479
-
480
-			return new DataResponse(['id' => $userid]);
481
-		} catch (HintException $e) {
482
-			$this->logger->warning(
483
-				'Failed addUser attempt with hint exception.',
484
-				[
485
-					'app' => 'ocs_api',
486
-					'exception' => $e,
487
-				]
488
-			);
489
-			throw new OCSException($e->getHint(), 107);
490
-		} catch (OCSException $e) {
491
-			$this->logger->warning(
492
-				'Failed addUser attempt with ocs exception.',
493
-				[
494
-					'app' => 'ocs_api',
495
-					'exception' => $e,
496
-				]
497
-			);
498
-			throw $e;
499
-		} catch (InvalidArgumentException $e) {
500
-			$this->logger->error(
501
-				'Failed addUser attempt with invalid argument exception.',
502
-				[
503
-					'app' => 'ocs_api',
504
-					'exception' => $e,
505
-				]
506
-			);
507
-			throw new OCSException($e->getMessage(), 101);
508
-		} catch (\Exception $e) {
509
-			$this->logger->error(
510
-				'Failed addUser attempt with exception.',
511
-				[
512
-					'app' => 'ocs_api',
513
-					'exception' => $e
514
-				]
515
-			);
516
-			throw new OCSException('Bad request', 101);
517
-		}
518
-	}
519
-
520
-	/**
521
-	 * @NoAdminRequired
522
-	 * @NoSubAdminRequired
523
-	 *
524
-	 * gets user info
525
-	 *
526
-	 * @param string $userId
527
-	 * @return DataResponse
528
-	 * @throws OCSException
529
-	 */
530
-	public function getUser(string $userId): DataResponse {
531
-		$includeScopes = false;
532
-		$currentUser = $this->userSession->getUser();
533
-		if ($currentUser && $currentUser->getUID() === $userId) {
534
-			$includeScopes = true;
535
-		}
536
-
537
-		$data = $this->getUserData($userId, $includeScopes);
538
-		// getUserData returns empty array if not enough permissions
539
-		if (empty($data)) {
540
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
541
-		}
542
-		return new DataResponse($data);
543
-	}
544
-
545
-	/**
546
-	 * @NoAdminRequired
547
-	 * @NoSubAdminRequired
548
-	 *
549
-	 * gets user info from the currently logged in user
550
-	 *
551
-	 * @return DataResponse
552
-	 * @throws OCSException
553
-	 */
554
-	public function getCurrentUser(): DataResponse {
555
-		$user = $this->userSession->getUser();
556
-		if ($user) {
557
-			$data = $this->getUserData($user->getUID(), true);
558
-			return new DataResponse($data);
559
-		}
560
-
561
-		throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
562
-	}
563
-
564
-	/**
565
-	 * @NoAdminRequired
566
-	 * @NoSubAdminRequired
567
-	 *
568
-	 * @return DataResponse
569
-	 * @throws OCSException
570
-	 */
571
-	public function getEditableFields(): DataResponse {
572
-		$currentLoggedInUser = $this->userSession->getUser();
573
-		if (!$currentLoggedInUser instanceof IUser) {
574
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
575
-		}
576
-
577
-		return $this->getEditableFieldsForUser($currentLoggedInUser->getUID());
578
-	}
579
-
580
-	/**
581
-	 * @NoAdminRequired
582
-	 * @NoSubAdminRequired
583
-	 *
584
-	 * @param string $userId
585
-	 * @return DataResponse
586
-	 * @throws OCSException
587
-	 */
588
-	public function getEditableFieldsForUser(string $userId): DataResponse {
589
-		$currentLoggedInUser = $this->userSession->getUser();
590
-		if (!$currentLoggedInUser instanceof IUser) {
591
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
592
-		}
593
-
594
-		$permittedFields = [];
595
-
596
-		if ($userId !== $currentLoggedInUser->getUID()) {
597
-			$targetUser = $this->userManager->get($userId);
598
-			if (!$targetUser instanceof IUser) {
599
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
600
-			}
601
-
602
-			$subAdminManager = $this->groupManager->getSubAdmin();
603
-			if (
604
-				!$this->groupManager->isAdmin($currentLoggedInUser->getUID())
605
-				&& !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
606
-			) {
607
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
608
-			}
609
-		} else {
610
-			$targetUser = $currentLoggedInUser;
611
-		}
612
-
613
-		// Editing self (display, email)
614
-		if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
615
-			if (
616
-				$targetUser->getBackend() instanceof ISetDisplayNameBackend
617
-				|| $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
618
-			) {
619
-				$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
620
-			}
621
-			$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
622
-		}
623
-
624
-		$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
625
-		$permittedFields[] = IAccountManager::PROPERTY_PHONE;
626
-		$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
627
-		$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
628
-		$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
629
-		$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
630
-		$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
631
-		$permittedFields[] = IAccountManager::PROPERTY_ROLE;
632
-		$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
633
-		$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
634
-		$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
635
-
636
-		return new DataResponse($permittedFields);
637
-	}
638
-
639
-	/**
640
-	 * @NoAdminRequired
641
-	 * @NoSubAdminRequired
642
-	 * @PasswordConfirmationRequired
643
-	 * @UserRateThrottle(limit=5, period=60)
644
-	 *
645
-	 * @throws OCSException
646
-	 */
647
-	public function editUserMultiValue(
648
-		string $userId,
649
-		string $collectionName,
650
-		string $key,
651
-		string $value
652
-	): DataResponse {
653
-		$currentLoggedInUser = $this->userSession->getUser();
654
-		if ($currentLoggedInUser === null) {
655
-			throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
656
-		}
657
-
658
-		$targetUser = $this->userManager->get($userId);
659
-		if ($targetUser === null) {
660
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
661
-		}
662
-
663
-		$subAdminManager = $this->groupManager->getSubAdmin();
664
-		$isAdminOrSubadmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID())
665
-			|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser);
666
-
667
-		$permittedFields = [];
668
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
669
-			// Editing self (display, email)
670
-			$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
671
-			$permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX;
672
-		} else {
673
-			// Check if admin / subadmin
674
-			if ($isAdminOrSubadmin) {
675
-				// They have permissions over the user
676
-				$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
677
-			} else {
678
-				// No rights
679
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
680
-			}
681
-		}
682
-
683
-		// Check if permitted to edit this field
684
-		if (!in_array($collectionName, $permittedFields)) {
685
-			throw new OCSException('', 103);
686
-		}
687
-
688
-		switch ($collectionName) {
689
-			case IAccountManager::COLLECTION_EMAIL:
690
-				$userAccount = $this->accountManager->getAccount($targetUser);
691
-				$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
692
-				$mailCollection->removePropertyByValue($key);
693
-				if ($value !== '') {
694
-					$mailCollection->addPropertyWithDefaults($value);
695
-					$property = $mailCollection->getPropertyByValue($key);
696
-					if ($isAdminOrSubadmin && $property) {
697
-						// admin set mails are auto-verified
698
-						$property->setLocallyVerified(IAccountManager::VERIFIED);
699
-					}
700
-				}
701
-				$this->accountManager->updateAccount($userAccount);
702
-				break;
703
-
704
-			case IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX:
705
-				$userAccount = $this->accountManager->getAccount($targetUser);
706
-				$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
707
-				$targetProperty = null;
708
-				foreach ($mailCollection->getProperties() as $property) {
709
-					if ($property->getValue() === $key) {
710
-						$targetProperty = $property;
711
-						break;
712
-					}
713
-				}
714
-				if ($targetProperty instanceof IAccountProperty) {
715
-					try {
716
-						$targetProperty->setScope($value);
717
-						$this->accountManager->updateAccount($userAccount);
718
-					} catch (InvalidArgumentException $e) {
719
-						throw new OCSException('', 102);
720
-					}
721
-				} else {
722
-					throw new OCSException('', 102);
723
-				}
724
-				break;
725
-
726
-			default:
727
-				throw new OCSException('', 103);
728
-		}
729
-		return new DataResponse();
730
-	}
731
-
732
-	/**
733
-	 * @NoAdminRequired
734
-	 * @NoSubAdminRequired
735
-	 * @PasswordConfirmationRequired
736
-	 * @UserRateThrottle(limit=50, period=600)
737
-	 *
738
-	 * edit users
739
-	 *
740
-	 * @param string $userId
741
-	 * @param string $key
742
-	 * @param string $value
743
-	 * @return DataResponse
744
-	 * @throws OCSException
745
-	 */
746
-	public function editUser(string $userId, string $key, string $value): DataResponse {
747
-		$currentLoggedInUser = $this->userSession->getUser();
748
-
749
-		$targetUser = $this->userManager->get($userId);
750
-		if ($targetUser === null) {
751
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
752
-		}
753
-
754
-		$permittedFields = [];
755
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
756
-			// Editing self (display, email)
757
-			if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
758
-				if (
759
-					$targetUser->getBackend() instanceof ISetDisplayNameBackend
760
-					|| $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
761
-				) {
762
-					$permittedFields[] = self::USER_FIELD_DISPLAYNAME;
763
-					$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
764
-				}
765
-				$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
766
-			}
767
-
768
-			$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX;
769
-			$permittedFields[] = IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX;
770
-
771
-			$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
772
-
773
-			$permittedFields[] = self::USER_FIELD_PASSWORD;
774
-			$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
775
-			if (
776
-				$this->config->getSystemValue('force_language', false) === false ||
777
-				$this->groupManager->isAdmin($currentLoggedInUser->getUID())
778
-			) {
779
-				$permittedFields[] = self::USER_FIELD_LANGUAGE;
780
-			}
781
-
782
-			if (
783
-				$this->config->getSystemValue('force_locale', false) === false ||
784
-				$this->groupManager->isAdmin($currentLoggedInUser->getUID())
785
-			) {
786
-				$permittedFields[] = self::USER_FIELD_LOCALE;
787
-			}
788
-
789
-			$permittedFields[] = IAccountManager::PROPERTY_PHONE;
790
-			$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
791
-			$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
792
-			$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
793
-			$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
794
-			$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
795
-			$permittedFields[] = IAccountManager::PROPERTY_ROLE;
796
-			$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
797
-			$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
798
-			$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
799
-			$permittedFields[] = IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX;
800
-			$permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX;
801
-			$permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX;
802
-			$permittedFields[] = IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX;
803
-			$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX;
804
-			$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX;
805
-			$permittedFields[] = IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX;
806
-			$permittedFields[] = IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX;
807
-			$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX;
808
-			$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX;
809
-
810
-			$permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
811
-
812
-			// If admin they can edit their own quota and manager
813
-			if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
814
-				$permittedFields[] = self::USER_FIELD_QUOTA;
815
-				$permittedFields[] = self::USER_FIELD_MANAGER;
816
-
817
-			}
818
-		} else {
819
-			// Check if admin / subadmin
820
-			$subAdminManager = $this->groupManager->getSubAdmin();
821
-			if (
822
-				$this->groupManager->isAdmin($currentLoggedInUser->getUID())
823
-				|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
824
-			) {
825
-				// They have permissions over the user
826
-				if (
827
-					$targetUser->getBackend() instanceof ISetDisplayNameBackend
828
-					|| $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
829
-				) {
830
-					$permittedFields[] = self::USER_FIELD_DISPLAYNAME;
831
-					$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
832
-				}
833
-				$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
834
-				$permittedFields[] = IAccountManager::COLLECTION_EMAIL;
835
-				$permittedFields[] = self::USER_FIELD_PASSWORD;
836
-				$permittedFields[] = self::USER_FIELD_LANGUAGE;
837
-				$permittedFields[] = self::USER_FIELD_LOCALE;
838
-				$permittedFields[] = IAccountManager::PROPERTY_PHONE;
839
-				$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
840
-				$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
841
-				$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
842
-				$permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
843
-				$permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
844
-				$permittedFields[] = IAccountManager::PROPERTY_ROLE;
845
-				$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
846
-				$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
847
-				$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
848
-				$permittedFields[] = self::USER_FIELD_QUOTA;
849
-				$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
850
-				$permittedFields[] = self::USER_FIELD_MANAGER;
851
-			} else {
852
-				// No rights
853
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
854
-			}
855
-		}
856
-		// Check if permitted to edit this field
857
-		if (!in_array($key, $permittedFields)) {
858
-			throw new OCSException('', 103);
859
-		}
860
-		// Process the edit
861
-		switch ($key) {
862
-			case self::USER_FIELD_DISPLAYNAME:
863
-			case IAccountManager::PROPERTY_DISPLAYNAME:
864
-				try {
865
-					$targetUser->setDisplayName($value);
866
-				} catch (InvalidArgumentException $e) {
867
-					throw new OCSException($e->getMessage(), 101);
868
-				}
869
-				break;
870
-			case self::USER_FIELD_QUOTA:
871
-				$quota = $value;
872
-				if ($quota !== 'none' && $quota !== 'default') {
873
-					if (is_numeric($quota)) {
874
-						$quota = (float) $quota;
875
-					} else {
876
-						$quota = \OCP\Util::computerFileSize($quota);
877
-					}
878
-					if ($quota === false) {
879
-						throw new OCSException('Invalid quota value ' . $value, 102);
880
-					}
881
-					if ($quota === -1) {
882
-						$quota = 'none';
883
-					} else {
884
-						$maxQuota = (int) $this->config->getAppValue('files', 'max_quota', '-1');
885
-						if ($maxQuota !== -1 && $quota > $maxQuota) {
886
-							throw new OCSException('Invalid quota value. ' . $value . ' is exceeding the maximum quota', 102);
887
-						}
888
-						$quota = \OCP\Util::humanFileSize($quota);
889
-					}
890
-				}
891
-				// no else block because quota can be set to 'none' in previous if
892
-				if ($quota === 'none') {
893
-					$allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1';
894
-					if (!$allowUnlimitedQuota) {
895
-						throw new OCSException('Unlimited quota is forbidden on this instance', 102);
896
-					}
897
-				}
898
-				$targetUser->setQuota($quota);
899
-				break;
900
-			case self::USER_FIELD_MANAGER:
901
-				$targetUser->setManagerUids([$value]);
902
-				break;
903
-			case self::USER_FIELD_PASSWORD:
904
-				try {
905
-					if (strlen($value) > IUserManager::MAX_PASSWORD_LENGTH) {
906
-						throw new OCSException('Invalid password value', 102);
907
-					}
908
-					if (!$targetUser->canChangePassword()) {
909
-						throw new OCSException('Setting the password is not supported by the users backend', 103);
910
-					}
911
-					$targetUser->setPassword($value);
912
-				} catch (HintException $e) { // password policy error
913
-					throw new OCSException($e->getMessage(), 103);
914
-				}
915
-				break;
916
-			case self::USER_FIELD_LANGUAGE:
917
-				$languagesCodes = $this->l10nFactory->findAvailableLanguages();
918
-				if (!in_array($value, $languagesCodes, true) && $value !== 'en') {
919
-					throw new OCSException('Invalid language', 102);
920
-				}
921
-				$this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value);
922
-				break;
923
-			case self::USER_FIELD_LOCALE:
924
-				if (!$this->l10nFactory->localeExists($value)) {
925
-					throw new OCSException('Invalid locale', 102);
926
-				}
927
-				$this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
928
-				break;
929
-			case self::USER_FIELD_NOTIFICATION_EMAIL:
930
-				$success = false;
931
-				if ($value === '' || filter_var($value, FILTER_VALIDATE_EMAIL)) {
932
-					try {
933
-						$targetUser->setPrimaryEMailAddress($value);
934
-						$success = true;
935
-					} catch (InvalidArgumentException $e) {
936
-						$this->logger->info(
937
-							'Cannot set primary email, because provided address is not verified',
938
-							[
939
-								'app' => 'provisioning_api',
940
-								'exception' => $e,
941
-							]
942
-						);
943
-					}
944
-				}
945
-				if (!$success) {
946
-					throw new OCSException('', 102);
947
-				}
948
-				break;
949
-			case IAccountManager::PROPERTY_EMAIL:
950
-				if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
951
-					$targetUser->setEMailAddress($value);
952
-				} else {
953
-					throw new OCSException('', 102);
954
-				}
955
-				break;
956
-			case IAccountManager::COLLECTION_EMAIL:
957
-				if (filter_var($value, FILTER_VALIDATE_EMAIL) && $value !== $targetUser->getSystemEMailAddress()) {
958
-					$userAccount = $this->accountManager->getAccount($targetUser);
959
-					$mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
960
-
961
-					if ($mailCollection->getPropertyByValue($value)) {
962
-						throw new OCSException('', 102);
963
-					}
964
-
965
-					$mailCollection->addPropertyWithDefaults($value);
966
-					$this->accountManager->updateAccount($userAccount);
967
-				} else {
968
-					throw new OCSException('', 102);
969
-				}
970
-				break;
971
-			case IAccountManager::PROPERTY_PHONE:
972
-			case IAccountManager::PROPERTY_ADDRESS:
973
-			case IAccountManager::PROPERTY_WEBSITE:
974
-			case IAccountManager::PROPERTY_TWITTER:
975
-			case IAccountManager::PROPERTY_FEDIVERSE:
976
-			case IAccountManager::PROPERTY_ORGANISATION:
977
-			case IAccountManager::PROPERTY_ROLE:
978
-			case IAccountManager::PROPERTY_HEADLINE:
979
-			case IAccountManager::PROPERTY_BIOGRAPHY:
980
-				$userAccount = $this->accountManager->getAccount($targetUser);
981
-				try {
982
-					$userProperty = $userAccount->getProperty($key);
983
-					if ($userProperty->getValue() !== $value) {
984
-						try {
985
-							$userProperty->setValue($value);
986
-							if ($userProperty->getName() === IAccountManager::PROPERTY_PHONE) {
987
-								$this->knownUserService->deleteByContactUserId($targetUser->getUID());
988
-							}
989
-						} catch (InvalidArgumentException $e) {
990
-							throw new OCSException('Invalid ' . $e->getMessage(), 102);
991
-						}
992
-					}
993
-				} catch (PropertyDoesNotExistException $e) {
994
-					$userAccount->setProperty($key, $value, IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED);
995
-				}
996
-				try {
997
-					$this->accountManager->updateAccount($userAccount);
998
-				} catch (InvalidArgumentException $e) {
999
-					throw new OCSException('Invalid ' . $e->getMessage(), 102);
1000
-				}
1001
-				break;
1002
-			case IAccountManager::PROPERTY_PROFILE_ENABLED:
1003
-				$userAccount = $this->accountManager->getAccount($targetUser);
1004
-				try {
1005
-					$userProperty = $userAccount->getProperty($key);
1006
-					if ($userProperty->getValue() !== $value) {
1007
-						$userProperty->setValue($value);
1008
-					}
1009
-				} catch (PropertyDoesNotExistException $e) {
1010
-					$userAccount->setProperty($key, $value, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
1011
-				}
1012
-				$this->accountManager->updateAccount($userAccount);
1013
-				break;
1014
-			case IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX:
1015
-			case IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX:
1016
-			case IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX:
1017
-			case IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX:
1018
-			case IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX:
1019
-			case IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX:
1020
-			case IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX:
1021
-			case IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX:
1022
-			case IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX:
1023
-			case IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX:
1024
-			case IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX:
1025
-			case IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX:
1026
-			case IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX:
1027
-				$propertyName = substr($key, 0, strlen($key) - strlen(self::SCOPE_SUFFIX));
1028
-				$userAccount = $this->accountManager->getAccount($targetUser);
1029
-				$userProperty = $userAccount->getProperty($propertyName);
1030
-				if ($userProperty->getScope() !== $value) {
1031
-					try {
1032
-						$userProperty->setScope($value);
1033
-						$this->accountManager->updateAccount($userAccount);
1034
-					} catch (InvalidArgumentException $e) {
1035
-						throw new OCSException('Invalid ' . $e->getMessage(), 102);
1036
-					}
1037
-				}
1038
-				break;
1039
-			default:
1040
-				throw new OCSException('', 103);
1041
-		}
1042
-		return new DataResponse();
1043
-	}
1044
-
1045
-	/**
1046
-	 * @PasswordConfirmationRequired
1047
-	 * @NoAdminRequired
1048
-	 *
1049
-	 * @param string $userId
1050
-	 *
1051
-	 * @return DataResponse
1052
-	 *
1053
-	 * @throws OCSException
1054
-	 */
1055
-	public function wipeUserDevices(string $userId): DataResponse {
1056
-		/** @var IUser $currentLoggedInUser */
1057
-		$currentLoggedInUser = $this->userSession->getUser();
1058
-
1059
-		$targetUser = $this->userManager->get($userId);
1060
-
1061
-		if ($targetUser === null) {
1062
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1063
-		}
1064
-
1065
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1066
-			throw new OCSException('', 101);
1067
-		}
1068
-
1069
-		// If not permitted
1070
-		$subAdminManager = $this->groupManager->getSubAdmin();
1071
-		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1072
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1073
-		}
1074
-
1075
-		$this->remoteWipe->markAllTokensForWipe($targetUser);
1076
-
1077
-		return new DataResponse();
1078
-	}
1079
-
1080
-	/**
1081
-	 * @PasswordConfirmationRequired
1082
-	 * @NoAdminRequired
1083
-	 *
1084
-	 * @param string $userId
1085
-	 * @return DataResponse
1086
-	 * @throws OCSException
1087
-	 */
1088
-	public function deleteUser(string $userId): DataResponse {
1089
-		$currentLoggedInUser = $this->userSession->getUser();
1090
-
1091
-		$targetUser = $this->userManager->get($userId);
1092
-
1093
-		if ($targetUser === null) {
1094
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1095
-		}
1096
-
1097
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1098
-			throw new OCSException('', 101);
1099
-		}
1100
-
1101
-		// If not permitted
1102
-		$subAdminManager = $this->groupManager->getSubAdmin();
1103
-		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1104
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1105
-		}
1106
-
1107
-		// Go ahead with the delete
1108
-		if ($targetUser->delete()) {
1109
-			return new DataResponse();
1110
-		} else {
1111
-			throw new OCSException('', 101);
1112
-		}
1113
-	}
1114
-
1115
-	/**
1116
-	 * @PasswordConfirmationRequired
1117
-	 * @NoAdminRequired
1118
-	 *
1119
-	 * @param string $userId
1120
-	 * @return DataResponse
1121
-	 * @throws OCSException
1122
-	 * @throws OCSForbiddenException
1123
-	 */
1124
-	public function disableUser(string $userId): DataResponse {
1125
-		return $this->setEnabled($userId, false);
1126
-	}
1127
-
1128
-	/**
1129
-	 * @PasswordConfirmationRequired
1130
-	 * @NoAdminRequired
1131
-	 *
1132
-	 * @param string $userId
1133
-	 * @return DataResponse
1134
-	 * @throws OCSException
1135
-	 * @throws OCSForbiddenException
1136
-	 */
1137
-	public function enableUser(string $userId): DataResponse {
1138
-		return $this->setEnabled($userId, true);
1139
-	}
1140
-
1141
-	/**
1142
-	 * @param string $userId
1143
-	 * @param bool $value
1144
-	 * @return DataResponse
1145
-	 * @throws OCSException
1146
-	 */
1147
-	private function setEnabled(string $userId, bool $value): DataResponse {
1148
-		$currentLoggedInUser = $this->userSession->getUser();
1149
-
1150
-		$targetUser = $this->userManager->get($userId);
1151
-		if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
1152
-			throw new OCSException('', 101);
1153
-		}
1154
-
1155
-		// If not permitted
1156
-		$subAdminManager = $this->groupManager->getSubAdmin();
1157
-		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1158
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1159
-		}
1160
-
1161
-		// enable/disable the user now
1162
-		$targetUser->setEnabled($value);
1163
-		return new DataResponse();
1164
-	}
1165
-
1166
-	/**
1167
-	 * @NoAdminRequired
1168
-	 * @NoSubAdminRequired
1169
-	 *
1170
-	 * @param string $userId
1171
-	 * @return DataResponse
1172
-	 * @throws OCSException
1173
-	 */
1174
-	public function getUsersGroups(string $userId): DataResponse {
1175
-		$loggedInUser = $this->userSession->getUser();
1176
-
1177
-		$targetUser = $this->userManager->get($userId);
1178
-		if ($targetUser === null) {
1179
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1180
-		}
1181
-
1182
-		if ($targetUser->getUID() === $loggedInUser->getUID() || $this->groupManager->isAdmin($loggedInUser->getUID())) {
1183
-			// Self lookup or admin lookup
1184
-			return new DataResponse([
1185
-				'groups' => $this->groupManager->getUserGroupIds($targetUser)
1186
-			]);
1187
-		} else {
1188
-			$subAdminManager = $this->groupManager->getSubAdmin();
1189
-
1190
-			// Looking up someone else
1191
-			if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
1192
-				// Return the group that the method caller is subadmin of for the user in question
1193
-				/** @var IGroup[] $getSubAdminsGroups */
1194
-				$getSubAdminsGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
1195
-				foreach ($getSubAdminsGroups as $key => $group) {
1196
-					$getSubAdminsGroups[$key] = $group->getGID();
1197
-				}
1198
-				$groups = array_intersect(
1199
-					$getSubAdminsGroups,
1200
-					$this->groupManager->getUserGroupIds($targetUser)
1201
-				);
1202
-				return new DataResponse(['groups' => $groups]);
1203
-			} else {
1204
-				// Not permitted
1205
-				throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1206
-			}
1207
-		}
1208
-	}
1209
-
1210
-	/**
1211
-	 * @PasswordConfirmationRequired
1212
-	 * @NoAdminRequired
1213
-	 *
1214
-	 * @param string $userId
1215
-	 * @param string $groupid
1216
-	 * @return DataResponse
1217
-	 * @throws OCSException
1218
-	 */
1219
-	public function addToGroup(string $userId, string $groupid = ''): DataResponse {
1220
-		if ($groupid === '') {
1221
-			throw new OCSException('', 101);
1222
-		}
1223
-
1224
-		$group = $this->groupManager->get($groupid);
1225
-		$targetUser = $this->userManager->get($userId);
1226
-		if ($group === null) {
1227
-			throw new OCSException('', 102);
1228
-		}
1229
-		if ($targetUser === null) {
1230
-			throw new OCSException('', 103);
1231
-		}
1232
-
1233
-		// If they're not an admin, check they are a subadmin of the group in question
1234
-		$loggedInUser = $this->userSession->getUser();
1235
-		$subAdminManager = $this->groupManager->getSubAdmin();
1236
-		if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1237
-			throw new OCSException('', 104);
1238
-		}
1239
-
1240
-		// Add user to group
1241
-		$group->addUser($targetUser);
1242
-		return new DataResponse();
1243
-	}
1244
-
1245
-	/**
1246
-	 * @PasswordConfirmationRequired
1247
-	 * @NoAdminRequired
1248
-	 *
1249
-	 * @param string $userId
1250
-	 * @param string $groupid
1251
-	 * @return DataResponse
1252
-	 * @throws OCSException
1253
-	 */
1254
-	public function removeFromGroup(string $userId, string $groupid): DataResponse {
1255
-		$loggedInUser = $this->userSession->getUser();
1256
-
1257
-		if ($groupid === null || trim($groupid) === '') {
1258
-			throw new OCSException('', 101);
1259
-		}
1260
-
1261
-		$group = $this->groupManager->get($groupid);
1262
-		if ($group === null) {
1263
-			throw new OCSException('', 102);
1264
-		}
1265
-
1266
-		$targetUser = $this->userManager->get($userId);
1267
-		if ($targetUser === null) {
1268
-			throw new OCSException('', 103);
1269
-		}
1270
-
1271
-		// If they're not an admin, check they are a subadmin of the group in question
1272
-		$subAdminManager = $this->groupManager->getSubAdmin();
1273
-		if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1274
-			throw new OCSException('', 104);
1275
-		}
1276
-
1277
-		// Check they aren't removing themselves from 'admin' or their 'subadmin; group
1278
-		if ($targetUser->getUID() === $loggedInUser->getUID()) {
1279
-			if ($this->groupManager->isAdmin($loggedInUser->getUID())) {
1280
-				if ($group->getGID() === 'admin') {
1281
-					throw new OCSException('Cannot remove yourself from the admin group', 105);
1282
-				}
1283
-			} else {
1284
-				// Not an admin, so the user must be a subadmin of this group, but that is not allowed.
1285
-				throw new OCSException('Cannot remove yourself from this group as you are a SubAdmin', 105);
1286
-			}
1287
-		} elseif (!$this->groupManager->isAdmin($loggedInUser->getUID())) {
1288
-			/** @var IGroup[] $subAdminGroups */
1289
-			$subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
1290
-			$subAdminGroups = array_map(function (IGroup $subAdminGroup) {
1291
-				return $subAdminGroup->getGID();
1292
-			}, $subAdminGroups);
1293
-			$userGroups = $this->groupManager->getUserGroupIds($targetUser);
1294
-			$userSubAdminGroups = array_intersect($subAdminGroups, $userGroups);
1295
-
1296
-			if (count($userSubAdminGroups) <= 1) {
1297
-				// Subadmin must not be able to remove a user from all their subadmin groups.
1298
-				throw new OCSException('Not viable to remove user from the last group you are SubAdmin of', 105);
1299
-			}
1300
-		}
1301
-
1302
-		// Remove user from group
1303
-		$group->removeUser($targetUser);
1304
-		return new DataResponse();
1305
-	}
1306
-
1307
-	/**
1308
-	 * Creates a subadmin
1309
-	 *
1310
-	 * @PasswordConfirmationRequired
1311
-	 *
1312
-	 * @param string $userId
1313
-	 * @param string $groupid
1314
-	 * @return DataResponse
1315
-	 * @throws OCSException
1316
-	 */
1317
-	public function addSubAdmin(string $userId, string $groupid): DataResponse {
1318
-		$group = $this->groupManager->get($groupid);
1319
-		$user = $this->userManager->get($userId);
1320
-
1321
-		// Check if the user exists
1322
-		if ($user === null) {
1323
-			throw new OCSException('User does not exist', 101);
1324
-		}
1325
-		// Check if group exists
1326
-		if ($group === null) {
1327
-			throw new OCSException('Group does not exist', 102);
1328
-		}
1329
-		// Check if trying to make subadmin of admin group
1330
-		if ($group->getGID() === 'admin') {
1331
-			throw new OCSException('Cannot create subadmins for admin group', 103);
1332
-		}
1333
-
1334
-		$subAdminManager = $this->groupManager->getSubAdmin();
1335
-
1336
-		// We cannot be subadmin twice
1337
-		if ($subAdminManager->isSubAdminOfGroup($user, $group)) {
1338
-			return new DataResponse();
1339
-		}
1340
-		// Go
1341
-		$subAdminManager->createSubAdmin($user, $group);
1342
-		return new DataResponse();
1343
-	}
1344
-
1345
-	/**
1346
-	 * Removes a subadmin from a group
1347
-	 *
1348
-	 * @PasswordConfirmationRequired
1349
-	 *
1350
-	 * @param string $userId
1351
-	 * @param string $groupid
1352
-	 * @return DataResponse
1353
-	 * @throws OCSException
1354
-	 */
1355
-	public function removeSubAdmin(string $userId, string $groupid): DataResponse {
1356
-		$group = $this->groupManager->get($groupid);
1357
-		$user = $this->userManager->get($userId);
1358
-		$subAdminManager = $this->groupManager->getSubAdmin();
1359
-
1360
-		// Check if the user exists
1361
-		if ($user === null) {
1362
-			throw new OCSException('User does not exist', 101);
1363
-		}
1364
-		// Check if the group exists
1365
-		if ($group === null) {
1366
-			throw new OCSException('Group does not exist', 101);
1367
-		}
1368
-		// Check if they are a subadmin of this said group
1369
-		if (!$subAdminManager->isSubAdminOfGroup($user, $group)) {
1370
-			throw new OCSException('User is not a subadmin of this group', 102);
1371
-		}
1372
-
1373
-		// Go
1374
-		$subAdminManager->deleteSubAdmin($user, $group);
1375
-		return new DataResponse();
1376
-	}
1377
-
1378
-	/**
1379
-	 * Get the groups a user is a subadmin of
1380
-	 *
1381
-	 * @param string $userId
1382
-	 * @return DataResponse
1383
-	 * @throws OCSException
1384
-	 */
1385
-	public function getUserSubAdminGroups(string $userId): DataResponse {
1386
-		$groups = $this->getUserSubAdminGroupsData($userId);
1387
-		return new DataResponse($groups);
1388
-	}
1389
-
1390
-	/**
1391
-	 * @NoAdminRequired
1392
-	 * @PasswordConfirmationRequired
1393
-	 *
1394
-	 * resend welcome message
1395
-	 *
1396
-	 * @param string $userId
1397
-	 * @return DataResponse
1398
-	 * @throws OCSException
1399
-	 */
1400
-	public function resendWelcomeMessage(string $userId): DataResponse {
1401
-		$currentLoggedInUser = $this->userSession->getUser();
1402
-
1403
-		$targetUser = $this->userManager->get($userId);
1404
-		if ($targetUser === null) {
1405
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1406
-		}
1407
-
1408
-		// Check if admin / subadmin
1409
-		$subAdminManager = $this->groupManager->getSubAdmin();
1410
-		if (
1411
-			!$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
1412
-			&& !$this->groupManager->isAdmin($currentLoggedInUser->getUID())
1413
-		) {
1414
-			// No rights
1415
-			throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1416
-		}
1417
-
1418
-		$email = $targetUser->getEMailAddress();
1419
-		if ($email === '' || $email === null) {
1420
-			throw new OCSException('Email address not available', 101);
1421
-		}
1422
-
1423
-		try {
1424
-			$emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
1425
-			$this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
1426
-		} catch (\Exception $e) {
1427
-			$this->logger->error(
1428
-				"Can't send new user mail to $email",
1429
-				[
1430
-					'app' => 'settings',
1431
-					'exception' => $e,
1432
-				]
1433
-			);
1434
-			throw new OCSException('Sending email failed', 102);
1435
-		}
1436
-
1437
-		return new DataResponse();
1438
-	}
80
+    /** @var IURLGenerator */
81
+    protected $urlGenerator;
82
+    /** @var LoggerInterface */
83
+    private $logger;
84
+    /** @var IFactory */
85
+    protected $l10nFactory;
86
+    /** @var NewUserMailHelper */
87
+    private $newUserMailHelper;
88
+    /** @var ISecureRandom */
89
+    private $secureRandom;
90
+    /** @var RemoteWipe */
91
+    private $remoteWipe;
92
+    /** @var KnownUserService */
93
+    private $knownUserService;
94
+    /** @var IEventDispatcher */
95
+    private $eventDispatcher;
96
+
97
+    public function __construct(
98
+        string $appName,
99
+        IRequest $request,
100
+        IUserManager $userManager,
101
+        IConfig $config,
102
+        IGroupManager $groupManager,
103
+        IUserSession $userSession,
104
+        IAccountManager $accountManager,
105
+        IURLGenerator $urlGenerator,
106
+        LoggerInterface $logger,
107
+        IFactory $l10nFactory,
108
+        NewUserMailHelper $newUserMailHelper,
109
+        ISecureRandom $secureRandom,
110
+        RemoteWipe $remoteWipe,
111
+        KnownUserService $knownUserService,
112
+        IEventDispatcher $eventDispatcher
113
+    ) {
114
+        parent::__construct(
115
+            $appName,
116
+            $request,
117
+            $userManager,
118
+            $config,
119
+            $groupManager,
120
+            $userSession,
121
+            $accountManager,
122
+            $l10nFactory
123
+        );
124
+
125
+        $this->urlGenerator = $urlGenerator;
126
+        $this->logger = $logger;
127
+        $this->l10nFactory = $l10nFactory;
128
+        $this->newUserMailHelper = $newUserMailHelper;
129
+        $this->secureRandom = $secureRandom;
130
+        $this->remoteWipe = $remoteWipe;
131
+        $this->knownUserService = $knownUserService;
132
+        $this->eventDispatcher = $eventDispatcher;
133
+    }
134
+
135
+    /**
136
+     * @NoAdminRequired
137
+     *
138
+     * returns a list of users
139
+     *
140
+     * @param string $search
141
+     * @param int $limit
142
+     * @param int $offset
143
+     * @return DataResponse
144
+     */
145
+    public function getUsers(string $search = '', int $limit = null, int $offset = 0): DataResponse {
146
+        $user = $this->userSession->getUser();
147
+        $users = [];
148
+
149
+        // Admin? Or SubAdmin?
150
+        $uid = $user->getUID();
151
+        $subAdminManager = $this->groupManager->getSubAdmin();
152
+        if ($this->groupManager->isAdmin($uid)) {
153
+            $users = $this->userManager->search($search, $limit, $offset);
154
+        } elseif ($subAdminManager->isSubAdmin($user)) {
155
+            $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
156
+            foreach ($subAdminOfGroups as $key => $group) {
157
+                $subAdminOfGroups[$key] = $group->getGID();
158
+            }
159
+
160
+            $users = [];
161
+            foreach ($subAdminOfGroups as $group) {
162
+                $users = array_merge($users, $this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
163
+            }
164
+        }
165
+
166
+        $users = array_keys($users);
167
+
168
+        return new DataResponse([
169
+            'users' => $users
170
+        ]);
171
+    }
172
+
173
+    /**
174
+     * @NoAdminRequired
175
+     *
176
+     * returns a list of users and their data
177
+     */
178
+    public function getUsersDetails(string $search = '', int $limit = null, int $offset = 0): DataResponse {
179
+        $currentUser = $this->userSession->getUser();
180
+        $users = [];
181
+
182
+        // Admin? Or SubAdmin?
183
+        $uid = $currentUser->getUID();
184
+        $subAdminManager = $this->groupManager->getSubAdmin();
185
+        if ($this->groupManager->isAdmin($uid)) {
186
+            $users = $this->userManager->search($search, $limit, $offset);
187
+            $users = array_keys($users);
188
+        } elseif ($subAdminManager->isSubAdmin($currentUser)) {
189
+            $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
190
+            foreach ($subAdminOfGroups as $key => $group) {
191
+                $subAdminOfGroups[$key] = $group->getGID();
192
+            }
193
+
194
+            $users = [];
195
+            foreach ($subAdminOfGroups as $group) {
196
+                $users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
197
+            }
198
+            $users = array_merge(...$users);
199
+        }
200
+
201
+        $usersDetails = [];
202
+        foreach ($users as $userId) {
203
+            $userId = (string) $userId;
204
+            $userData = $this->getUserData($userId);
205
+            // Do not insert empty entry
206
+            if (!empty($userData)) {
207
+                $usersDetails[$userId] = $userData;
208
+            } else {
209
+                // Logged user does not have permissions to see this user
210
+                // only showing its id
211
+                $usersDetails[$userId] = ['id' => $userId];
212
+            }
213
+        }
214
+
215
+        return new DataResponse([
216
+            'users' => $usersDetails
217
+        ]);
218
+    }
219
+
220
+
221
+    /**
222
+     * @NoAdminRequired
223
+     * @NoSubAdminRequired
224
+     *
225
+     * @param string $location
226
+     * @param array $search
227
+     * @return DataResponse
228
+     */
229
+    public function searchByPhoneNumbers(string $location, array $search): DataResponse {
230
+        $phoneUtil = PhoneNumberUtil::getInstance();
231
+
232
+        if ($phoneUtil->getCountryCodeForRegion($location) === 0) {
233
+            // Not a valid region code
234
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
235
+        }
236
+
237
+        /** @var IUser $user */
238
+        $user = $this->userSession->getUser();
239
+        $knownTo = $user->getUID();
240
+        $defaultPhoneRegion = $this->config->getSystemValueString('default_phone_region');
241
+
242
+        $normalizedNumberToKey = [];
243
+        foreach ($search as $key => $phoneNumbers) {
244
+            foreach ($phoneNumbers as $phone) {
245
+                try {
246
+                    $phoneNumber = $phoneUtil->parse($phone, $location);
247
+                    if ($phoneUtil->isValidNumber($phoneNumber)) {
248
+                        $normalizedNumber = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
249
+                        $normalizedNumberToKey[$normalizedNumber] = (string) $key;
250
+                    }
251
+                } catch (NumberParseException $e) {
252
+                }
253
+
254
+                if ($defaultPhoneRegion !== '' && $defaultPhoneRegion !== $location && strpos($phone, '0') === 0) {
255
+                    // If the number has a leading zero (no country code),
256
+                    // we also check the default phone region of the instance,
257
+                    // when it's different to the user's given region.
258
+                    try {
259
+                        $phoneNumber = $phoneUtil->parse($phone, $defaultPhoneRegion);
260
+                        if ($phoneUtil->isValidNumber($phoneNumber)) {
261
+                            $normalizedNumber = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
262
+                            $normalizedNumberToKey[$normalizedNumber] = (string) $key;
263
+                        }
264
+                    } catch (NumberParseException $e) {
265
+                    }
266
+                }
267
+            }
268
+        }
269
+
270
+        $phoneNumbers = array_keys($normalizedNumberToKey);
271
+
272
+        if (empty($phoneNumbers)) {
273
+            return new DataResponse();
274
+        }
275
+
276
+        // Cleanup all previous entries and only allow new matches
277
+        $this->knownUserService->deleteKnownTo($knownTo);
278
+
279
+        $userMatches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, $phoneNumbers);
280
+
281
+        if (empty($userMatches)) {
282
+            return new DataResponse();
283
+        }
284
+
285
+        $cloudUrl = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
286
+        if (strpos($cloudUrl, 'http://') === 0) {
287
+            $cloudUrl = substr($cloudUrl, strlen('http://'));
288
+        } elseif (strpos($cloudUrl, 'https://') === 0) {
289
+            $cloudUrl = substr($cloudUrl, strlen('https://'));
290
+        }
291
+
292
+        $matches = [];
293
+        foreach ($userMatches as $phone => $userId) {
294
+            // Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book
295
+            $matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl;
296
+            $this->knownUserService->storeIsKnownToUser($knownTo, $userId);
297
+        }
298
+
299
+        return new DataResponse($matches);
300
+    }
301
+
302
+    /**
303
+     * @throws OCSException
304
+     */
305
+    private function createNewUserId(): string {
306
+        $attempts = 0;
307
+        do {
308
+            $uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
309
+            if (!$this->userManager->userExists($uidCandidate)) {
310
+                return $uidCandidate;
311
+            }
312
+            $attempts++;
313
+        } while ($attempts < 10);
314
+        throw new OCSException('Could not create non-existing user id', 111);
315
+    }
316
+
317
+    /**
318
+     * @PasswordConfirmationRequired
319
+     * @NoAdminRequired
320
+     *
321
+     * @param string $userid
322
+     * @param string $password
323
+     * @param string $displayName
324
+     * @param string $email
325
+     * @param array $groups
326
+     * @param array $subadmin
327
+     * @param string $quota
328
+     * @param string $language
329
+     * @return DataResponse
330
+     * @throws OCSException
331
+     */
332
+    public function addUser(
333
+        string $userid,
334
+        string $password = '',
335
+        string $displayName = '',
336
+        string $email = '',
337
+        array $groups = [],
338
+        array $subadmin = [],
339
+        string $quota = '',
340
+        string $language = '',
341
+        ?string $manager = null,
342
+    ): DataResponse {
343
+        $user = $this->userSession->getUser();
344
+        $isAdmin = $this->groupManager->isAdmin($user->getUID());
345
+        $subAdminManager = $this->groupManager->getSubAdmin();
346
+
347
+        if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
348
+            $userid = $this->createNewUserId();
349
+        }
350
+
351
+        if ($this->userManager->userExists($userid)) {
352
+            $this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
353
+            throw new OCSException($this->l10nFactory->get('provisioning_api')->t('User already exists'), 102);
354
+        }
355
+
356
+        if ($groups !== []) {
357
+            foreach ($groups as $group) {
358
+                if (!$this->groupManager->groupExists($group)) {
359
+                    throw new OCSException('group ' . $group . ' does not exist', 104);
360
+                }
361
+                if (!$isAdmin && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
362
+                    throw new OCSException('insufficient privileges for group ' . $group, 105);
363
+                }
364
+            }
365
+        } else {
366
+            if (!$isAdmin) {
367
+                throw new OCSException('no group specified (required for subadmins)', 106);
368
+            }
369
+        }
370
+
371
+        $subadminGroups = [];
372
+        if ($subadmin !== []) {
373
+            foreach ($subadmin as $groupid) {
374
+                $group = $this->groupManager->get($groupid);
375
+                // Check if group exists
376
+                if ($group === null) {
377
+                    throw new OCSException('Subadmin group does not exist', 102);
378
+                }
379
+                // Check if trying to make subadmin of admin group
380
+                if ($group->getGID() === 'admin') {
381
+                    throw new OCSException('Cannot create subadmins for admin group', 103);
382
+                }
383
+                // Check if has permission to promote subadmins
384
+                if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin) {
385
+                    throw new OCSForbiddenException('No permissions to promote subadmins');
386
+                }
387
+                $subadminGroups[] = $group;
388
+            }
389
+        }
390
+
391
+        $generatePasswordResetToken = false;
392
+        if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
393
+            throw new OCSException('Invalid password value', 101);
394
+        }
395
+        if ($password === '') {
396
+            if ($email === '') {
397
+                throw new OCSException('To send a password link to the user an email address is required.', 108);
398
+            }
399
+
400
+            $passwordEvent = new GenerateSecurePasswordEvent();
401
+            $this->eventDispatcher->dispatchTyped($passwordEvent);
402
+
403
+            $password = $passwordEvent->getPassword();
404
+            if ($password === null) {
405
+                // Fallback: ensure to pass password_policy in any case
406
+                $password = $this->secureRandom->generate(10)
407
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_UPPER)
408
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_LOWER)
409
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_DIGITS)
410
+                    . $this->secureRandom->generate(1, ISecureRandom::CHAR_SYMBOLS);
411
+            }
412
+            $generatePasswordResetToken = true;
413
+        }
414
+
415
+        if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
416
+            throw new OCSException('Required email address was not provided', 110);
417
+        }
418
+
419
+        try {
420
+            $newUser = $this->userManager->createUser($userid, $password);
421
+            $this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
422
+
423
+            foreach ($groups as $group) {
424
+                $this->groupManager->get($group)->addUser($newUser);
425
+                $this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
426
+            }
427
+            foreach ($subadminGroups as $group) {
428
+                $subAdminManager->createSubAdmin($newUser, $group);
429
+            }
430
+
431
+            if ($displayName !== '') {
432
+                try {
433
+                    $this->editUser($userid, self::USER_FIELD_DISPLAYNAME, $displayName);
434
+                } catch (OCSException $e) {
435
+                    if ($newUser instanceof IUser) {
436
+                        $newUser->delete();
437
+                    }
438
+                    throw $e;
439
+                }
440
+            }
441
+
442
+            if ($quota !== '') {
443
+                $this->editUser($userid, self::USER_FIELD_QUOTA, $quota);
444
+            }
445
+
446
+            if ($language !== '') {
447
+                $this->editUser($userid, self::USER_FIELD_LANGUAGE, $language);
448
+            }
449
+
450
+            /**
451
+             * null -> nothing sent
452
+             * '' -> unset manager
453
+             * else -> set manager
454
+             */
455
+            if ($manager !== null) {
456
+                $this->editUser($userid, self::USER_FIELD_MANAGER, $manager);
457
+            }
458
+
459
+            // Send new user mail only if a mail is set
460
+            if ($email !== '') {
461
+                $newUser->setEMailAddress($email);
462
+                if ($this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes') {
463
+                    try {
464
+                        $emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken);
465
+                        $this->newUserMailHelper->sendMail($newUser, $emailTemplate);
466
+                    } catch (\Exception $e) {
467
+                        // Mail could be failing hard or just be plain not configured
468
+                        // Logging error as it is the hardest of the two
469
+                        $this->logger->error(
470
+                            "Unable to send the invitation mail to $email",
471
+                            [
472
+                                'app' => 'ocs_api',
473
+                                'exception' => $e,
474
+                            ]
475
+                        );
476
+                    }
477
+                }
478
+            }
479
+
480
+            return new DataResponse(['id' => $userid]);
481
+        } catch (HintException $e) {
482
+            $this->logger->warning(
483
+                'Failed addUser attempt with hint exception.',
484
+                [
485
+                    'app' => 'ocs_api',
486
+                    'exception' => $e,
487
+                ]
488
+            );
489
+            throw new OCSException($e->getHint(), 107);
490
+        } catch (OCSException $e) {
491
+            $this->logger->warning(
492
+                'Failed addUser attempt with ocs exception.',
493
+                [
494
+                    'app' => 'ocs_api',
495
+                    'exception' => $e,
496
+                ]
497
+            );
498
+            throw $e;
499
+        } catch (InvalidArgumentException $e) {
500
+            $this->logger->error(
501
+                'Failed addUser attempt with invalid argument exception.',
502
+                [
503
+                    'app' => 'ocs_api',
504
+                    'exception' => $e,
505
+                ]
506
+            );
507
+            throw new OCSException($e->getMessage(), 101);
508
+        } catch (\Exception $e) {
509
+            $this->logger->error(
510
+                'Failed addUser attempt with exception.',
511
+                [
512
+                    'app' => 'ocs_api',
513
+                    'exception' => $e
514
+                ]
515
+            );
516
+            throw new OCSException('Bad request', 101);
517
+        }
518
+    }
519
+
520
+    /**
521
+     * @NoAdminRequired
522
+     * @NoSubAdminRequired
523
+     *
524
+     * gets user info
525
+     *
526
+     * @param string $userId
527
+     * @return DataResponse
528
+     * @throws OCSException
529
+     */
530
+    public function getUser(string $userId): DataResponse {
531
+        $includeScopes = false;
532
+        $currentUser = $this->userSession->getUser();
533
+        if ($currentUser && $currentUser->getUID() === $userId) {
534
+            $includeScopes = true;
535
+        }
536
+
537
+        $data = $this->getUserData($userId, $includeScopes);
538
+        // getUserData returns empty array if not enough permissions
539
+        if (empty($data)) {
540
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
541
+        }
542
+        return new DataResponse($data);
543
+    }
544
+
545
+    /**
546
+     * @NoAdminRequired
547
+     * @NoSubAdminRequired
548
+     *
549
+     * gets user info from the currently logged in user
550
+     *
551
+     * @return DataResponse
552
+     * @throws OCSException
553
+     */
554
+    public function getCurrentUser(): DataResponse {
555
+        $user = $this->userSession->getUser();
556
+        if ($user) {
557
+            $data = $this->getUserData($user->getUID(), true);
558
+            return new DataResponse($data);
559
+        }
560
+
561
+        throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
562
+    }
563
+
564
+    /**
565
+     * @NoAdminRequired
566
+     * @NoSubAdminRequired
567
+     *
568
+     * @return DataResponse
569
+     * @throws OCSException
570
+     */
571
+    public function getEditableFields(): DataResponse {
572
+        $currentLoggedInUser = $this->userSession->getUser();
573
+        if (!$currentLoggedInUser instanceof IUser) {
574
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
575
+        }
576
+
577
+        return $this->getEditableFieldsForUser($currentLoggedInUser->getUID());
578
+    }
579
+
580
+    /**
581
+     * @NoAdminRequired
582
+     * @NoSubAdminRequired
583
+     *
584
+     * @param string $userId
585
+     * @return DataResponse
586
+     * @throws OCSException
587
+     */
588
+    public function getEditableFieldsForUser(string $userId): DataResponse {
589
+        $currentLoggedInUser = $this->userSession->getUser();
590
+        if (!$currentLoggedInUser instanceof IUser) {
591
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
592
+        }
593
+
594
+        $permittedFields = [];
595
+
596
+        if ($userId !== $currentLoggedInUser->getUID()) {
597
+            $targetUser = $this->userManager->get($userId);
598
+            if (!$targetUser instanceof IUser) {
599
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
600
+            }
601
+
602
+            $subAdminManager = $this->groupManager->getSubAdmin();
603
+            if (
604
+                !$this->groupManager->isAdmin($currentLoggedInUser->getUID())
605
+                && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
606
+            ) {
607
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
608
+            }
609
+        } else {
610
+            $targetUser = $currentLoggedInUser;
611
+        }
612
+
613
+        // Editing self (display, email)
614
+        if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
615
+            if (
616
+                $targetUser->getBackend() instanceof ISetDisplayNameBackend
617
+                || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
618
+            ) {
619
+                $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
620
+            }
621
+            $permittedFields[] = IAccountManager::PROPERTY_EMAIL;
622
+        }
623
+
624
+        $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
625
+        $permittedFields[] = IAccountManager::PROPERTY_PHONE;
626
+        $permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
627
+        $permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
628
+        $permittedFields[] = IAccountManager::PROPERTY_TWITTER;
629
+        $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
630
+        $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
631
+        $permittedFields[] = IAccountManager::PROPERTY_ROLE;
632
+        $permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
633
+        $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
634
+        $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
635
+
636
+        return new DataResponse($permittedFields);
637
+    }
638
+
639
+    /**
640
+     * @NoAdminRequired
641
+     * @NoSubAdminRequired
642
+     * @PasswordConfirmationRequired
643
+     * @UserRateThrottle(limit=5, period=60)
644
+     *
645
+     * @throws OCSException
646
+     */
647
+    public function editUserMultiValue(
648
+        string $userId,
649
+        string $collectionName,
650
+        string $key,
651
+        string $value
652
+    ): DataResponse {
653
+        $currentLoggedInUser = $this->userSession->getUser();
654
+        if ($currentLoggedInUser === null) {
655
+            throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
656
+        }
657
+
658
+        $targetUser = $this->userManager->get($userId);
659
+        if ($targetUser === null) {
660
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
661
+        }
662
+
663
+        $subAdminManager = $this->groupManager->getSubAdmin();
664
+        $isAdminOrSubadmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID())
665
+            || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser);
666
+
667
+        $permittedFields = [];
668
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
669
+            // Editing self (display, email)
670
+            $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
671
+            $permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX;
672
+        } else {
673
+            // Check if admin / subadmin
674
+            if ($isAdminOrSubadmin) {
675
+                // They have permissions over the user
676
+                $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
677
+            } else {
678
+                // No rights
679
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
680
+            }
681
+        }
682
+
683
+        // Check if permitted to edit this field
684
+        if (!in_array($collectionName, $permittedFields)) {
685
+            throw new OCSException('', 103);
686
+        }
687
+
688
+        switch ($collectionName) {
689
+            case IAccountManager::COLLECTION_EMAIL:
690
+                $userAccount = $this->accountManager->getAccount($targetUser);
691
+                $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
692
+                $mailCollection->removePropertyByValue($key);
693
+                if ($value !== '') {
694
+                    $mailCollection->addPropertyWithDefaults($value);
695
+                    $property = $mailCollection->getPropertyByValue($key);
696
+                    if ($isAdminOrSubadmin && $property) {
697
+                        // admin set mails are auto-verified
698
+                        $property->setLocallyVerified(IAccountManager::VERIFIED);
699
+                    }
700
+                }
701
+                $this->accountManager->updateAccount($userAccount);
702
+                break;
703
+
704
+            case IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX:
705
+                $userAccount = $this->accountManager->getAccount($targetUser);
706
+                $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
707
+                $targetProperty = null;
708
+                foreach ($mailCollection->getProperties() as $property) {
709
+                    if ($property->getValue() === $key) {
710
+                        $targetProperty = $property;
711
+                        break;
712
+                    }
713
+                }
714
+                if ($targetProperty instanceof IAccountProperty) {
715
+                    try {
716
+                        $targetProperty->setScope($value);
717
+                        $this->accountManager->updateAccount($userAccount);
718
+                    } catch (InvalidArgumentException $e) {
719
+                        throw new OCSException('', 102);
720
+                    }
721
+                } else {
722
+                    throw new OCSException('', 102);
723
+                }
724
+                break;
725
+
726
+            default:
727
+                throw new OCSException('', 103);
728
+        }
729
+        return new DataResponse();
730
+    }
731
+
732
+    /**
733
+     * @NoAdminRequired
734
+     * @NoSubAdminRequired
735
+     * @PasswordConfirmationRequired
736
+     * @UserRateThrottle(limit=50, period=600)
737
+     *
738
+     * edit users
739
+     *
740
+     * @param string $userId
741
+     * @param string $key
742
+     * @param string $value
743
+     * @return DataResponse
744
+     * @throws OCSException
745
+     */
746
+    public function editUser(string $userId, string $key, string $value): DataResponse {
747
+        $currentLoggedInUser = $this->userSession->getUser();
748
+
749
+        $targetUser = $this->userManager->get($userId);
750
+        if ($targetUser === null) {
751
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
752
+        }
753
+
754
+        $permittedFields = [];
755
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
756
+            // Editing self (display, email)
757
+            if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
758
+                if (
759
+                    $targetUser->getBackend() instanceof ISetDisplayNameBackend
760
+                    || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
761
+                ) {
762
+                    $permittedFields[] = self::USER_FIELD_DISPLAYNAME;
763
+                    $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
764
+                }
765
+                $permittedFields[] = IAccountManager::PROPERTY_EMAIL;
766
+            }
767
+
768
+            $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX;
769
+            $permittedFields[] = IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX;
770
+
771
+            $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
772
+
773
+            $permittedFields[] = self::USER_FIELD_PASSWORD;
774
+            $permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
775
+            if (
776
+                $this->config->getSystemValue('force_language', false) === false ||
777
+                $this->groupManager->isAdmin($currentLoggedInUser->getUID())
778
+            ) {
779
+                $permittedFields[] = self::USER_FIELD_LANGUAGE;
780
+            }
781
+
782
+            if (
783
+                $this->config->getSystemValue('force_locale', false) === false ||
784
+                $this->groupManager->isAdmin($currentLoggedInUser->getUID())
785
+            ) {
786
+                $permittedFields[] = self::USER_FIELD_LOCALE;
787
+            }
788
+
789
+            $permittedFields[] = IAccountManager::PROPERTY_PHONE;
790
+            $permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
791
+            $permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
792
+            $permittedFields[] = IAccountManager::PROPERTY_TWITTER;
793
+            $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
794
+            $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
795
+            $permittedFields[] = IAccountManager::PROPERTY_ROLE;
796
+            $permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
797
+            $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
798
+            $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
799
+            $permittedFields[] = IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX;
800
+            $permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX;
801
+            $permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX;
802
+            $permittedFields[] = IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX;
803
+            $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX;
804
+            $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX;
805
+            $permittedFields[] = IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX;
806
+            $permittedFields[] = IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX;
807
+            $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX;
808
+            $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX;
809
+
810
+            $permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
811
+
812
+            // If admin they can edit their own quota and manager
813
+            if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
814
+                $permittedFields[] = self::USER_FIELD_QUOTA;
815
+                $permittedFields[] = self::USER_FIELD_MANAGER;
816
+
817
+            }
818
+        } else {
819
+            // Check if admin / subadmin
820
+            $subAdminManager = $this->groupManager->getSubAdmin();
821
+            if (
822
+                $this->groupManager->isAdmin($currentLoggedInUser->getUID())
823
+                || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
824
+            ) {
825
+                // They have permissions over the user
826
+                if (
827
+                    $targetUser->getBackend() instanceof ISetDisplayNameBackend
828
+                    || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)
829
+                ) {
830
+                    $permittedFields[] = self::USER_FIELD_DISPLAYNAME;
831
+                    $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
832
+                }
833
+                $permittedFields[] = IAccountManager::PROPERTY_EMAIL;
834
+                $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
835
+                $permittedFields[] = self::USER_FIELD_PASSWORD;
836
+                $permittedFields[] = self::USER_FIELD_LANGUAGE;
837
+                $permittedFields[] = self::USER_FIELD_LOCALE;
838
+                $permittedFields[] = IAccountManager::PROPERTY_PHONE;
839
+                $permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
840
+                $permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
841
+                $permittedFields[] = IAccountManager::PROPERTY_TWITTER;
842
+                $permittedFields[] = IAccountManager::PROPERTY_FEDIVERSE;
843
+                $permittedFields[] = IAccountManager::PROPERTY_ORGANISATION;
844
+                $permittedFields[] = IAccountManager::PROPERTY_ROLE;
845
+                $permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
846
+                $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
847
+                $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
848
+                $permittedFields[] = self::USER_FIELD_QUOTA;
849
+                $permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
850
+                $permittedFields[] = self::USER_FIELD_MANAGER;
851
+            } else {
852
+                // No rights
853
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
854
+            }
855
+        }
856
+        // Check if permitted to edit this field
857
+        if (!in_array($key, $permittedFields)) {
858
+            throw new OCSException('', 103);
859
+        }
860
+        // Process the edit
861
+        switch ($key) {
862
+            case self::USER_FIELD_DISPLAYNAME:
863
+            case IAccountManager::PROPERTY_DISPLAYNAME:
864
+                try {
865
+                    $targetUser->setDisplayName($value);
866
+                } catch (InvalidArgumentException $e) {
867
+                    throw new OCSException($e->getMessage(), 101);
868
+                }
869
+                break;
870
+            case self::USER_FIELD_QUOTA:
871
+                $quota = $value;
872
+                if ($quota !== 'none' && $quota !== 'default') {
873
+                    if (is_numeric($quota)) {
874
+                        $quota = (float) $quota;
875
+                    } else {
876
+                        $quota = \OCP\Util::computerFileSize($quota);
877
+                    }
878
+                    if ($quota === false) {
879
+                        throw new OCSException('Invalid quota value ' . $value, 102);
880
+                    }
881
+                    if ($quota === -1) {
882
+                        $quota = 'none';
883
+                    } else {
884
+                        $maxQuota = (int) $this->config->getAppValue('files', 'max_quota', '-1');
885
+                        if ($maxQuota !== -1 && $quota > $maxQuota) {
886
+                            throw new OCSException('Invalid quota value. ' . $value . ' is exceeding the maximum quota', 102);
887
+                        }
888
+                        $quota = \OCP\Util::humanFileSize($quota);
889
+                    }
890
+                }
891
+                // no else block because quota can be set to 'none' in previous if
892
+                if ($quota === 'none') {
893
+                    $allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1';
894
+                    if (!$allowUnlimitedQuota) {
895
+                        throw new OCSException('Unlimited quota is forbidden on this instance', 102);
896
+                    }
897
+                }
898
+                $targetUser->setQuota($quota);
899
+                break;
900
+            case self::USER_FIELD_MANAGER:
901
+                $targetUser->setManagerUids([$value]);
902
+                break;
903
+            case self::USER_FIELD_PASSWORD:
904
+                try {
905
+                    if (strlen($value) > IUserManager::MAX_PASSWORD_LENGTH) {
906
+                        throw new OCSException('Invalid password value', 102);
907
+                    }
908
+                    if (!$targetUser->canChangePassword()) {
909
+                        throw new OCSException('Setting the password is not supported by the users backend', 103);
910
+                    }
911
+                    $targetUser->setPassword($value);
912
+                } catch (HintException $e) { // password policy error
913
+                    throw new OCSException($e->getMessage(), 103);
914
+                }
915
+                break;
916
+            case self::USER_FIELD_LANGUAGE:
917
+                $languagesCodes = $this->l10nFactory->findAvailableLanguages();
918
+                if (!in_array($value, $languagesCodes, true) && $value !== 'en') {
919
+                    throw new OCSException('Invalid language', 102);
920
+                }
921
+                $this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value);
922
+                break;
923
+            case self::USER_FIELD_LOCALE:
924
+                if (!$this->l10nFactory->localeExists($value)) {
925
+                    throw new OCSException('Invalid locale', 102);
926
+                }
927
+                $this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
928
+                break;
929
+            case self::USER_FIELD_NOTIFICATION_EMAIL:
930
+                $success = false;
931
+                if ($value === '' || filter_var($value, FILTER_VALIDATE_EMAIL)) {
932
+                    try {
933
+                        $targetUser->setPrimaryEMailAddress($value);
934
+                        $success = true;
935
+                    } catch (InvalidArgumentException $e) {
936
+                        $this->logger->info(
937
+                            'Cannot set primary email, because provided address is not verified',
938
+                            [
939
+                                'app' => 'provisioning_api',
940
+                                'exception' => $e,
941
+                            ]
942
+                        );
943
+                    }
944
+                }
945
+                if (!$success) {
946
+                    throw new OCSException('', 102);
947
+                }
948
+                break;
949
+            case IAccountManager::PROPERTY_EMAIL:
950
+                if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
951
+                    $targetUser->setEMailAddress($value);
952
+                } else {
953
+                    throw new OCSException('', 102);
954
+                }
955
+                break;
956
+            case IAccountManager::COLLECTION_EMAIL:
957
+                if (filter_var($value, FILTER_VALIDATE_EMAIL) && $value !== $targetUser->getSystemEMailAddress()) {
958
+                    $userAccount = $this->accountManager->getAccount($targetUser);
959
+                    $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL);
960
+
961
+                    if ($mailCollection->getPropertyByValue($value)) {
962
+                        throw new OCSException('', 102);
963
+                    }
964
+
965
+                    $mailCollection->addPropertyWithDefaults($value);
966
+                    $this->accountManager->updateAccount($userAccount);
967
+                } else {
968
+                    throw new OCSException('', 102);
969
+                }
970
+                break;
971
+            case IAccountManager::PROPERTY_PHONE:
972
+            case IAccountManager::PROPERTY_ADDRESS:
973
+            case IAccountManager::PROPERTY_WEBSITE:
974
+            case IAccountManager::PROPERTY_TWITTER:
975
+            case IAccountManager::PROPERTY_FEDIVERSE:
976
+            case IAccountManager::PROPERTY_ORGANISATION:
977
+            case IAccountManager::PROPERTY_ROLE:
978
+            case IAccountManager::PROPERTY_HEADLINE:
979
+            case IAccountManager::PROPERTY_BIOGRAPHY:
980
+                $userAccount = $this->accountManager->getAccount($targetUser);
981
+                try {
982
+                    $userProperty = $userAccount->getProperty($key);
983
+                    if ($userProperty->getValue() !== $value) {
984
+                        try {
985
+                            $userProperty->setValue($value);
986
+                            if ($userProperty->getName() === IAccountManager::PROPERTY_PHONE) {
987
+                                $this->knownUserService->deleteByContactUserId($targetUser->getUID());
988
+                            }
989
+                        } catch (InvalidArgumentException $e) {
990
+                            throw new OCSException('Invalid ' . $e->getMessage(), 102);
991
+                        }
992
+                    }
993
+                } catch (PropertyDoesNotExistException $e) {
994
+                    $userAccount->setProperty($key, $value, IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED);
995
+                }
996
+                try {
997
+                    $this->accountManager->updateAccount($userAccount);
998
+                } catch (InvalidArgumentException $e) {
999
+                    throw new OCSException('Invalid ' . $e->getMessage(), 102);
1000
+                }
1001
+                break;
1002
+            case IAccountManager::PROPERTY_PROFILE_ENABLED:
1003
+                $userAccount = $this->accountManager->getAccount($targetUser);
1004
+                try {
1005
+                    $userProperty = $userAccount->getProperty($key);
1006
+                    if ($userProperty->getValue() !== $value) {
1007
+                        $userProperty->setValue($value);
1008
+                    }
1009
+                } catch (PropertyDoesNotExistException $e) {
1010
+                    $userAccount->setProperty($key, $value, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED);
1011
+                }
1012
+                $this->accountManager->updateAccount($userAccount);
1013
+                break;
1014
+            case IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX:
1015
+            case IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX:
1016
+            case IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX:
1017
+            case IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX:
1018
+            case IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX:
1019
+            case IAccountManager::PROPERTY_TWITTER . self::SCOPE_SUFFIX:
1020
+            case IAccountManager::PROPERTY_FEDIVERSE . self::SCOPE_SUFFIX:
1021
+            case IAccountManager::PROPERTY_ORGANISATION . self::SCOPE_SUFFIX:
1022
+            case IAccountManager::PROPERTY_ROLE . self::SCOPE_SUFFIX:
1023
+            case IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX:
1024
+            case IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX:
1025
+            case IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX:
1026
+            case IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX:
1027
+                $propertyName = substr($key, 0, strlen($key) - strlen(self::SCOPE_SUFFIX));
1028
+                $userAccount = $this->accountManager->getAccount($targetUser);
1029
+                $userProperty = $userAccount->getProperty($propertyName);
1030
+                if ($userProperty->getScope() !== $value) {
1031
+                    try {
1032
+                        $userProperty->setScope($value);
1033
+                        $this->accountManager->updateAccount($userAccount);
1034
+                    } catch (InvalidArgumentException $e) {
1035
+                        throw new OCSException('Invalid ' . $e->getMessage(), 102);
1036
+                    }
1037
+                }
1038
+                break;
1039
+            default:
1040
+                throw new OCSException('', 103);
1041
+        }
1042
+        return new DataResponse();
1043
+    }
1044
+
1045
+    /**
1046
+     * @PasswordConfirmationRequired
1047
+     * @NoAdminRequired
1048
+     *
1049
+     * @param string $userId
1050
+     *
1051
+     * @return DataResponse
1052
+     *
1053
+     * @throws OCSException
1054
+     */
1055
+    public function wipeUserDevices(string $userId): DataResponse {
1056
+        /** @var IUser $currentLoggedInUser */
1057
+        $currentLoggedInUser = $this->userSession->getUser();
1058
+
1059
+        $targetUser = $this->userManager->get($userId);
1060
+
1061
+        if ($targetUser === null) {
1062
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1063
+        }
1064
+
1065
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1066
+            throw new OCSException('', 101);
1067
+        }
1068
+
1069
+        // If not permitted
1070
+        $subAdminManager = $this->groupManager->getSubAdmin();
1071
+        if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1072
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1073
+        }
1074
+
1075
+        $this->remoteWipe->markAllTokensForWipe($targetUser);
1076
+
1077
+        return new DataResponse();
1078
+    }
1079
+
1080
+    /**
1081
+     * @PasswordConfirmationRequired
1082
+     * @NoAdminRequired
1083
+     *
1084
+     * @param string $userId
1085
+     * @return DataResponse
1086
+     * @throws OCSException
1087
+     */
1088
+    public function deleteUser(string $userId): DataResponse {
1089
+        $currentLoggedInUser = $this->userSession->getUser();
1090
+
1091
+        $targetUser = $this->userManager->get($userId);
1092
+
1093
+        if ($targetUser === null) {
1094
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1095
+        }
1096
+
1097
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
1098
+            throw new OCSException('', 101);
1099
+        }
1100
+
1101
+        // If not permitted
1102
+        $subAdminManager = $this->groupManager->getSubAdmin();
1103
+        if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1104
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1105
+        }
1106
+
1107
+        // Go ahead with the delete
1108
+        if ($targetUser->delete()) {
1109
+            return new DataResponse();
1110
+        } else {
1111
+            throw new OCSException('', 101);
1112
+        }
1113
+    }
1114
+
1115
+    /**
1116
+     * @PasswordConfirmationRequired
1117
+     * @NoAdminRequired
1118
+     *
1119
+     * @param string $userId
1120
+     * @return DataResponse
1121
+     * @throws OCSException
1122
+     * @throws OCSForbiddenException
1123
+     */
1124
+    public function disableUser(string $userId): DataResponse {
1125
+        return $this->setEnabled($userId, false);
1126
+    }
1127
+
1128
+    /**
1129
+     * @PasswordConfirmationRequired
1130
+     * @NoAdminRequired
1131
+     *
1132
+     * @param string $userId
1133
+     * @return DataResponse
1134
+     * @throws OCSException
1135
+     * @throws OCSForbiddenException
1136
+     */
1137
+    public function enableUser(string $userId): DataResponse {
1138
+        return $this->setEnabled($userId, true);
1139
+    }
1140
+
1141
+    /**
1142
+     * @param string $userId
1143
+     * @param bool $value
1144
+     * @return DataResponse
1145
+     * @throws OCSException
1146
+     */
1147
+    private function setEnabled(string $userId, bool $value): DataResponse {
1148
+        $currentLoggedInUser = $this->userSession->getUser();
1149
+
1150
+        $targetUser = $this->userManager->get($userId);
1151
+        if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
1152
+            throw new OCSException('', 101);
1153
+        }
1154
+
1155
+        // If not permitted
1156
+        $subAdminManager = $this->groupManager->getSubAdmin();
1157
+        if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
1158
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1159
+        }
1160
+
1161
+        // enable/disable the user now
1162
+        $targetUser->setEnabled($value);
1163
+        return new DataResponse();
1164
+    }
1165
+
1166
+    /**
1167
+     * @NoAdminRequired
1168
+     * @NoSubAdminRequired
1169
+     *
1170
+     * @param string $userId
1171
+     * @return DataResponse
1172
+     * @throws OCSException
1173
+     */
1174
+    public function getUsersGroups(string $userId): DataResponse {
1175
+        $loggedInUser = $this->userSession->getUser();
1176
+
1177
+        $targetUser = $this->userManager->get($userId);
1178
+        if ($targetUser === null) {
1179
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1180
+        }
1181
+
1182
+        if ($targetUser->getUID() === $loggedInUser->getUID() || $this->groupManager->isAdmin($loggedInUser->getUID())) {
1183
+            // Self lookup or admin lookup
1184
+            return new DataResponse([
1185
+                'groups' => $this->groupManager->getUserGroupIds($targetUser)
1186
+            ]);
1187
+        } else {
1188
+            $subAdminManager = $this->groupManager->getSubAdmin();
1189
+
1190
+            // Looking up someone else
1191
+            if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
1192
+                // Return the group that the method caller is subadmin of for the user in question
1193
+                /** @var IGroup[] $getSubAdminsGroups */
1194
+                $getSubAdminsGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
1195
+                foreach ($getSubAdminsGroups as $key => $group) {
1196
+                    $getSubAdminsGroups[$key] = $group->getGID();
1197
+                }
1198
+                $groups = array_intersect(
1199
+                    $getSubAdminsGroups,
1200
+                    $this->groupManager->getUserGroupIds($targetUser)
1201
+                );
1202
+                return new DataResponse(['groups' => $groups]);
1203
+            } else {
1204
+                // Not permitted
1205
+                throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1206
+            }
1207
+        }
1208
+    }
1209
+
1210
+    /**
1211
+     * @PasswordConfirmationRequired
1212
+     * @NoAdminRequired
1213
+     *
1214
+     * @param string $userId
1215
+     * @param string $groupid
1216
+     * @return DataResponse
1217
+     * @throws OCSException
1218
+     */
1219
+    public function addToGroup(string $userId, string $groupid = ''): DataResponse {
1220
+        if ($groupid === '') {
1221
+            throw new OCSException('', 101);
1222
+        }
1223
+
1224
+        $group = $this->groupManager->get($groupid);
1225
+        $targetUser = $this->userManager->get($userId);
1226
+        if ($group === null) {
1227
+            throw new OCSException('', 102);
1228
+        }
1229
+        if ($targetUser === null) {
1230
+            throw new OCSException('', 103);
1231
+        }
1232
+
1233
+        // If they're not an admin, check they are a subadmin of the group in question
1234
+        $loggedInUser = $this->userSession->getUser();
1235
+        $subAdminManager = $this->groupManager->getSubAdmin();
1236
+        if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1237
+            throw new OCSException('', 104);
1238
+        }
1239
+
1240
+        // Add user to group
1241
+        $group->addUser($targetUser);
1242
+        return new DataResponse();
1243
+    }
1244
+
1245
+    /**
1246
+     * @PasswordConfirmationRequired
1247
+     * @NoAdminRequired
1248
+     *
1249
+     * @param string $userId
1250
+     * @param string $groupid
1251
+     * @return DataResponse
1252
+     * @throws OCSException
1253
+     */
1254
+    public function removeFromGroup(string $userId, string $groupid): DataResponse {
1255
+        $loggedInUser = $this->userSession->getUser();
1256
+
1257
+        if ($groupid === null || trim($groupid) === '') {
1258
+            throw new OCSException('', 101);
1259
+        }
1260
+
1261
+        $group = $this->groupManager->get($groupid);
1262
+        if ($group === null) {
1263
+            throw new OCSException('', 102);
1264
+        }
1265
+
1266
+        $targetUser = $this->userManager->get($userId);
1267
+        if ($targetUser === null) {
1268
+            throw new OCSException('', 103);
1269
+        }
1270
+
1271
+        // If they're not an admin, check they are a subadmin of the group in question
1272
+        $subAdminManager = $this->groupManager->getSubAdmin();
1273
+        if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
1274
+            throw new OCSException('', 104);
1275
+        }
1276
+
1277
+        // Check they aren't removing themselves from 'admin' or their 'subadmin; group
1278
+        if ($targetUser->getUID() === $loggedInUser->getUID()) {
1279
+            if ($this->groupManager->isAdmin($loggedInUser->getUID())) {
1280
+                if ($group->getGID() === 'admin') {
1281
+                    throw new OCSException('Cannot remove yourself from the admin group', 105);
1282
+                }
1283
+            } else {
1284
+                // Not an admin, so the user must be a subadmin of this group, but that is not allowed.
1285
+                throw new OCSException('Cannot remove yourself from this group as you are a SubAdmin', 105);
1286
+            }
1287
+        } elseif (!$this->groupManager->isAdmin($loggedInUser->getUID())) {
1288
+            /** @var IGroup[] $subAdminGroups */
1289
+            $subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
1290
+            $subAdminGroups = array_map(function (IGroup $subAdminGroup) {
1291
+                return $subAdminGroup->getGID();
1292
+            }, $subAdminGroups);
1293
+            $userGroups = $this->groupManager->getUserGroupIds($targetUser);
1294
+            $userSubAdminGroups = array_intersect($subAdminGroups, $userGroups);
1295
+
1296
+            if (count($userSubAdminGroups) <= 1) {
1297
+                // Subadmin must not be able to remove a user from all their subadmin groups.
1298
+                throw new OCSException('Not viable to remove user from the last group you are SubAdmin of', 105);
1299
+            }
1300
+        }
1301
+
1302
+        // Remove user from group
1303
+        $group->removeUser($targetUser);
1304
+        return new DataResponse();
1305
+    }
1306
+
1307
+    /**
1308
+     * Creates a subadmin
1309
+     *
1310
+     * @PasswordConfirmationRequired
1311
+     *
1312
+     * @param string $userId
1313
+     * @param string $groupid
1314
+     * @return DataResponse
1315
+     * @throws OCSException
1316
+     */
1317
+    public function addSubAdmin(string $userId, string $groupid): DataResponse {
1318
+        $group = $this->groupManager->get($groupid);
1319
+        $user = $this->userManager->get($userId);
1320
+
1321
+        // Check if the user exists
1322
+        if ($user === null) {
1323
+            throw new OCSException('User does not exist', 101);
1324
+        }
1325
+        // Check if group exists
1326
+        if ($group === null) {
1327
+            throw new OCSException('Group does not exist', 102);
1328
+        }
1329
+        // Check if trying to make subadmin of admin group
1330
+        if ($group->getGID() === 'admin') {
1331
+            throw new OCSException('Cannot create subadmins for admin group', 103);
1332
+        }
1333
+
1334
+        $subAdminManager = $this->groupManager->getSubAdmin();
1335
+
1336
+        // We cannot be subadmin twice
1337
+        if ($subAdminManager->isSubAdminOfGroup($user, $group)) {
1338
+            return new DataResponse();
1339
+        }
1340
+        // Go
1341
+        $subAdminManager->createSubAdmin($user, $group);
1342
+        return new DataResponse();
1343
+    }
1344
+
1345
+    /**
1346
+     * Removes a subadmin from a group
1347
+     *
1348
+     * @PasswordConfirmationRequired
1349
+     *
1350
+     * @param string $userId
1351
+     * @param string $groupid
1352
+     * @return DataResponse
1353
+     * @throws OCSException
1354
+     */
1355
+    public function removeSubAdmin(string $userId, string $groupid): DataResponse {
1356
+        $group = $this->groupManager->get($groupid);
1357
+        $user = $this->userManager->get($userId);
1358
+        $subAdminManager = $this->groupManager->getSubAdmin();
1359
+
1360
+        // Check if the user exists
1361
+        if ($user === null) {
1362
+            throw new OCSException('User does not exist', 101);
1363
+        }
1364
+        // Check if the group exists
1365
+        if ($group === null) {
1366
+            throw new OCSException('Group does not exist', 101);
1367
+        }
1368
+        // Check if they are a subadmin of this said group
1369
+        if (!$subAdminManager->isSubAdminOfGroup($user, $group)) {
1370
+            throw new OCSException('User is not a subadmin of this group', 102);
1371
+        }
1372
+
1373
+        // Go
1374
+        $subAdminManager->deleteSubAdmin($user, $group);
1375
+        return new DataResponse();
1376
+    }
1377
+
1378
+    /**
1379
+     * Get the groups a user is a subadmin of
1380
+     *
1381
+     * @param string $userId
1382
+     * @return DataResponse
1383
+     * @throws OCSException
1384
+     */
1385
+    public function getUserSubAdminGroups(string $userId): DataResponse {
1386
+        $groups = $this->getUserSubAdminGroupsData($userId);
1387
+        return new DataResponse($groups);
1388
+    }
1389
+
1390
+    /**
1391
+     * @NoAdminRequired
1392
+     * @PasswordConfirmationRequired
1393
+     *
1394
+     * resend welcome message
1395
+     *
1396
+     * @param string $userId
1397
+     * @return DataResponse
1398
+     * @throws OCSException
1399
+     */
1400
+    public function resendWelcomeMessage(string $userId): DataResponse {
1401
+        $currentLoggedInUser = $this->userSession->getUser();
1402
+
1403
+        $targetUser = $this->userManager->get($userId);
1404
+        if ($targetUser === null) {
1405
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1406
+        }
1407
+
1408
+        // Check if admin / subadmin
1409
+        $subAdminManager = $this->groupManager->getSubAdmin();
1410
+        if (
1411
+            !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
1412
+            && !$this->groupManager->isAdmin($currentLoggedInUser->getUID())
1413
+        ) {
1414
+            // No rights
1415
+            throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
1416
+        }
1417
+
1418
+        $email = $targetUser->getEMailAddress();
1419
+        if ($email === '' || $email === null) {
1420
+            throw new OCSException('Email address not available', 101);
1421
+        }
1422
+
1423
+        try {
1424
+            $emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
1425
+            $this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
1426
+        } catch (\Exception $e) {
1427
+            $this->logger->error(
1428
+                "Can't send new user mail to $email",
1429
+                [
1430
+                    'app' => 'settings',
1431
+                    'exception' => $e,
1432
+                ]
1433
+            );
1434
+            throw new OCSException('Sending email failed', 102);
1435
+        }
1436
+
1437
+        return new DataResponse();
1438
+    }
1439 1439
 }
Please login to merge, or discard this patch.