Passed
Push — master ( fda6ff...86a3b7 )
by Joas
16:22 queued 12s
created

UsersController::addSubAdmin()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 5
nop 2
dl 0
loc 26
rs 9.5222
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 *
8
 * @author Arthur Schiwon <[email protected]>
9
 * @author Bjoern Schiessle <[email protected]>
10
 * @author Christoph Wurst <[email protected]>
11
 * @author Daniel Calviño Sánchez <[email protected]>
12
 * @author Daniel Kesselberg <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author John Molakvoæ (skjnldsv) <[email protected]>
15
 * @author Julius Härtl <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author michag86 <[email protected]>
18
 * @author Mikael Hammarin <[email protected]>
19
 * @author Morris Jobke <[email protected]>
20
 * @author Robin Appelman <[email protected]>
21
 * @author Roeland Jago Douma <[email protected]>
22
 * @author Thomas Citharel <[email protected]>
23
 * @author Thomas Müller <[email protected]>
24
 * @author Tom Needham <[email protected]>
25
 *
26
 * @license AGPL-3.0
27
 *
28
 * This code is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License, version 3,
30
 * as published by the Free Software Foundation.
31
 *
32
 * This program is distributed in the hope that it will be useful,
33
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35
 * GNU Affero General Public License for more details.
36
 *
37
 * You should have received a copy of the GNU Affero General Public License, version 3,
38
 * along with this program. If not, see <http://www.gnu.org/licenses/>
39
 *
40
 */
41
42
namespace OCA\Provisioning_API\Controller;
43
44
use libphonenumber\NumberParseException;
45
use libphonenumber\PhoneNumber;
46
use libphonenumber\PhoneNumberFormat;
47
use libphonenumber\PhoneNumberUtil;
48
use OC\Accounts\AccountManager;
49
use OC\Authentication\Token\RemoteWipe;
50
use OC\HintException;
51
use OCA\Provisioning_API\FederatedShareProviderFactory;
52
use OCA\Settings\Mailer\NewUserMailHelper;
53
use OCP\Accounts\IAccountManager;
54
use OCP\App\IAppManager;
55
use OCP\AppFramework\Http;
56
use OCP\AppFramework\Http\DataResponse;
57
use OCP\AppFramework\OCS\OCSException;
58
use OCP\AppFramework\OCS\OCSForbiddenException;
59
use OCP\IConfig;
60
use OCP\IGroup;
61
use OCP\IGroupManager;
62
use OCP\ILogger;
63
use OCP\IRequest;
64
use OCP\IURLGenerator;
65
use OCP\IUser;
66
use OCP\IUserManager;
67
use OCP\IUserSession;
68
use OCP\L10N\IFactory;
69
use OCP\Security\ISecureRandom;
70
use OCP\Security\Events\GenerateSecurePasswordEvent;
71
use OCP\EventDispatcher\IEventDispatcher;
72
73
class UsersController extends AUserData {
74
75
	/** @var IAppManager */
76
	private $appManager;
77
	/** @var IURLGenerator */
78
	protected $urlGenerator;
79
	/** @var ILogger */
80
	private $logger;
81
	/** @var IFactory */
82
	protected $l10nFactory;
83
	/** @var NewUserMailHelper */
84
	private $newUserMailHelper;
85
	/** @var FederatedShareProviderFactory */
86
	private $federatedShareProviderFactory;
87
	/** @var ISecureRandom */
88
	private $secureRandom;
89
	/** @var RemoteWipe */
90
	private $remoteWipe;
91
	/** @var IEventDispatcher */
92
	private $eventDispatcher;
93
94
	public function __construct(string $appName,
95
								IRequest $request,
96
								IUserManager $userManager,
97
								IConfig $config,
98
								IAppManager $appManager,
99
								IGroupManager $groupManager,
100
								IUserSession $userSession,
101
								AccountManager $accountManager,
102
								IURLGenerator $urlGenerator,
103
								ILogger $logger,
104
								IFactory $l10nFactory,
105
								NewUserMailHelper $newUserMailHelper,
106
								FederatedShareProviderFactory $federatedShareProviderFactory,
107
								ISecureRandom $secureRandom,
108
								RemoteWipe $remoteWipe,
109
								IEventDispatcher $eventDispatcher) {
110
		parent::__construct($appName,
111
							$request,
112
							$userManager,
113
							$config,
114
							$groupManager,
115
							$userSession,
116
							$accountManager,
117
							$l10nFactory);
118
119
		$this->appManager = $appManager;
120
		$this->urlGenerator = $urlGenerator;
121
		$this->logger = $logger;
122
		$this->l10nFactory = $l10nFactory;
123
		$this->newUserMailHelper = $newUserMailHelper;
124
		$this->federatedShareProviderFactory = $federatedShareProviderFactory;
125
		$this->secureRandom = $secureRandom;
126
		$this->remoteWipe = $remoteWipe;
127
		$this->eventDispatcher = $eventDispatcher;
128
	}
129
130
	/**
131
	 * @NoAdminRequired
132
	 *
133
	 * returns a list of users
134
	 *
135
	 * @param string $search
136
	 * @param int $limit
137
	 * @param int $offset
138
	 * @return DataResponse
139
	 */
140
	public function getUsers(string $search = '', int $limit = null, int $offset = 0): DataResponse {
141
		$user = $this->userSession->getUser();
142
		$users = [];
143
144
		// Admin? Or SubAdmin?
145
		$uid = $user->getUID();
146
		$subAdminManager = $this->groupManager->getSubAdmin();
0 ignored issues
show
Bug introduced by
The method getSubAdmin() does not exist on OCP\IGroupManager. Since it exists in all sub-types, consider adding an abstract or default implementation to OCP\IGroupManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

146
		/** @scrutinizer ignore-call */ 
147
  $subAdminManager = $this->groupManager->getSubAdmin();
Loading history...
147
		if ($this->groupManager->isAdmin($uid)) {
148
			$users = $this->userManager->search($search, $limit, $offset);
149
		} elseif ($subAdminManager->isSubAdmin($user)) {
150
			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
151
			foreach ($subAdminOfGroups as $key => $group) {
152
				$subAdminOfGroups[$key] = $group->getGID();
153
			}
154
155
			$users = [];
156
			foreach ($subAdminOfGroups as $group) {
157
				$users = array_merge($users, $this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
158
			}
159
		}
160
161
		$users = array_keys($users);
162
163
		return new DataResponse([
164
			'users' => $users
165
		]);
166
	}
167
168
	/**
169
	 * @NoAdminRequired
170
	 *
171
	 * returns a list of users and their data
172
	 */
173
	public function getUsersDetails(string $search = '', int $limit = null, int $offset = 0): DataResponse {
174
		$currentUser = $this->userSession->getUser();
175
		$users = [];
176
177
		// Admin? Or SubAdmin?
178
		$uid = $currentUser->getUID();
179
		$subAdminManager = $this->groupManager->getSubAdmin();
180
		if ($this->groupManager->isAdmin($uid)) {
181
			$users = $this->userManager->search($search, $limit, $offset);
182
			$users = array_keys($users);
183
		} elseif ($subAdminManager->isSubAdmin($currentUser)) {
184
			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
185
			foreach ($subAdminOfGroups as $key => $group) {
186
				$subAdminOfGroups[$key] = $group->getGID();
187
			}
188
189
			$users = [];
190
			foreach ($subAdminOfGroups as $group) {
191
				$users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
192
			}
193
			$users = array_merge(...$users);
194
		}
195
196
		$usersDetails = [];
197
		foreach ($users as $userId) {
198
			$userId = (string) $userId;
199
			$userData = $this->getUserData($userId);
200
			// Do not insert empty entry
201
			if (!empty($userData)) {
202
				$usersDetails[$userId] = $userData;
203
			} else {
204
				// Logged user does not have permissions to see this user
205
				// only showing its id
206
				$usersDetails[$userId] = ['id' => $userId];
207
			}
208
		}
209
210
		return new DataResponse([
211
			'users' => $usersDetails
212
		]);
213
	}
214
215
216
	/**
217
	 * @NoAdminRequired
218
	 * @NoSubAdminRequired
219
	 *
220
	 * @param string $location
221
	 * @param array $search
222
	 * @return DataResponse
223
	 */
224
	public function searchByPhoneNumbers(string $location, array $search): DataResponse {
225
		$phoneUtil = PhoneNumberUtil::getInstance();
226
227
		if ($phoneUtil->getCountryCodeForRegion($location) === 0) {
228
			// Not a valid region code
229
			return new DataResponse([], Http::STATUS_BAD_REQUEST);
230
		}
231
232
		$normalizedNumberToKey = [];
233
		foreach ($search as $key => $phoneNumbers) {
234
			foreach ($phoneNumbers as $phone) {
235
				try {
236
					$phoneNumber = $phoneUtil->parse($phone, $location);
237
					if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) {
238
						$normalizedNumber = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
239
						$normalizedNumberToKey[$normalizedNumber] = (string) $key;
240
					}
241
				} catch (NumberParseException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
242
				}
243
			}
244
		}
245
246
		$phoneNumbers = array_keys($normalizedNumberToKey);
247
248
		if (empty($phoneNumbers)) {
249
			return new DataResponse();
250
		}
251
252
		$userMatches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, $phoneNumbers);
253
254
		if (empty($userMatches)) {
255
			return new DataResponse();
256
		}
257
258
		$cloudUrl = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
259
		if (strpos($cloudUrl, 'http://') === 0) {
260
			$cloudUrl = substr($cloudUrl, strlen('http://'));
261
		} elseif (strpos($cloudUrl, 'https://') === 0) {
262
			$cloudUrl = substr($cloudUrl, strlen('https://'));
263
		}
264
265
		$matches = [];
266
		foreach ($userMatches as $phone => $userId) {
267
			// Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book
268
			$matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl;
269
		}
270
271
		return new DataResponse($matches);
272
	}
273
274
	/**
275
	 * @throws OCSException
276
	 */
277
	private function createNewUserId(): string {
278
		$attempts = 0;
279
		do {
280
			$uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
281
			if (!$this->userManager->userExists($uidCandidate)) {
282
				return $uidCandidate;
283
			}
284
			$attempts++;
285
		} while ($attempts < 10);
286
		throw new OCSException('Could not create non-existing user id', 111);
287
	}
288
289
	/**
290
	 * @PasswordConfirmationRequired
291
	 * @NoAdminRequired
292
	 *
293
	 * @param string $userid
294
	 * @param string $password
295
	 * @param string $displayName
296
	 * @param string $email
297
	 * @param array $groups
298
	 * @param array $subadmin
299
	 * @param string $quota
300
	 * @param string $language
301
	 * @return DataResponse
302
	 * @throws OCSException
303
	 */
304
	public function addUser(string $userid,
305
							string $password = '',
306
							string $displayName = '',
307
							string $email = '',
308
							array $groups = [],
309
							array $subadmin = [],
310
							string $quota = '',
311
							string $language = ''): DataResponse {
312
		$user = $this->userSession->getUser();
313
		$isAdmin = $this->groupManager->isAdmin($user->getUID());
314
		$subAdminManager = $this->groupManager->getSubAdmin();
315
316
		if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
317
			$userid = $this->createNewUserId();
318
		}
319
320
		if ($this->userManager->userExists($userid)) {
321
			$this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
322
			throw new OCSException('User already exists', 102);
323
		}
324
325
		if ($groups !== []) {
326
			foreach ($groups as $group) {
327
				if (!$this->groupManager->groupExists($group)) {
328
					throw new OCSException('group '.$group.' does not exist', 104);
329
				}
330
				if (!$isAdmin && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
331
					throw new OCSException('insufficient privileges for group '. $group, 105);
332
				}
333
			}
334
		} else {
335
			if (!$isAdmin) {
336
				throw new OCSException('no group specified (required for subadmins)', 106);
337
			}
338
		}
339
340
		$subadminGroups = [];
341
		if ($subadmin !== []) {
342
			foreach ($subadmin as $groupid) {
343
				$group = $this->groupManager->get($groupid);
344
				// Check if group exists
345
				if ($group === null) {
346
					throw new OCSException('Subadmin group does not exist',  102);
347
				}
348
				// Check if trying to make subadmin of admin group
349
				if ($group->getGID() === 'admin') {
350
					throw new OCSException('Cannot create subadmins for admin group', 103);
351
				}
352
				// Check if has permission to promote subadmins
353
				if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin) {
354
					throw new OCSForbiddenException('No permissions to promote subadmins');
355
				}
356
				$subadminGroups[] = $group;
357
			}
358
		}
359
360
		$generatePasswordResetToken = false;
361
		if ($password === '') {
362
			if ($email === '') {
363
				throw new OCSException('To send a password link to the user an email address is required.', 108);
364
			}
365
366
			$passwordEvent = new GenerateSecurePasswordEvent();
367
			$this->eventDispatcher->dispatchTyped($passwordEvent);
368
369
			$password = $passwordEvent->getPassword();
370
			if ($password === null) {
371
				// Fallback: ensure to pass password_policy in any case
372
				$password = $this->secureRandom->generate(10)
373
					. $this->secureRandom->generate(1, ISecureRandom::CHAR_UPPER)
374
					. $this->secureRandom->generate(1, ISecureRandom::CHAR_LOWER)
375
					. $this->secureRandom->generate(1, ISecureRandom::CHAR_DIGITS)
376
					. $this->secureRandom->generate(1, ISecureRandom::CHAR_SYMBOLS);
377
			}
378
			$generatePasswordResetToken = true;
379
		}
380
381
		if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
382
			throw new OCSException('Required email address was not provided', 110);
383
		}
384
385
		try {
386
			$newUser = $this->userManager->createUser($userid, $password);
387
			$this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
388
389
			foreach ($groups as $group) {
390
				$this->groupManager->get($group)->addUser($newUser);
391
				$this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
392
			}
393
			foreach ($subadminGroups as $group) {
394
				$subAdminManager->createSubAdmin($newUser, $group);
395
			}
396
397
			if ($displayName !== '') {
398
				$this->editUser($userid, 'display', $displayName);
399
			}
400
401
			if ($quota !== '') {
402
				$this->editUser($userid, 'quota', $quota);
403
			}
404
405
			if ($language !== '') {
406
				$this->editUser($userid, 'language', $language);
407
			}
408
409
			// Send new user mail only if a mail is set
410
			if ($email !== '') {
411
				$newUser->setEMailAddress($email);
412
				if ($this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes') {
413
					try {
414
						$emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken);
415
						$this->newUserMailHelper->sendMail($newUser, $emailTemplate);
416
					} catch (\Exception $e) {
417
						// Mail could be failing hard or just be plain not configured
418
						// Logging error as it is the hardest of the two
419
						$this->logger->logException($e, [
420
							'message' => "Unable to send the invitation mail to $email",
421
							'level' => ILogger::ERROR,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

421
							'level' => /** @scrutinizer ignore-deprecated */ ILogger::ERROR,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
422
							'app' => 'ocs_api',
423
						]);
424
					}
425
				}
426
			}
427
428
			return new DataResponse(['id' => $userid]);
429
		} catch (HintException $e) {
430
			$this->logger->logException($e, [
431
				'message' => 'Failed addUser attempt with hint exception.',
432
				'level' => ILogger::WARN,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::WARN has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

432
				'level' => /** @scrutinizer ignore-deprecated */ ILogger::WARN,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
433
				'app' => 'ocs_api',
434
			]);
435
			throw new OCSException($e->getHint(), 107);
436
		} catch (OCSException $e) {
437
			$this->logger->logException($e, [
438
				'message' => 'Failed addUser attempt with ocs exeption.',
439
				'level' => ILogger::ERROR,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

439
				'level' => /** @scrutinizer ignore-deprecated */ ILogger::ERROR,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
440
				'app' => 'ocs_api',
441
			]);
442
			throw $e;
443
		} catch (\Exception $e) {
444
			$this->logger->logException($e, [
445
				'message' => 'Failed addUser attempt with exception.',
446
				'level' => ILogger::ERROR,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

446
				'level' => /** @scrutinizer ignore-deprecated */ ILogger::ERROR,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
447
				'app' => 'ocs_api',
448
			]);
449
			throw new OCSException('Bad request', 101);
450
		}
451
	}
452
453
	/**
454
	 * @NoAdminRequired
455
	 * @NoSubAdminRequired
456
	 *
457
	 * gets user info
458
	 *
459
	 * @param string $userId
460
	 * @return DataResponse
461
	 * @throws OCSException
462
	 */
463
	public function getUser(string $userId): DataResponse {
464
		$data = $this->getUserData($userId);
465
		// getUserData returns empty array if not enough permissions
466
		if (empty($data)) {
467
			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
468
		}
469
		return new DataResponse($data);
470
	}
471
472
	/**
473
	 * @NoAdminRequired
474
	 * @NoSubAdminRequired
475
	 *
476
	 * gets user info from the currently logged in user
477
	 *
478
	 * @return DataResponse
479
	 * @throws OCSException
480
	 */
481
	public function getCurrentUser(): DataResponse {
482
		$user = $this->userSession->getUser();
483
		if ($user) {
484
			$data = $this->getUserData($user->getUID());
485
			// rename "displayname" to "display-name" only for this call to keep
486
			// the API stable.
487
			$data['display-name'] = $data['displayname'];
488
			unset($data['displayname']);
489
			return new DataResponse($data);
490
		}
491
492
		throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
493
	}
494
495
	/**
496
	 * @NoAdminRequired
497
	 * @NoSubAdminRequired
498
	 */
499
	public function getEditableFields(): DataResponse {
500
		$permittedFields = [];
501
502
		// Editing self (display, email)
503
		if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
504
			$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
505
			$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
506
		}
507
508
		if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
509
			$shareProvider = $this->federatedShareProviderFactory->get();
510
			if ($shareProvider->isLookupServerUploadEnabled()) {
511
				$permittedFields[] = IAccountManager::PROPERTY_PHONE;
512
				$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
513
				$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
514
				$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
515
			}
516
		}
517
518
		return new DataResponse($permittedFields);
519
	}
520
521
	/**
522
	 * @NoAdminRequired
523
	 * @NoSubAdminRequired
524
	 * @PasswordConfirmationRequired
525
	 *
526
	 * edit users
527
	 *
528
	 * @param string $userId
529
	 * @param string $key
530
	 * @param string $value
531
	 * @return DataResponse
532
	 * @throws OCSException
533
	 */
534
	public function editUser(string $userId, string $key, string $value): DataResponse {
535
		$currentLoggedInUser = $this->userSession->getUser();
536
537
		$targetUser = $this->userManager->get($userId);
538
		if ($targetUser === null) {
539
			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
540
		}
541
542
		$permittedFields = [];
543
		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
544
			// Editing self (display, email)
545
			if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
546
				$permittedFields[] = 'display';
547
				$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
548
				$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
549
			}
550
551
			$permittedFields[] = 'password';
552
			if ($this->config->getSystemValue('force_language', false) === false ||
553
				$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
554
				$permittedFields[] = 'language';
555
			}
556
557
			if ($this->config->getSystemValue('force_locale', false) === false ||
558
				$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
559
				$permittedFields[] = 'locale';
560
			}
561
562
			if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
563
				$shareProvider = $this->federatedShareProviderFactory->get();
564
				if ($shareProvider->isLookupServerUploadEnabled()) {
565
					$permittedFields[] = IAccountManager::PROPERTY_PHONE;
566
					$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
567
					$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
568
					$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
569
				}
570
			}
571
572
			// If admin they can edit their own quota
573
			if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
574
				$permittedFields[] = 'quota';
575
			}
576
		} else {
577
			// Check if admin / subadmin
578
			$subAdminManager = $this->groupManager->getSubAdmin();
579
			if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())
580
			|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
581
				// They have permissions over the user
582
				$permittedFields[] = 'display';
583
				$permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME;
584
				$permittedFields[] = IAccountManager::PROPERTY_EMAIL;
585
				$permittedFields[] = 'password';
586
				$permittedFields[] = 'language';
587
				$permittedFields[] = 'locale';
588
				$permittedFields[] = IAccountManager::PROPERTY_PHONE;
589
				$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
590
				$permittedFields[] = IAccountManager::PROPERTY_WEBSITE;
591
				$permittedFields[] = IAccountManager::PROPERTY_TWITTER;
592
				$permittedFields[] = 'quota';
593
			} else {
594
				// No rights
595
				throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
596
			}
597
		}
598
		// Check if permitted to edit this field
599
		if (!in_array($key, $permittedFields)) {
600
			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
601
		}
602
		// Process the edit
603
		switch ($key) {
604
			case 'display':
605
			case IAccountManager::PROPERTY_DISPLAYNAME:
606
				$targetUser->setDisplayName($value);
607
				break;
608
			case 'quota':
609
				$quota = $value;
610
				if ($quota !== 'none' && $quota !== 'default') {
611
					if (is_numeric($quota)) {
612
						$quota = (float) $quota;
613
					} else {
614
						$quota = \OCP\Util::computerFileSize($quota);
615
					}
616
					if ($quota === false) {
0 ignored issues
show
introduced by
The condition $quota === false is always false.
Loading history...
617
						throw new OCSException('Invalid quota value '.$value, 103);
618
					}
619
					if ($quota === -1) {
0 ignored issues
show
introduced by
The condition $quota === -1 is always false.
Loading history...
620
						$quota = 'none';
621
					} else {
622
						$quota = \OCP\Util::humanFileSize($quota);
0 ignored issues
show
Bug introduced by
$quota of type double is incompatible with the type integer expected by parameter $bytes of OCP\Util::humanFileSize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

622
						$quota = \OCP\Util::humanFileSize(/** @scrutinizer ignore-type */ $quota);
Loading history...
623
					}
624
				}
625
				$targetUser->setQuota($quota);
626
				break;
627
			case 'password':
628
				try {
629
					if (!$targetUser->canChangePassword()) {
630
						throw new OCSException('Setting the password is not supported by the users backend', 103);
631
					}
632
					$targetUser->setPassword($value);
633
				} catch (HintException $e) { // password policy error
634
					throw new OCSException($e->getMessage(), 103);
635
				}
636
				break;
637
			case 'language':
638
				$languagesCodes = $this->l10nFactory->findAvailableLanguages();
639
				if (!in_array($value, $languagesCodes, true) && $value !== 'en') {
640
					throw new OCSException('Invalid language', 102);
641
				}
642
				$this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value);
643
				break;
644
			case 'locale':
645
				if (!$this->l10nFactory->localeExists($value)) {
646
					throw new OCSException('Invalid locale', 102);
647
				}
648
				$this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
649
				break;
650
			case IAccountManager::PROPERTY_EMAIL:
651
				if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
652
					$targetUser->setEMailAddress($value);
653
				} else {
654
					throw new OCSException('', 102);
655
				}
656
				break;
657
			case IAccountManager::PROPERTY_PHONE:
658
			case IAccountManager::PROPERTY_ADDRESS:
659
			case IAccountManager::PROPERTY_WEBSITE:
660
			case IAccountManager::PROPERTY_TWITTER:
661
				$userAccount = $this->accountManager->getUser($targetUser);
662
				if ($userAccount[$key]['value'] !== $value) {
663
					$userAccount[$key]['value'] = $value;
664
					try {
665
						$this->accountManager->updateUser($targetUser, $userAccount, true);
666
					} catch (\InvalidArgumentException $e) {
667
						throw new OCSException('Invalid ' . $e->getMessage(), 102);
668
					}
669
				}
670
				break;
671
			default:
672
				throw new OCSException('', 103);
673
		}
674
		return new DataResponse();
675
	}
676
677
	/**
678
	 * @PasswordConfirmationRequired
679
	 * @NoAdminRequired
680
	 *
681
	 * @param string $userId
682
	 *
683
	 * @return DataResponse
684
	 *
685
	 * @throws OCSException
686
	 */
687
	public function wipeUserDevices(string $userId): DataResponse {
688
		/** @var IUser $currentLoggedInUser */
689
		$currentLoggedInUser = $this->userSession->getUser();
690
691
		$targetUser = $this->userManager->get($userId);
692
693
		if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
694
			throw new OCSException('', 101);
695
		}
696
697
		// If not permitted
698
		$subAdminManager = $this->groupManager->getSubAdmin();
699
		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
700
			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
701
		}
702
703
		$this->remoteWipe->markAllTokensForWipe($targetUser);
704
705
		return new DataResponse();
706
	}
707
708
	/**
709
	 * @PasswordConfirmationRequired
710
	 * @NoAdminRequired
711
	 *
712
	 * @param string $userId
713
	 * @return DataResponse
714
	 * @throws OCSException
715
	 */
716
	public function deleteUser(string $userId): DataResponse {
717
		$currentLoggedInUser = $this->userSession->getUser();
718
719
		$targetUser = $this->userManager->get($userId);
720
721
		if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
722
			throw new OCSException('', 101);
723
		}
724
725
		// If not permitted
726
		$subAdminManager = $this->groupManager->getSubAdmin();
727
		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
728
			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
729
		}
730
731
		// Go ahead with the delete
732
		if ($targetUser->delete()) {
733
			return new DataResponse();
734
		} else {
735
			throw new OCSException('', 101);
736
		}
737
	}
738
739
	/**
740
	 * @PasswordConfirmationRequired
741
	 * @NoAdminRequired
742
	 *
743
	 * @param string $userId
744
	 * @return DataResponse
745
	 * @throws OCSException
746
	 * @throws OCSForbiddenException
747
	 */
748
	public function disableUser(string $userId): DataResponse {
749
		return $this->setEnabled($userId, false);
750
	}
751
752
	/**
753
	 * @PasswordConfirmationRequired
754
	 * @NoAdminRequired
755
	 *
756
	 * @param string $userId
757
	 * @return DataResponse
758
	 * @throws OCSException
759
	 * @throws OCSForbiddenException
760
	 */
761
	public function enableUser(string $userId): DataResponse {
762
		return $this->setEnabled($userId, true);
763
	}
764
765
	/**
766
	 * @param string $userId
767
	 * @param bool $value
768
	 * @return DataResponse
769
	 * @throws OCSException
770
	 */
771
	private function setEnabled(string $userId, bool $value): DataResponse {
772
		$currentLoggedInUser = $this->userSession->getUser();
773
774
		$targetUser = $this->userManager->get($userId);
775
		if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
776
			throw new OCSException('', 101);
777
		}
778
779
		// If not permitted
780
		$subAdminManager = $this->groupManager->getSubAdmin();
781
		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
782
			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
783
		}
784
785
		// enable/disable the user now
786
		$targetUser->setEnabled($value);
787
		return new DataResponse();
788
	}
789
790
	/**
791
	 * @NoAdminRequired
792
	 * @NoSubAdminRequired
793
	 *
794
	 * @param string $userId
795
	 * @return DataResponse
796
	 * @throws OCSException
797
	 */
798
	public function getUsersGroups(string $userId): DataResponse {
799
		$loggedInUser = $this->userSession->getUser();
800
801
		$targetUser = $this->userManager->get($userId);
802
		if ($targetUser === null) {
803
			throw new OCSException('', \OCP\API::RESPOND_NOT_FOUND);
804
		}
805
806
		if ($targetUser->getUID() === $loggedInUser->getUID() || $this->groupManager->isAdmin($loggedInUser->getUID())) {
807
			// Self lookup or admin lookup
808
			return new DataResponse([
809
				'groups' => $this->groupManager->getUserGroupIds($targetUser)
810
			]);
811
		} else {
812
			$subAdminManager = $this->groupManager->getSubAdmin();
813
814
			// Looking up someone else
815
			if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
816
				// Return the group that the method caller is subadmin of for the user in question
817
				/** @var IGroup[] $getSubAdminsGroups */
818
				$getSubAdminsGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
819
				foreach ($getSubAdminsGroups as $key => $group) {
820
					$getSubAdminsGroups[$key] = $group->getGID();
821
				}
822
				$groups = array_intersect(
823
					$getSubAdminsGroups,
824
					$this->groupManager->getUserGroupIds($targetUser)
825
				);
826
				return new DataResponse(['groups' => $groups]);
827
			} else {
828
				// Not permitted
829
				throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
830
			}
831
		}
832
	}
833
834
	/**
835
	 * @PasswordConfirmationRequired
836
	 * @NoAdminRequired
837
	 *
838
	 * @param string $userId
839
	 * @param string $groupid
840
	 * @return DataResponse
841
	 * @throws OCSException
842
	 */
843
	public function addToGroup(string $userId, string $groupid = ''): DataResponse {
844
		if ($groupid === '') {
845
			throw new OCSException('', 101);
846
		}
847
848
		$group = $this->groupManager->get($groupid);
849
		$targetUser = $this->userManager->get($userId);
850
		if ($group === null) {
851
			throw new OCSException('', 102);
852
		}
853
		if ($targetUser === null) {
854
			throw new OCSException('', 103);
855
		}
856
857
		// If they're not an admin, check they are a subadmin of the group in question
858
		$loggedInUser = $this->userSession->getUser();
859
		$subAdminManager = $this->groupManager->getSubAdmin();
860
		if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
861
			throw new OCSException('', 104);
862
		}
863
864
		// Add user to group
865
		$group->addUser($targetUser);
866
		return new DataResponse();
867
	}
868
869
	/**
870
	 * @PasswordConfirmationRequired
871
	 * @NoAdminRequired
872
	 *
873
	 * @param string $userId
874
	 * @param string $groupid
875
	 * @return DataResponse
876
	 * @throws OCSException
877
	 */
878
	public function removeFromGroup(string $userId, string $groupid): DataResponse {
879
		$loggedInUser = $this->userSession->getUser();
880
881
		if ($groupid === null || trim($groupid) === '') {
882
			throw new OCSException('', 101);
883
		}
884
885
		$group = $this->groupManager->get($groupid);
886
		if ($group === null) {
887
			throw new OCSException('', 102);
888
		}
889
890
		$targetUser = $this->userManager->get($userId);
891
		if ($targetUser === null) {
892
			throw new OCSException('', 103);
893
		}
894
895
		// If they're not an admin, check they are a subadmin of the group in question
896
		$subAdminManager = $this->groupManager->getSubAdmin();
897
		if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
898
			throw new OCSException('', 104);
899
		}
900
901
		// Check they aren't removing themselves from 'admin' or their 'subadmin; group
902
		if ($targetUser->getUID() === $loggedInUser->getUID()) {
903
			if ($this->groupManager->isAdmin($loggedInUser->getUID())) {
904
				if ($group->getGID() === 'admin') {
905
					throw new OCSException('Cannot remove yourself from the admin group', 105);
906
				}
907
			} else {
908
				// Not an admin, so the user must be a subadmin of this group, but that is not allowed.
909
				throw new OCSException('Cannot remove yourself from this group as you are a SubAdmin', 105);
910
			}
911
		} elseif (!$this->groupManager->isAdmin($loggedInUser->getUID())) {
912
			/** @var IGroup[] $subAdminGroups */
913
			$subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
914
			$subAdminGroups = array_map(function (IGroup $subAdminGroup) {
915
				return $subAdminGroup->getGID();
916
			}, $subAdminGroups);
917
			$userGroups = $this->groupManager->getUserGroupIds($targetUser);
918
			$userSubAdminGroups = array_intersect($subAdminGroups, $userGroups);
919
920
			if (count($userSubAdminGroups) <= 1) {
921
				// Subadmin must not be able to remove a user from all their subadmin groups.
922
				throw new OCSException('Not viable to remove user from the last group you are SubAdmin of', 105);
923
			}
924
		}
925
926
		// Remove user from group
927
		$group->removeUser($targetUser);
928
		return new DataResponse();
929
	}
930
931
	/**
932
	 * Creates a subadmin
933
	 *
934
	 * @PasswordConfirmationRequired
935
	 *
936
	 * @param string $userId
937
	 * @param string $groupid
938
	 * @return DataResponse
939
	 * @throws OCSException
940
	 */
941
	public function addSubAdmin(string $userId, string $groupid): DataResponse {
942
		$group = $this->groupManager->get($groupid);
943
		$user = $this->userManager->get($userId);
944
945
		// Check if the user exists
946
		if ($user === null) {
947
			throw new OCSException('User does not exist', 101);
948
		}
949
		// Check if group exists
950
		if ($group === null) {
951
			throw new OCSException('Group does not exist',  102);
952
		}
953
		// Check if trying to make subadmin of admin group
954
		if ($group->getGID() === 'admin') {
955
			throw new OCSException('Cannot create subadmins for admin group', 103);
956
		}
957
958
		$subAdminManager = $this->groupManager->getSubAdmin();
959
960
		// We cannot be subadmin twice
961
		if ($subAdminManager->isSubAdminOfGroup($user, $group)) {
962
			return new DataResponse();
963
		}
964
		// Go
965
		$subAdminManager->createSubAdmin($user, $group);
966
		return new DataResponse();
967
	}
968
969
	/**
970
	 * Removes a subadmin from a group
971
	 *
972
	 * @PasswordConfirmationRequired
973
	 *
974
	 * @param string $userId
975
	 * @param string $groupid
976
	 * @return DataResponse
977
	 * @throws OCSException
978
	 */
979
	public function removeSubAdmin(string $userId, string $groupid): DataResponse {
980
		$group = $this->groupManager->get($groupid);
981
		$user = $this->userManager->get($userId);
982
		$subAdminManager = $this->groupManager->getSubAdmin();
983
984
		// Check if the user exists
985
		if ($user === null) {
986
			throw new OCSException('User does not exist', 101);
987
		}
988
		// Check if the group exists
989
		if ($group === null) {
990
			throw new OCSException('Group does not exist', 101);
991
		}
992
		// Check if they are a subadmin of this said group
993
		if (!$subAdminManager->isSubAdminOfGroup($user, $group)) {
994
			throw new OCSException('User is not a subadmin of this group', 102);
995
		}
996
997
		// Go
998
		$subAdminManager->deleteSubAdmin($user, $group);
999
		return new DataResponse();
1000
	}
1001
1002
	/**
1003
	 * Get the groups a user is a subadmin of
1004
	 *
1005
	 * @param string $userId
1006
	 * @return DataResponse
1007
	 * @throws OCSException
1008
	 */
1009
	public function getUserSubAdminGroups(string $userId): DataResponse {
1010
		$groups = $this->getUserSubAdminGroupsData($userId);
1011
		return new DataResponse($groups);
1012
	}
1013
1014
	/**
1015
	 * @NoAdminRequired
1016
	 * @PasswordConfirmationRequired
1017
	 *
1018
	 * resend welcome message
1019
	 *
1020
	 * @param string $userId
1021
	 * @return DataResponse
1022
	 * @throws OCSException
1023
	 */
1024
	public function resendWelcomeMessage(string $userId): DataResponse {
1025
		$currentLoggedInUser = $this->userSession->getUser();
1026
1027
		$targetUser = $this->userManager->get($userId);
1028
		if ($targetUser === null) {
1029
			throw new OCSException('', \OCP\API::RESPOND_NOT_FOUND);
1030
		}
1031
1032
		// Check if admin / subadmin
1033
		$subAdminManager = $this->groupManager->getSubAdmin();
1034
		if (!$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
1035
			&& !$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
1036
			// No rights
1037
			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
1038
		}
1039
1040
		$email = $targetUser->getEMailAddress();
1041
		if ($email === '' || $email === null) {
1042
			throw new OCSException('Email address not available', 101);
1043
		}
1044
1045
		try {
1046
			$emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
1047
			$this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
1048
		} catch (\Exception $e) {
1049
			$this->logger->logException($e, [
1050
				'message' => "Can't send new user mail to $email",
1051
				'level' => ILogger::ERROR,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1051
				'level' => /** @scrutinizer ignore-deprecated */ ILogger::ERROR,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1052
				'app' => 'settings',
1053
			]);
1054
			throw new OCSException('Sending email failed', 102);
1055
		}
1056
1057
		return new DataResponse();
1058
	}
1059
}
1060