Passed
Push — master ( e1a300...aa651f )
by Joas
15:36 queued 12s
created
lib/private/Accounts/AccountManager.php 1 patch
Indentation   +510 added lines, -510 removed lines patch added patch discarded remove patch
@@ -58,514 +58,514 @@
 block discarded – undo
58 58
  */
59 59
 class AccountManager implements IAccountManager {
60 60
 
61
-	/** @var  IDBConnection database connection */
62
-	private $connection;
63
-
64
-	/** @var IConfig */
65
-	private $config;
66
-
67
-	/** @var string table name */
68
-	private $table = 'accounts';
69
-
70
-	/** @var string table name */
71
-	private $dataTable = 'accounts_data';
72
-
73
-	/** @var EventDispatcherInterface */
74
-	private $eventDispatcher;
75
-
76
-	/** @var IJobList */
77
-	private $jobList;
78
-
79
-	/** @var LoggerInterface */
80
-	private $logger;
81
-
82
-	public function __construct(IDBConnection $connection,
83
-								IConfig $config,
84
-								EventDispatcherInterface $eventDispatcher,
85
-								IJobList $jobList,
86
-								LoggerInterface $logger) {
87
-		$this->connection = $connection;
88
-		$this->config = $config;
89
-		$this->eventDispatcher = $eventDispatcher;
90
-		$this->jobList = $jobList;
91
-		$this->logger = $logger;
92
-	}
93
-
94
-	/**
95
-	 * @param string $input
96
-	 * @return string Provided phone number in E.164 format when it was a valid number
97
-	 * @throws \InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
98
-	 */
99
-	protected function parsePhoneNumber(string $input): string {
100
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
101
-
102
-		if ($defaultRegion === '') {
103
-			// When no default region is set, only +49… numbers are valid
104
-			if (strpos($input, '+') !== 0) {
105
-				throw new \InvalidArgumentException(self::PROPERTY_PHONE);
106
-			}
107
-
108
-			$defaultRegion = 'EN';
109
-		}
110
-
111
-		$phoneUtil = PhoneNumberUtil::getInstance();
112
-		try {
113
-			$phoneNumber = $phoneUtil->parse($input, $defaultRegion);
114
-			if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) {
115
-				return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
116
-			}
117
-		} catch (NumberParseException $e) {
118
-		}
119
-
120
-		throw new \InvalidArgumentException(self::PROPERTY_PHONE);
121
-	}
122
-
123
-	/**
124
-	 *
125
-	 * @param string $input
126
-	 * @return string
127
-	 * @throws \InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
128
-	 */
129
-	protected function parseWebsite(string $input): string {
130
-		$parts = parse_url($input);
131
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
132
-			throw new \InvalidArgumentException(self::PROPERTY_WEBSITE);
133
-		}
134
-
135
-		if (!isset($parts['host']) || $parts['host'] === '') {
136
-			throw new \InvalidArgumentException(self::PROPERTY_WEBSITE);
137
-		}
138
-
139
-		return $input;
140
-	}
141
-
142
-	/**
143
-	 * update user record
144
-	 *
145
-	 * @param IUser $user
146
-	 * @param array $data
147
-	 * @param bool $throwOnData Set to true if you can inform the user about invalid data
148
-	 * @return array The potentially modified data (e.g. phone numbers are converted to E.164 format)
149
-	 * @throws \InvalidArgumentException Message is the property that was invalid
150
-	 */
151
-	public function updateUser(IUser $user, array $data, bool $throwOnData = false): array {
152
-		$userData = $this->getUser($user);
153
-		$updated = true;
154
-
155
-		if (isset($data[self::PROPERTY_PHONE]) && $data[self::PROPERTY_PHONE]['value'] !== '') {
156
-			// Sanitize null value.
157
-			$data[self::PROPERTY_PHONE]['value'] = $data[self::PROPERTY_PHONE]['value'] ?? '';
158
-
159
-			try {
160
-				$data[self::PROPERTY_PHONE]['value'] = $this->parsePhoneNumber($data[self::PROPERTY_PHONE]['value']);
161
-			} catch (\InvalidArgumentException $e) {
162
-				if ($throwOnData) {
163
-					throw $e;
164
-				}
165
-				$data[self::PROPERTY_PHONE]['value'] = '';
166
-			}
167
-		}
168
-
169
-		// set a max length
170
-		foreach ($data as $propertyName => $propertyData) {
171
-			if (isset($data[$propertyName]) && isset($data[$propertyName]['value']) && strlen($data[$propertyName]['value']) > 2048) {
172
-				if ($throwOnData) {
173
-					throw new \InvalidArgumentException($propertyName);
174
-				} else {
175
-					$data[$propertyName]['value'] = '';
176
-				}
177
-			}
178
-		}
179
-
180
-		if (isset($data[self::PROPERTY_WEBSITE]) && $data[self::PROPERTY_WEBSITE]['value'] !== '') {
181
-			try {
182
-				$data[self::PROPERTY_WEBSITE]['value'] = $this->parseWebsite($data[self::PROPERTY_WEBSITE]['value']);
183
-			} catch (\InvalidArgumentException $e) {
184
-				if ($throwOnData) {
185
-					throw $e;
186
-				}
187
-				$data[self::PROPERTY_WEBSITE]['value'] = '';
188
-			}
189
-		}
190
-
191
-		$allowedScopes = [
192
-			self::SCOPE_PRIVATE,
193
-			self::SCOPE_LOCAL,
194
-			self::SCOPE_FEDERATED,
195
-			self::SCOPE_PUBLISHED,
196
-			self::VISIBILITY_PRIVATE,
197
-			self::VISIBILITY_CONTACTS_ONLY,
198
-			self::VISIBILITY_PUBLIC,
199
-		];
200
-
201
-		// validate and convert scope values
202
-		foreach ($data as $propertyName => $propertyData) {
203
-			if (isset($propertyData['scope'])) {
204
-				if ($throwOnData && !in_array($propertyData['scope'], $allowedScopes, true)) {
205
-					throw new \InvalidArgumentException('scope');
206
-				}
207
-
208
-				if (
209
-					$propertyData['scope'] === self::SCOPE_PRIVATE
210
-					&& ($propertyName === self::PROPERTY_DISPLAYNAME || $propertyName === self::PROPERTY_EMAIL)
211
-				) {
212
-					if ($throwOnData) {
213
-						// v2-private is not available for these fields
214
-						throw new \InvalidArgumentException('scope');
215
-					} else {
216
-						// default to local
217
-						$data[$propertyName]['scope'] = self::SCOPE_LOCAL;
218
-					}
219
-				} else {
220
-					// migrate scope values to the new format
221
-					// invalid scopes are mapped to a default value
222
-					$data[$propertyName]['scope'] = AccountProperty::mapScopeToV2($propertyData['scope']);
223
-				}
224
-			}
225
-		}
226
-
227
-		if (empty($userData)) {
228
-			$this->insertNewUser($user, $data);
229
-		} elseif ($userData !== $data) {
230
-			$data = $this->checkEmailVerification($userData, $data, $user);
231
-			$data = $this->updateVerifyStatus($userData, $data);
232
-			$this->updateExistingUser($user, $data);
233
-		} else {
234
-			// nothing needs to be done if new and old data set are the same
235
-			$updated = false;
236
-		}
237
-
238
-		if ($updated) {
239
-			$this->eventDispatcher->dispatch(
240
-				'OC\AccountManager::userUpdated',
241
-				new GenericEvent($user, $data)
242
-			);
243
-		}
244
-
245
-		return $data;
246
-	}
247
-
248
-	/**
249
-	 * delete user from accounts table
250
-	 *
251
-	 * @param IUser $user
252
-	 */
253
-	public function deleteUser(IUser $user) {
254
-		$uid = $user->getUID();
255
-		$query = $this->connection->getQueryBuilder();
256
-		$query->delete($this->table)
257
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
258
-			->execute();
259
-
260
-		$this->deleteUserData($user);
261
-	}
262
-
263
-	/**
264
-	 * delete user from accounts table
265
-	 *
266
-	 * @param IUser $user
267
-	 */
268
-	public function deleteUserData(IUser $user): void {
269
-		$uid = $user->getUID();
270
-		$query = $this->connection->getQueryBuilder();
271
-		$query->delete($this->dataTable)
272
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
273
-			->execute();
274
-	}
275
-
276
-	/**
277
-	 * get stored data from a given user
278
-	 *
279
-	 * @param IUser $user
280
-	 * @return array
281
-	 *
282
-	 * @deprecated use getAccount instead to make sure migrated properties work correctly
283
-	 */
284
-	public function getUser(IUser $user) {
285
-		$uid = $user->getUID();
286
-		$query = $this->connection->getQueryBuilder();
287
-		$query->select('data')
288
-			->from($this->table)
289
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
290
-			->setParameter('uid', $uid);
291
-		$result = $query->execute();
292
-		$accountData = $result->fetchAll();
293
-		$result->closeCursor();
294
-
295
-		if (empty($accountData)) {
296
-			$userData = $this->buildDefaultUserRecord($user);
297
-			$this->insertNewUser($user, $userData);
298
-			return $userData;
299
-		}
300
-
301
-		$userDataArray = json_decode($accountData[0]['data'], true);
302
-		$jsonError = json_last_error();
303
-		if ($userDataArray === null || $userDataArray === [] || $jsonError !== JSON_ERROR_NONE) {
304
-			$this->logger->critical("User data of $uid contained invalid JSON (error $jsonError), hence falling back to a default user record");
305
-			return $this->buildDefaultUserRecord($user);
306
-		}
307
-
308
-		$userDataArray = $this->addMissingDefaultValues($userDataArray);
309
-
310
-		return $userDataArray;
311
-	}
312
-
313
-	public function searchUsers(string $property, array $values): array {
314
-		$chunks = array_chunk($values, 500);
315
-		$query = $this->connection->getQueryBuilder();
316
-		$query->select('*')
317
-			->from($this->dataTable)
318
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
319
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
320
-
321
-		$matches = [];
322
-		foreach ($chunks as $chunk) {
323
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
324
-			$result = $query->execute();
325
-
326
-			while ($row = $result->fetch()) {
327
-				$matches[$row['value']] = $row['uid'];
328
-			}
329
-			$result->closeCursor();
330
-		}
331
-
332
-		return $matches;
333
-	}
334
-
335
-	/**
336
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
337
-	 *
338
-	 * @param $oldData
339
-	 * @param $newData
340
-	 * @param IUser $user
341
-	 * @return array
342
-	 */
343
-	protected function checkEmailVerification($oldData, $newData, IUser $user) {
344
-		if ($oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']) {
345
-			$this->jobList->add(VerifyUserData::class,
346
-				[
347
-					'verificationCode' => '',
348
-					'data' => $newData[self::PROPERTY_EMAIL]['value'],
349
-					'type' => self::PROPERTY_EMAIL,
350
-					'uid' => $user->getUID(),
351
-					'try' => 0,
352
-					'lastRun' => time()
353
-				]
354
-			);
355
-			$newData[self::PROPERTY_EMAIL]['verified'] = self::VERIFICATION_IN_PROGRESS;
356
-		}
357
-
358
-		return $newData;
359
-	}
360
-
361
-	/**
362
-	 * make sure that all expected data are set
363
-	 *
364
-	 * @param array $userData
365
-	 * @return array
366
-	 */
367
-	protected function addMissingDefaultValues(array $userData) {
368
-		foreach ($userData as $key => $value) {
369
-			if (!isset($userData[$key]['verified'])) {
370
-				$userData[$key]['verified'] = self::NOT_VERIFIED;
371
-			}
372
-		}
373
-
374
-		return $userData;
375
-	}
376
-
377
-	/**
378
-	 * reset verification status if personal data changed
379
-	 *
380
-	 * @param array $oldData
381
-	 * @param array $newData
382
-	 * @return array
383
-	 */
384
-	protected function updateVerifyStatus($oldData, $newData) {
385
-
386
-		// which account was already verified successfully?
387
-		$twitterVerified = isset($oldData[self::PROPERTY_TWITTER]['verified']) && $oldData[self::PROPERTY_TWITTER]['verified'] === self::VERIFIED;
388
-		$websiteVerified = isset($oldData[self::PROPERTY_WEBSITE]['verified']) && $oldData[self::PROPERTY_WEBSITE]['verified'] === self::VERIFIED;
389
-		$emailVerified = isset($oldData[self::PROPERTY_EMAIL]['verified']) && $oldData[self::PROPERTY_EMAIL]['verified'] === self::VERIFIED;
390
-
391
-		// keep old verification status if we don't have a new one
392
-		if (!isset($newData[self::PROPERTY_TWITTER]['verified'])) {
393
-			// keep old verification status if value didn't changed and an old value exists
394
-			$keepOldStatus = $newData[self::PROPERTY_TWITTER]['value'] === $oldData[self::PROPERTY_TWITTER]['value'] && isset($oldData[self::PROPERTY_TWITTER]['verified']);
395
-			$newData[self::PROPERTY_TWITTER]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_TWITTER]['verified'] : self::NOT_VERIFIED;
396
-		}
397
-
398
-		if (!isset($newData[self::PROPERTY_WEBSITE]['verified'])) {
399
-			// keep old verification status if value didn't changed and an old value exists
400
-			$keepOldStatus = $newData[self::PROPERTY_WEBSITE]['value'] === $oldData[self::PROPERTY_WEBSITE]['value'] && isset($oldData[self::PROPERTY_WEBSITE]['verified']);
401
-			$newData[self::PROPERTY_WEBSITE]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_WEBSITE]['verified'] : self::NOT_VERIFIED;
402
-		}
403
-
404
-		if (!isset($newData[self::PROPERTY_EMAIL]['verified'])) {
405
-			// keep old verification status if value didn't changed and an old value exists
406
-			$keepOldStatus = $newData[self::PROPERTY_EMAIL]['value'] === $oldData[self::PROPERTY_EMAIL]['value'] && isset($oldData[self::PROPERTY_EMAIL]['verified']);
407
-			$newData[self::PROPERTY_EMAIL]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_EMAIL]['verified'] : self::VERIFICATION_IN_PROGRESS;
408
-		}
409
-
410
-		// reset verification status if a value from a previously verified data was changed
411
-		if ($twitterVerified &&
412
-			$oldData[self::PROPERTY_TWITTER]['value'] !== $newData[self::PROPERTY_TWITTER]['value']
413
-		) {
414
-			$newData[self::PROPERTY_TWITTER]['verified'] = self::NOT_VERIFIED;
415
-		}
416
-
417
-		if ($websiteVerified &&
418
-			$oldData[self::PROPERTY_WEBSITE]['value'] !== $newData[self::PROPERTY_WEBSITE]['value']
419
-		) {
420
-			$newData[self::PROPERTY_WEBSITE]['verified'] = self::NOT_VERIFIED;
421
-		}
422
-
423
-		if ($emailVerified &&
424
-			$oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']
425
-		) {
426
-			$newData[self::PROPERTY_EMAIL]['verified'] = self::NOT_VERIFIED;
427
-		}
428
-
429
-		return $newData;
430
-	}
431
-
432
-	/**
433
-	 * add new user to accounts table
434
-	 *
435
-	 * @param IUser $user
436
-	 * @param array $data
437
-	 */
438
-	protected function insertNewUser(IUser $user, array $data): void {
439
-		$uid = $user->getUID();
440
-		$jsonEncodedData = json_encode($data);
441
-		$query = $this->connection->getQueryBuilder();
442
-		$query->insert($this->table)
443
-			->values(
444
-				[
445
-					'uid' => $query->createNamedParameter($uid),
446
-					'data' => $query->createNamedParameter($jsonEncodedData),
447
-				]
448
-			)
449
-			->execute();
450
-
451
-		$this->deleteUserData($user);
452
-		$this->writeUserData($user, $data);
453
-	}
454
-
455
-	/**
456
-	 * update existing user in accounts table
457
-	 *
458
-	 * @param IUser $user
459
-	 * @param array $data
460
-	 */
461
-	protected function updateExistingUser(IUser $user, array $data): void {
462
-		$uid = $user->getUID();
463
-		$jsonEncodedData = json_encode($data);
464
-		$query = $this->connection->getQueryBuilder();
465
-		$query->update($this->table)
466
-			->set('data', $query->createNamedParameter($jsonEncodedData))
467
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
468
-			->execute();
469
-
470
-		$this->deleteUserData($user);
471
-		$this->writeUserData($user, $data);
472
-	}
473
-
474
-	protected function writeUserData(IUser $user, array $data): void {
475
-		$query = $this->connection->getQueryBuilder();
476
-		$query->insert($this->dataTable)
477
-			->values(
478
-				[
479
-					'uid' => $query->createNamedParameter($user->getUID()),
480
-					'name' => $query->createParameter('name'),
481
-					'value' => $query->createParameter('value'),
482
-				]
483
-			);
484
-		foreach ($data as $propertyName => $property) {
485
-			if ($propertyName === self::PROPERTY_AVATAR) {
486
-				continue;
487
-			}
488
-
489
-			$query->setParameter('name', $propertyName)
490
-				->setParameter('value', $property['value'] ?? '');
491
-			$query->execute();
492
-		}
493
-	}
494
-
495
-	/**
496
-	 * build default user record in case not data set exists yet
497
-	 *
498
-	 * @param IUser $user
499
-	 * @return array
500
-	 */
501
-	protected function buildDefaultUserRecord(IUser $user) {
502
-		return [
503
-			self::PROPERTY_DISPLAYNAME =>
504
-				[
505
-					'value' => $user->getDisplayName(),
506
-					'scope' => self::SCOPE_FEDERATED,
507
-					'verified' => self::NOT_VERIFIED,
508
-				],
509
-			self::PROPERTY_ADDRESS =>
510
-				[
511
-					'value' => '',
512
-					'scope' => self::SCOPE_LOCAL,
513
-					'verified' => self::NOT_VERIFIED,
514
-				],
515
-			self::PROPERTY_WEBSITE =>
516
-				[
517
-					'value' => '',
518
-					'scope' => self::SCOPE_LOCAL,
519
-					'verified' => self::NOT_VERIFIED,
520
-				],
521
-			self::PROPERTY_EMAIL =>
522
-				[
523
-					'value' => $user->getEMailAddress(),
524
-					'scope' => self::SCOPE_FEDERATED,
525
-					'verified' => self::NOT_VERIFIED,
526
-				],
527
-			self::PROPERTY_AVATAR =>
528
-				[
529
-					'scope' => self::SCOPE_FEDERATED
530
-				],
531
-			self::PROPERTY_PHONE =>
532
-				[
533
-					'value' => '',
534
-					'scope' => self::SCOPE_LOCAL,
535
-					'verified' => self::NOT_VERIFIED,
536
-				],
537
-			self::PROPERTY_TWITTER =>
538
-				[
539
-					'value' => '',
540
-					'scope' => self::SCOPE_LOCAL,
541
-					'verified' => self::NOT_VERIFIED,
542
-				],
543
-		];
544
-	}
545
-
546
-	private function parseAccountData(IUser $user, $data): Account {
547
-		$account = new Account($user);
548
-		foreach ($data as $property => $accountData) {
549
-			$account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
550
-		}
551
-		return $account;
552
-	}
553
-
554
-	public function getAccount(IUser $user): IAccount {
555
-		return $this->parseAccountData($user, $this->getUser($user));
556
-	}
557
-
558
-	public function updateAccount(IAccount $account): void {
559
-		$data = [];
560
-
561
-		foreach ($account->getProperties() as $property) {
562
-			$data[$property->getName()] = [
563
-				'value' => $property->getValue(),
564
-				'scope' => $property->getScope(),
565
-				'verified' => $property->getVerified(),
566
-			];
567
-		}
568
-
569
-		$this->updateUser($account->getUser(), $data, true);
570
-	}
61
+    /** @var  IDBConnection database connection */
62
+    private $connection;
63
+
64
+    /** @var IConfig */
65
+    private $config;
66
+
67
+    /** @var string table name */
68
+    private $table = 'accounts';
69
+
70
+    /** @var string table name */
71
+    private $dataTable = 'accounts_data';
72
+
73
+    /** @var EventDispatcherInterface */
74
+    private $eventDispatcher;
75
+
76
+    /** @var IJobList */
77
+    private $jobList;
78
+
79
+    /** @var LoggerInterface */
80
+    private $logger;
81
+
82
+    public function __construct(IDBConnection $connection,
83
+                                IConfig $config,
84
+                                EventDispatcherInterface $eventDispatcher,
85
+                                IJobList $jobList,
86
+                                LoggerInterface $logger) {
87
+        $this->connection = $connection;
88
+        $this->config = $config;
89
+        $this->eventDispatcher = $eventDispatcher;
90
+        $this->jobList = $jobList;
91
+        $this->logger = $logger;
92
+    }
93
+
94
+    /**
95
+     * @param string $input
96
+     * @return string Provided phone number in E.164 format when it was a valid number
97
+     * @throws \InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
98
+     */
99
+    protected function parsePhoneNumber(string $input): string {
100
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
101
+
102
+        if ($defaultRegion === '') {
103
+            // When no default region is set, only +49… numbers are valid
104
+            if (strpos($input, '+') !== 0) {
105
+                throw new \InvalidArgumentException(self::PROPERTY_PHONE);
106
+            }
107
+
108
+            $defaultRegion = 'EN';
109
+        }
110
+
111
+        $phoneUtil = PhoneNumberUtil::getInstance();
112
+        try {
113
+            $phoneNumber = $phoneUtil->parse($input, $defaultRegion);
114
+            if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) {
115
+                return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
116
+            }
117
+        } catch (NumberParseException $e) {
118
+        }
119
+
120
+        throw new \InvalidArgumentException(self::PROPERTY_PHONE);
121
+    }
122
+
123
+    /**
124
+     *
125
+     * @param string $input
126
+     * @return string
127
+     * @throws \InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
128
+     */
129
+    protected function parseWebsite(string $input): string {
130
+        $parts = parse_url($input);
131
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
132
+            throw new \InvalidArgumentException(self::PROPERTY_WEBSITE);
133
+        }
134
+
135
+        if (!isset($parts['host']) || $parts['host'] === '') {
136
+            throw new \InvalidArgumentException(self::PROPERTY_WEBSITE);
137
+        }
138
+
139
+        return $input;
140
+    }
141
+
142
+    /**
143
+     * update user record
144
+     *
145
+     * @param IUser $user
146
+     * @param array $data
147
+     * @param bool $throwOnData Set to true if you can inform the user about invalid data
148
+     * @return array The potentially modified data (e.g. phone numbers are converted to E.164 format)
149
+     * @throws \InvalidArgumentException Message is the property that was invalid
150
+     */
151
+    public function updateUser(IUser $user, array $data, bool $throwOnData = false): array {
152
+        $userData = $this->getUser($user);
153
+        $updated = true;
154
+
155
+        if (isset($data[self::PROPERTY_PHONE]) && $data[self::PROPERTY_PHONE]['value'] !== '') {
156
+            // Sanitize null value.
157
+            $data[self::PROPERTY_PHONE]['value'] = $data[self::PROPERTY_PHONE]['value'] ?? '';
158
+
159
+            try {
160
+                $data[self::PROPERTY_PHONE]['value'] = $this->parsePhoneNumber($data[self::PROPERTY_PHONE]['value']);
161
+            } catch (\InvalidArgumentException $e) {
162
+                if ($throwOnData) {
163
+                    throw $e;
164
+                }
165
+                $data[self::PROPERTY_PHONE]['value'] = '';
166
+            }
167
+        }
168
+
169
+        // set a max length
170
+        foreach ($data as $propertyName => $propertyData) {
171
+            if (isset($data[$propertyName]) && isset($data[$propertyName]['value']) && strlen($data[$propertyName]['value']) > 2048) {
172
+                if ($throwOnData) {
173
+                    throw new \InvalidArgumentException($propertyName);
174
+                } else {
175
+                    $data[$propertyName]['value'] = '';
176
+                }
177
+            }
178
+        }
179
+
180
+        if (isset($data[self::PROPERTY_WEBSITE]) && $data[self::PROPERTY_WEBSITE]['value'] !== '') {
181
+            try {
182
+                $data[self::PROPERTY_WEBSITE]['value'] = $this->parseWebsite($data[self::PROPERTY_WEBSITE]['value']);
183
+            } catch (\InvalidArgumentException $e) {
184
+                if ($throwOnData) {
185
+                    throw $e;
186
+                }
187
+                $data[self::PROPERTY_WEBSITE]['value'] = '';
188
+            }
189
+        }
190
+
191
+        $allowedScopes = [
192
+            self::SCOPE_PRIVATE,
193
+            self::SCOPE_LOCAL,
194
+            self::SCOPE_FEDERATED,
195
+            self::SCOPE_PUBLISHED,
196
+            self::VISIBILITY_PRIVATE,
197
+            self::VISIBILITY_CONTACTS_ONLY,
198
+            self::VISIBILITY_PUBLIC,
199
+        ];
200
+
201
+        // validate and convert scope values
202
+        foreach ($data as $propertyName => $propertyData) {
203
+            if (isset($propertyData['scope'])) {
204
+                if ($throwOnData && !in_array($propertyData['scope'], $allowedScopes, true)) {
205
+                    throw new \InvalidArgumentException('scope');
206
+                }
207
+
208
+                if (
209
+                    $propertyData['scope'] === self::SCOPE_PRIVATE
210
+                    && ($propertyName === self::PROPERTY_DISPLAYNAME || $propertyName === self::PROPERTY_EMAIL)
211
+                ) {
212
+                    if ($throwOnData) {
213
+                        // v2-private is not available for these fields
214
+                        throw new \InvalidArgumentException('scope');
215
+                    } else {
216
+                        // default to local
217
+                        $data[$propertyName]['scope'] = self::SCOPE_LOCAL;
218
+                    }
219
+                } else {
220
+                    // migrate scope values to the new format
221
+                    // invalid scopes are mapped to a default value
222
+                    $data[$propertyName]['scope'] = AccountProperty::mapScopeToV2($propertyData['scope']);
223
+                }
224
+            }
225
+        }
226
+
227
+        if (empty($userData)) {
228
+            $this->insertNewUser($user, $data);
229
+        } elseif ($userData !== $data) {
230
+            $data = $this->checkEmailVerification($userData, $data, $user);
231
+            $data = $this->updateVerifyStatus($userData, $data);
232
+            $this->updateExistingUser($user, $data);
233
+        } else {
234
+            // nothing needs to be done if new and old data set are the same
235
+            $updated = false;
236
+        }
237
+
238
+        if ($updated) {
239
+            $this->eventDispatcher->dispatch(
240
+                'OC\AccountManager::userUpdated',
241
+                new GenericEvent($user, $data)
242
+            );
243
+        }
244
+
245
+        return $data;
246
+    }
247
+
248
+    /**
249
+     * delete user from accounts table
250
+     *
251
+     * @param IUser $user
252
+     */
253
+    public function deleteUser(IUser $user) {
254
+        $uid = $user->getUID();
255
+        $query = $this->connection->getQueryBuilder();
256
+        $query->delete($this->table)
257
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
258
+            ->execute();
259
+
260
+        $this->deleteUserData($user);
261
+    }
262
+
263
+    /**
264
+     * delete user from accounts table
265
+     *
266
+     * @param IUser $user
267
+     */
268
+    public function deleteUserData(IUser $user): void {
269
+        $uid = $user->getUID();
270
+        $query = $this->connection->getQueryBuilder();
271
+        $query->delete($this->dataTable)
272
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
273
+            ->execute();
274
+    }
275
+
276
+    /**
277
+     * get stored data from a given user
278
+     *
279
+     * @param IUser $user
280
+     * @return array
281
+     *
282
+     * @deprecated use getAccount instead to make sure migrated properties work correctly
283
+     */
284
+    public function getUser(IUser $user) {
285
+        $uid = $user->getUID();
286
+        $query = $this->connection->getQueryBuilder();
287
+        $query->select('data')
288
+            ->from($this->table)
289
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
290
+            ->setParameter('uid', $uid);
291
+        $result = $query->execute();
292
+        $accountData = $result->fetchAll();
293
+        $result->closeCursor();
294
+
295
+        if (empty($accountData)) {
296
+            $userData = $this->buildDefaultUserRecord($user);
297
+            $this->insertNewUser($user, $userData);
298
+            return $userData;
299
+        }
300
+
301
+        $userDataArray = json_decode($accountData[0]['data'], true);
302
+        $jsonError = json_last_error();
303
+        if ($userDataArray === null || $userDataArray === [] || $jsonError !== JSON_ERROR_NONE) {
304
+            $this->logger->critical("User data of $uid contained invalid JSON (error $jsonError), hence falling back to a default user record");
305
+            return $this->buildDefaultUserRecord($user);
306
+        }
307
+
308
+        $userDataArray = $this->addMissingDefaultValues($userDataArray);
309
+
310
+        return $userDataArray;
311
+    }
312
+
313
+    public function searchUsers(string $property, array $values): array {
314
+        $chunks = array_chunk($values, 500);
315
+        $query = $this->connection->getQueryBuilder();
316
+        $query->select('*')
317
+            ->from($this->dataTable)
318
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
319
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
320
+
321
+        $matches = [];
322
+        foreach ($chunks as $chunk) {
323
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
324
+            $result = $query->execute();
325
+
326
+            while ($row = $result->fetch()) {
327
+                $matches[$row['value']] = $row['uid'];
328
+            }
329
+            $result->closeCursor();
330
+        }
331
+
332
+        return $matches;
333
+    }
334
+
335
+    /**
336
+     * check if we need to ask the server for email verification, if yes we create a cronjob
337
+     *
338
+     * @param $oldData
339
+     * @param $newData
340
+     * @param IUser $user
341
+     * @return array
342
+     */
343
+    protected function checkEmailVerification($oldData, $newData, IUser $user) {
344
+        if ($oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']) {
345
+            $this->jobList->add(VerifyUserData::class,
346
+                [
347
+                    'verificationCode' => '',
348
+                    'data' => $newData[self::PROPERTY_EMAIL]['value'],
349
+                    'type' => self::PROPERTY_EMAIL,
350
+                    'uid' => $user->getUID(),
351
+                    'try' => 0,
352
+                    'lastRun' => time()
353
+                ]
354
+            );
355
+            $newData[self::PROPERTY_EMAIL]['verified'] = self::VERIFICATION_IN_PROGRESS;
356
+        }
357
+
358
+        return $newData;
359
+    }
360
+
361
+    /**
362
+     * make sure that all expected data are set
363
+     *
364
+     * @param array $userData
365
+     * @return array
366
+     */
367
+    protected function addMissingDefaultValues(array $userData) {
368
+        foreach ($userData as $key => $value) {
369
+            if (!isset($userData[$key]['verified'])) {
370
+                $userData[$key]['verified'] = self::NOT_VERIFIED;
371
+            }
372
+        }
373
+
374
+        return $userData;
375
+    }
376
+
377
+    /**
378
+     * reset verification status if personal data changed
379
+     *
380
+     * @param array $oldData
381
+     * @param array $newData
382
+     * @return array
383
+     */
384
+    protected function updateVerifyStatus($oldData, $newData) {
385
+
386
+        // which account was already verified successfully?
387
+        $twitterVerified = isset($oldData[self::PROPERTY_TWITTER]['verified']) && $oldData[self::PROPERTY_TWITTER]['verified'] === self::VERIFIED;
388
+        $websiteVerified = isset($oldData[self::PROPERTY_WEBSITE]['verified']) && $oldData[self::PROPERTY_WEBSITE]['verified'] === self::VERIFIED;
389
+        $emailVerified = isset($oldData[self::PROPERTY_EMAIL]['verified']) && $oldData[self::PROPERTY_EMAIL]['verified'] === self::VERIFIED;
390
+
391
+        // keep old verification status if we don't have a new one
392
+        if (!isset($newData[self::PROPERTY_TWITTER]['verified'])) {
393
+            // keep old verification status if value didn't changed and an old value exists
394
+            $keepOldStatus = $newData[self::PROPERTY_TWITTER]['value'] === $oldData[self::PROPERTY_TWITTER]['value'] && isset($oldData[self::PROPERTY_TWITTER]['verified']);
395
+            $newData[self::PROPERTY_TWITTER]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_TWITTER]['verified'] : self::NOT_VERIFIED;
396
+        }
397
+
398
+        if (!isset($newData[self::PROPERTY_WEBSITE]['verified'])) {
399
+            // keep old verification status if value didn't changed and an old value exists
400
+            $keepOldStatus = $newData[self::PROPERTY_WEBSITE]['value'] === $oldData[self::PROPERTY_WEBSITE]['value'] && isset($oldData[self::PROPERTY_WEBSITE]['verified']);
401
+            $newData[self::PROPERTY_WEBSITE]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_WEBSITE]['verified'] : self::NOT_VERIFIED;
402
+        }
403
+
404
+        if (!isset($newData[self::PROPERTY_EMAIL]['verified'])) {
405
+            // keep old verification status if value didn't changed and an old value exists
406
+            $keepOldStatus = $newData[self::PROPERTY_EMAIL]['value'] === $oldData[self::PROPERTY_EMAIL]['value'] && isset($oldData[self::PROPERTY_EMAIL]['verified']);
407
+            $newData[self::PROPERTY_EMAIL]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_EMAIL]['verified'] : self::VERIFICATION_IN_PROGRESS;
408
+        }
409
+
410
+        // reset verification status if a value from a previously verified data was changed
411
+        if ($twitterVerified &&
412
+            $oldData[self::PROPERTY_TWITTER]['value'] !== $newData[self::PROPERTY_TWITTER]['value']
413
+        ) {
414
+            $newData[self::PROPERTY_TWITTER]['verified'] = self::NOT_VERIFIED;
415
+        }
416
+
417
+        if ($websiteVerified &&
418
+            $oldData[self::PROPERTY_WEBSITE]['value'] !== $newData[self::PROPERTY_WEBSITE]['value']
419
+        ) {
420
+            $newData[self::PROPERTY_WEBSITE]['verified'] = self::NOT_VERIFIED;
421
+        }
422
+
423
+        if ($emailVerified &&
424
+            $oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']
425
+        ) {
426
+            $newData[self::PROPERTY_EMAIL]['verified'] = self::NOT_VERIFIED;
427
+        }
428
+
429
+        return $newData;
430
+    }
431
+
432
+    /**
433
+     * add new user to accounts table
434
+     *
435
+     * @param IUser $user
436
+     * @param array $data
437
+     */
438
+    protected function insertNewUser(IUser $user, array $data): void {
439
+        $uid = $user->getUID();
440
+        $jsonEncodedData = json_encode($data);
441
+        $query = $this->connection->getQueryBuilder();
442
+        $query->insert($this->table)
443
+            ->values(
444
+                [
445
+                    'uid' => $query->createNamedParameter($uid),
446
+                    'data' => $query->createNamedParameter($jsonEncodedData),
447
+                ]
448
+            )
449
+            ->execute();
450
+
451
+        $this->deleteUserData($user);
452
+        $this->writeUserData($user, $data);
453
+    }
454
+
455
+    /**
456
+     * update existing user in accounts table
457
+     *
458
+     * @param IUser $user
459
+     * @param array $data
460
+     */
461
+    protected function updateExistingUser(IUser $user, array $data): void {
462
+        $uid = $user->getUID();
463
+        $jsonEncodedData = json_encode($data);
464
+        $query = $this->connection->getQueryBuilder();
465
+        $query->update($this->table)
466
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
467
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
468
+            ->execute();
469
+
470
+        $this->deleteUserData($user);
471
+        $this->writeUserData($user, $data);
472
+    }
473
+
474
+    protected function writeUserData(IUser $user, array $data): void {
475
+        $query = $this->connection->getQueryBuilder();
476
+        $query->insert($this->dataTable)
477
+            ->values(
478
+                [
479
+                    'uid' => $query->createNamedParameter($user->getUID()),
480
+                    'name' => $query->createParameter('name'),
481
+                    'value' => $query->createParameter('value'),
482
+                ]
483
+            );
484
+        foreach ($data as $propertyName => $property) {
485
+            if ($propertyName === self::PROPERTY_AVATAR) {
486
+                continue;
487
+            }
488
+
489
+            $query->setParameter('name', $propertyName)
490
+                ->setParameter('value', $property['value'] ?? '');
491
+            $query->execute();
492
+        }
493
+    }
494
+
495
+    /**
496
+     * build default user record in case not data set exists yet
497
+     *
498
+     * @param IUser $user
499
+     * @return array
500
+     */
501
+    protected function buildDefaultUserRecord(IUser $user) {
502
+        return [
503
+            self::PROPERTY_DISPLAYNAME =>
504
+                [
505
+                    'value' => $user->getDisplayName(),
506
+                    'scope' => self::SCOPE_FEDERATED,
507
+                    'verified' => self::NOT_VERIFIED,
508
+                ],
509
+            self::PROPERTY_ADDRESS =>
510
+                [
511
+                    'value' => '',
512
+                    'scope' => self::SCOPE_LOCAL,
513
+                    'verified' => self::NOT_VERIFIED,
514
+                ],
515
+            self::PROPERTY_WEBSITE =>
516
+                [
517
+                    'value' => '',
518
+                    'scope' => self::SCOPE_LOCAL,
519
+                    'verified' => self::NOT_VERIFIED,
520
+                ],
521
+            self::PROPERTY_EMAIL =>
522
+                [
523
+                    'value' => $user->getEMailAddress(),
524
+                    'scope' => self::SCOPE_FEDERATED,
525
+                    'verified' => self::NOT_VERIFIED,
526
+                ],
527
+            self::PROPERTY_AVATAR =>
528
+                [
529
+                    'scope' => self::SCOPE_FEDERATED
530
+                ],
531
+            self::PROPERTY_PHONE =>
532
+                [
533
+                    'value' => '',
534
+                    'scope' => self::SCOPE_LOCAL,
535
+                    'verified' => self::NOT_VERIFIED,
536
+                ],
537
+            self::PROPERTY_TWITTER =>
538
+                [
539
+                    'value' => '',
540
+                    'scope' => self::SCOPE_LOCAL,
541
+                    'verified' => self::NOT_VERIFIED,
542
+                ],
543
+        ];
544
+    }
545
+
546
+    private function parseAccountData(IUser $user, $data): Account {
547
+        $account = new Account($user);
548
+        foreach ($data as $property => $accountData) {
549
+            $account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
550
+        }
551
+        return $account;
552
+    }
553
+
554
+    public function getAccount(IUser $user): IAccount {
555
+        return $this->parseAccountData($user, $this->getUser($user));
556
+    }
557
+
558
+    public function updateAccount(IAccount $account): void {
559
+        $data = [];
560
+
561
+        foreach ($account->getProperties() as $property) {
562
+            $data[$property->getName()] = [
563
+                'value' => $property->getValue(),
564
+                'scope' => $property->getScope(),
565
+                'verified' => $property->getVerified(),
566
+            ];
567
+        }
568
+
569
+        $this->updateUser($account->getUser(), $data, true);
570
+    }
571 571
 }
Please login to merge, or discard this patch.
apps/settings/lib/Controller/UsersController.php 1 patch
Indentation   +545 added lines, -545 removed lines patch added patch discarded remove patch
@@ -70,549 +70,549 @@
 block discarded – undo
70 70
 use function in_array;
71 71
 
72 72
 class UsersController extends Controller {
73
-	/** @var UserManager */
74
-	private $userManager;
75
-	/** @var GroupManager */
76
-	private $groupManager;
77
-	/** @var IUserSession */
78
-	private $userSession;
79
-	/** @var IConfig */
80
-	private $config;
81
-	/** @var bool */
82
-	private $isAdmin;
83
-	/** @var IL10N */
84
-	private $l10n;
85
-	/** @var IMailer */
86
-	private $mailer;
87
-	/** @var Factory */
88
-	private $l10nFactory;
89
-	/** @var IAppManager */
90
-	private $appManager;
91
-	/** @var AccountManager */
92
-	private $accountManager;
93
-	/** @var Manager */
94
-	private $keyManager;
95
-	/** @var IJobList */
96
-	private $jobList;
97
-	/** @var IManager */
98
-	private $encryptionManager;
99
-	/** @var KnownUserService */
100
-	private $knownUserService;
101
-	/** @var IEventDispatcher */
102
-	private $dispatcher;
103
-
104
-
105
-	public function __construct(
106
-		string $appName,
107
-		IRequest $request,
108
-		IUserManager $userManager,
109
-		IGroupManager $groupManager,
110
-		IUserSession $userSession,
111
-		IConfig $config,
112
-		bool $isAdmin,
113
-		IL10N $l10n,
114
-		IMailer $mailer,
115
-		IFactory $l10nFactory,
116
-		IAppManager $appManager,
117
-		AccountManager $accountManager,
118
-		Manager $keyManager,
119
-		IJobList $jobList,
120
-		IManager $encryptionManager,
121
-		KnownUserService $knownUserService,
122
-		IEventDispatcher $dispatcher
123
-	) {
124
-		parent::__construct($appName, $request);
125
-		$this->userManager = $userManager;
126
-		$this->groupManager = $groupManager;
127
-		$this->userSession = $userSession;
128
-		$this->config = $config;
129
-		$this->isAdmin = $isAdmin;
130
-		$this->l10n = $l10n;
131
-		$this->mailer = $mailer;
132
-		$this->l10nFactory = $l10nFactory;
133
-		$this->appManager = $appManager;
134
-		$this->accountManager = $accountManager;
135
-		$this->keyManager = $keyManager;
136
-		$this->jobList = $jobList;
137
-		$this->encryptionManager = $encryptionManager;
138
-		$this->knownUserService = $knownUserService;
139
-		$this->dispatcher = $dispatcher;
140
-	}
141
-
142
-
143
-	/**
144
-	 * @NoCSRFRequired
145
-	 * @NoAdminRequired
146
-	 *
147
-	 * Display users list template
148
-	 *
149
-	 * @return TemplateResponse
150
-	 */
151
-	public function usersListByGroup(): TemplateResponse {
152
-		return $this->usersList();
153
-	}
154
-
155
-	/**
156
-	 * @NoCSRFRequired
157
-	 * @NoAdminRequired
158
-	 *
159
-	 * Display users list template
160
-	 *
161
-	 * @return TemplateResponse
162
-	 */
163
-	public function usersList(): TemplateResponse {
164
-		$user = $this->userSession->getUser();
165
-		$uid = $user->getUID();
166
-
167
-		\OC::$server->getNavigationManager()->setActiveEntry('core_users');
168
-
169
-		/* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */
170
-		$sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT;
171
-		$isLDAPUsed = false;
172
-		if ($this->config->getSystemValue('sort_groups_by_name', false)) {
173
-			$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
174
-		} else {
175
-			if ($this->appManager->isEnabledForUser('user_ldap')) {
176
-				$isLDAPUsed =
177
-					$this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy');
178
-				if ($isLDAPUsed) {
179
-					// LDAP user count can be slow, so we sort by group name here
180
-					$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
181
-				}
182
-			}
183
-		}
184
-
185
-		$canChangePassword = $this->canAdminChangeUserPasswords();
186
-
187
-		/* GROUPS */
188
-		$groupsInfo = new \OC\Group\MetaData(
189
-			$uid,
190
-			$this->isAdmin,
191
-			$this->groupManager,
192
-			$this->userSession
193
-		);
194
-
195
-		$groupsInfo->setSorting($sortGroupsBy);
196
-		[$adminGroup, $groups] = $groupsInfo->get();
197
-
198
-		if (!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) {
199
-			$isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) {
200
-				return $ldapFound || $backend instanceof User_Proxy;
201
-			});
202
-		}
203
-
204
-		$disabledUsers = -1;
205
-		$userCount = 0;
206
-
207
-		if (!$isLDAPUsed) {
208
-			if ($this->isAdmin) {
209
-				$disabledUsers = $this->userManager->countDisabledUsers();
210
-				$userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) {
211
-					return $v + (int)$w;
212
-				}, 0);
213
-			} else {
214
-				// User is subadmin !
215
-				// Map group list to names to retrieve the countDisabledUsersOfGroups
216
-				$userGroups = $this->groupManager->getUserGroups($user);
217
-				$groupsNames = [];
218
-
219
-				foreach ($groups as $key => $group) {
220
-					// $userCount += (int)$group['usercount'];
221
-					array_push($groupsNames, $group['name']);
222
-					// we prevent subadmins from looking up themselves
223
-					// so we lower the count of the groups he belongs to
224
-					if (array_key_exists($group['id'], $userGroups)) {
225
-						$groups[$key]['usercount']--;
226
-						$userCount -= 1; // we also lower from one the total count
227
-					}
228
-				}
229
-				$userCount += $this->userManager->countUsersOfGroups($groupsInfo->getGroups());
230
-				$disabledUsers = $this->userManager->countDisabledUsersOfGroups($groupsNames);
231
-			}
232
-
233
-			$userCount -= $disabledUsers;
234
-		}
235
-
236
-		$disabledUsersGroup = [
237
-			'id' => 'disabled',
238
-			'name' => 'Disabled users',
239
-			'usercount' => $disabledUsers
240
-		];
241
-
242
-		/* QUOTAS PRESETS */
243
-		$quotaPreset = $this->parseQuotaPreset($this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB'));
244
-		$defaultQuota = $this->config->getAppValue('files', 'default_quota', 'none');
245
-
246
-		$event = new BeforeTemplateRenderedEvent();
247
-		$this->dispatcher->dispatch('OC\Settings\Users::loadAdditionalScripts', $event);
248
-		$this->dispatcher->dispatchTyped($event);
249
-
250
-		/* LANGUAGES */
251
-		$languages = $this->l10nFactory->getLanguages();
252
-
253
-		/* FINAL DATA */
254
-		$serverData = [];
255
-		// groups
256
-		$serverData['groups'] = array_merge_recursive($adminGroup, [$disabledUsersGroup], $groups);
257
-		// Various data
258
-		$serverData['isAdmin'] = $this->isAdmin;
259
-		$serverData['sortGroups'] = $sortGroupsBy;
260
-		$serverData['quotaPreset'] = $quotaPreset;
261
-		$serverData['userCount'] = $userCount;
262
-		$serverData['languages'] = $languages;
263
-		$serverData['defaultLanguage'] = $this->config->getSystemValue('default_language', 'en');
264
-		$serverData['forceLanguage'] = $this->config->getSystemValue('force_language', false);
265
-		// Settings
266
-		$serverData['defaultQuota'] = $defaultQuota;
267
-		$serverData['canChangePassword'] = $canChangePassword;
268
-		$serverData['newUserGenerateUserID'] = $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes';
269
-		$serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes';
270
-		$serverData['newUserSendEmail'] = $this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes';
271
-
272
-		return new TemplateResponse('settings', 'settings-vue', ['serverData' => $serverData]);
273
-	}
274
-
275
-	/**
276
-	 * @param string $key
277
-	 * @param string $value
278
-	 *
279
-	 * @return JSONResponse
280
-	 */
281
-	public function setPreference(string $key, string $value): JSONResponse {
282
-		$allowed = ['newUser.sendEmail'];
283
-		if (!in_array($key, $allowed, true)) {
284
-			return new JSONResponse([], Http::STATUS_FORBIDDEN);
285
-		}
286
-
287
-		$this->config->setAppValue('core', $key, $value);
288
-
289
-		return new JSONResponse([]);
290
-	}
291
-
292
-	/**
293
-	 * Parse the app value for quota_present
294
-	 *
295
-	 * @param string $quotaPreset
296
-	 * @return array
297
-	 */
298
-	protected function parseQuotaPreset(string $quotaPreset): array {
299
-		// 1 GB, 5 GB, 10 GB => [1 GB, 5 GB, 10 GB]
300
-		$presets = array_filter(array_map('trim', explode(',', $quotaPreset)));
301
-		// Drop default and none, Make array indexes numerically
302
-		return array_values(array_diff($presets, ['default', 'none']));
303
-	}
304
-
305
-	/**
306
-	 * check if the admin can change the users password
307
-	 *
308
-	 * The admin can change the passwords if:
309
-	 *
310
-	 *   - no encryption module is loaded and encryption is disabled
311
-	 *   - encryption module is loaded but it doesn't require per user keys
312
-	 *
313
-	 * The admin can not change the passwords if:
314
-	 *
315
-	 *   - an encryption module is loaded and it uses per-user keys
316
-	 *   - encryption is enabled but no encryption modules are loaded
317
-	 *
318
-	 * @return bool
319
-	 */
320
-	protected function canAdminChangeUserPasswords(): bool {
321
-		$isEncryptionEnabled = $this->encryptionManager->isEnabled();
322
-		try {
323
-			$noUserSpecificEncryptionKeys = !$this->encryptionManager->getEncryptionModule()->needDetailedAccessList();
324
-			$isEncryptionModuleLoaded = true;
325
-		} catch (ModuleDoesNotExistsException $e) {
326
-			$noUserSpecificEncryptionKeys = true;
327
-			$isEncryptionModuleLoaded = false;
328
-		}
329
-		$canChangePassword = ($isEncryptionModuleLoaded && $noUserSpecificEncryptionKeys)
330
-			|| (!$isEncryptionModuleLoaded && !$isEncryptionEnabled);
331
-
332
-		return $canChangePassword;
333
-	}
334
-
335
-	/**
336
-	 * @NoAdminRequired
337
-	 * @NoSubAdminRequired
338
-	 * @PasswordConfirmationRequired
339
-	 *
340
-	 * @param string|null $avatarScope
341
-	 * @param string|null $displayname
342
-	 * @param string|null $displaynameScope
343
-	 * @param string|null $phone
344
-	 * @param string|null $phoneScope
345
-	 * @param string|null $email
346
-	 * @param string|null $emailScope
347
-	 * @param string|null $website
348
-	 * @param string|null $websiteScope
349
-	 * @param string|null $address
350
-	 * @param string|null $addressScope
351
-	 * @param string|null $twitter
352
-	 * @param string|null $twitterScope
353
-	 *
354
-	 * @return DataResponse
355
-	 */
356
-	public function setUserSettings(?string $avatarScope = null,
357
-									?string $displayname = null,
358
-									?string $displaynameScope = null,
359
-									?string $phone = null,
360
-									?string $phoneScope = null,
361
-									?string $email = null,
362
-									?string $emailScope = null,
363
-									?string $website = null,
364
-									?string $websiteScope = null,
365
-									?string $address = null,
366
-									?string $addressScope = null,
367
-									?string $twitter = null,
368
-									?string $twitterScope = null
369
-	) {
370
-		$user = $this->userSession->getUser();
371
-		if (!$user instanceof IUser) {
372
-			return new DataResponse(
373
-				[
374
-					'status' => 'error',
375
-					'data' => [
376
-						'message' => $this->l10n->t('Invalid user')
377
-					]
378
-				],
379
-				Http::STATUS_UNAUTHORIZED
380
-			);
381
-		}
382
-
383
-		$email = !is_null($email) ? strtolower($email) : $email;
384
-		if (!empty($email) && !$this->mailer->validateMailAddress($email)) {
385
-			return new DataResponse(
386
-				[
387
-					'status' => 'error',
388
-					'data' => [
389
-						'message' => $this->l10n->t('Invalid mail address')
390
-					]
391
-				],
392
-				Http::STATUS_UNPROCESSABLE_ENTITY
393
-			);
394
-		}
395
-
396
-		$data = $this->accountManager->getUser($user);
397
-		$beforeData = $data;
398
-		if (!is_null($avatarScope)) {
399
-			$data[IAccountManager::PROPERTY_AVATAR]['scope'] = $avatarScope;
400
-		}
401
-		if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
402
-			if (!is_null($displayname)) {
403
-				$data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $displayname;
404
-			}
405
-			if (!is_null($displaynameScope)) {
406
-				$data[IAccountManager::PROPERTY_DISPLAYNAME]['scope'] = $displaynameScope;
407
-			}
408
-			if (!is_null($email)) {
409
-				$data[IAccountManager::PROPERTY_EMAIL]['value'] = $email;
410
-			}
411
-			if (!is_null($emailScope)) {
412
-				$data[IAccountManager::PROPERTY_EMAIL]['scope'] = $emailScope;
413
-			}
414
-		}
415
-		if (!is_null($website)) {
416
-			$data[IAccountManager::PROPERTY_WEBSITE]['value'] = $website;
417
-		}
418
-		if (!is_null($websiteScope)) {
419
-			$data[IAccountManager::PROPERTY_WEBSITE]['scope'] = $websiteScope;
420
-		}
421
-		if (!is_null($address)) {
422
-			$data[IAccountManager::PROPERTY_ADDRESS]['value'] = $address;
423
-		}
424
-		if (!is_null($addressScope)) {
425
-			$data[IAccountManager::PROPERTY_ADDRESS]['scope'] = $addressScope;
426
-		}
427
-		if (!is_null($phone)) {
428
-			$data[IAccountManager::PROPERTY_PHONE]['value'] = $phone;
429
-		}
430
-		if (!is_null($phoneScope)) {
431
-			$data[IAccountManager::PROPERTY_PHONE]['scope'] = $phoneScope;
432
-		}
433
-		if (!is_null($twitter)) {
434
-			$data[IAccountManager::PROPERTY_TWITTER]['value'] = $twitter;
435
-		}
436
-		if (!is_null($twitterScope)) {
437
-			$data[IAccountManager::PROPERTY_TWITTER]['scope'] = $twitterScope;
438
-		}
439
-
440
-		try {
441
-			$data = $this->saveUserSettings($user, $data);
442
-			if ($beforeData[IAccountManager::PROPERTY_PHONE]['value'] !== $data[IAccountManager::PROPERTY_PHONE]['value']) {
443
-				$this->knownUserService->deleteByContactUserId($user->getUID());
444
-			}
445
-			return new DataResponse(
446
-				[
447
-					'status' => 'success',
448
-					'data' => [
449
-						'userId' => $user->getUID(),
450
-						'avatarScope' => $data[IAccountManager::PROPERTY_AVATAR]['scope'],
451
-						'displayname' => $data[IAccountManager::PROPERTY_DISPLAYNAME]['value'],
452
-						'displaynameScope' => $data[IAccountManager::PROPERTY_DISPLAYNAME]['scope'],
453
-						'phone' => $data[IAccountManager::PROPERTY_PHONE]['value'],
454
-						'phoneScope' => $data[IAccountManager::PROPERTY_PHONE]['scope'],
455
-						'email' => $data[IAccountManager::PROPERTY_EMAIL]['value'],
456
-						'emailScope' => $data[IAccountManager::PROPERTY_EMAIL]['scope'],
457
-						'website' => $data[IAccountManager::PROPERTY_WEBSITE]['value'],
458
-						'websiteScope' => $data[IAccountManager::PROPERTY_WEBSITE]['scope'],
459
-						'address' => $data[IAccountManager::PROPERTY_ADDRESS]['value'],
460
-						'addressScope' => $data[IAccountManager::PROPERTY_ADDRESS]['scope'],
461
-						'twitter' => $data[IAccountManager::PROPERTY_TWITTER]['value'],
462
-						'twitterScope' => $data[IAccountManager::PROPERTY_TWITTER]['scope'],
463
-						'message' => $this->l10n->t('Settings saved')
464
-					]
465
-				],
466
-				Http::STATUS_OK
467
-			);
468
-		} catch (ForbiddenException $e) {
469
-			return new DataResponse([
470
-				'status' => 'error',
471
-				'data' => [
472
-					'message' => $e->getMessage()
473
-				],
474
-			]);
475
-		} catch (\InvalidArgumentException $e) {
476
-			return new DataResponse([
477
-				'status' => 'error',
478
-				'data' => [
479
-					'message' => $e->getMessage()
480
-				],
481
-			]);
482
-		}
483
-	}
484
-	/**
485
-	 * update account manager with new user data
486
-	 *
487
-	 * @param IUser $user
488
-	 * @param array $data
489
-	 * @return array
490
-	 * @throws ForbiddenException
491
-	 * @throws \InvalidArgumentException
492
-	 */
493
-	protected function saveUserSettings(IUser $user, array $data): array {
494
-		// keep the user back-end up-to-date with the latest display name and email
495
-		// address
496
-		$oldDisplayName = $user->getDisplayName();
497
-		$oldDisplayName = is_null($oldDisplayName) ? '' : $oldDisplayName;
498
-		if (isset($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'])
499
-			&& $oldDisplayName !== $data[IAccountManager::PROPERTY_DISPLAYNAME]['value']
500
-		) {
501
-			$result = $user->setDisplayName($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']);
502
-			if ($result === false) {
503
-				throw new ForbiddenException($this->l10n->t('Unable to change full name'));
504
-			}
505
-		}
506
-
507
-		$oldEmailAddress = $user->getEMailAddress();
508
-		$oldEmailAddress = is_null($oldEmailAddress) ? '' : strtolower($oldEmailAddress);
509
-		if (isset($data[IAccountManager::PROPERTY_EMAIL]['value'])
510
-			&& $oldEmailAddress !== $data[IAccountManager::PROPERTY_EMAIL]['value']
511
-		) {
512
-			// this is the only permission a backend provides and is also used
513
-			// for the permission of setting a email address
514
-			if (!$user->canChangeDisplayName()) {
515
-				throw new ForbiddenException($this->l10n->t('Unable to change email address'));
516
-			}
517
-			$user->setEMailAddress($data[IAccountManager::PROPERTY_EMAIL]['value']);
518
-		}
519
-
520
-		try {
521
-			return $this->accountManager->updateUser($user, $data, true);
522
-		} catch (\InvalidArgumentException $e) {
523
-			if ($e->getMessage() === IAccountManager::PROPERTY_PHONE) {
524
-				throw new \InvalidArgumentException($this->l10n->t('Unable to set invalid phone number'));
525
-			}
526
-			if ($e->getMessage() === IAccountManager::PROPERTY_WEBSITE) {
527
-				throw new \InvalidArgumentException($this->l10n->t('Unable to set invalid website'));
528
-			}
529
-			throw new \InvalidArgumentException($this->l10n->t('Some account data was invalid'));
530
-		}
531
-	}
532
-
533
-	/**
534
-	 * Set the mail address of a user
535
-	 *
536
-	 * @NoAdminRequired
537
-	 * @NoSubAdminRequired
538
-	 * @PasswordConfirmationRequired
539
-	 *
540
-	 * @param string $account
541
-	 * @param bool $onlyVerificationCode only return verification code without updating the data
542
-	 * @return DataResponse
543
-	 */
544
-	public function getVerificationCode(string $account, bool $onlyVerificationCode): DataResponse {
545
-		$user = $this->userSession->getUser();
546
-
547
-		if ($user === null) {
548
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
549
-		}
550
-
551
-		$accountData = $this->accountManager->getUser($user);
552
-		$cloudId = $user->getCloudId();
553
-		$message = 'Use my Federated Cloud ID to share with me: ' . $cloudId;
554
-		$signature = $this->signMessage($user, $message);
555
-
556
-		$code = $message . ' ' . $signature;
557
-		$codeMd5 = $message . ' ' . md5($signature);
558
-
559
-		switch ($account) {
560
-			case 'verify-twitter':
561
-				$accountData[IAccountManager::PROPERTY_TWITTER]['verified'] = IAccountManager::VERIFICATION_IN_PROGRESS;
562
-				$msg = $this->l10n->t('In order to verify your Twitter account, post the following tweet on Twitter (please make sure to post it without any line breaks):');
563
-				$code = $codeMd5;
564
-				$type = IAccountManager::PROPERTY_TWITTER;
565
-				$accountData[IAccountManager::PROPERTY_TWITTER]['signature'] = $signature;
566
-				break;
567
-			case 'verify-website':
568
-				$accountData[IAccountManager::PROPERTY_WEBSITE]['verified'] = IAccountManager::VERIFICATION_IN_PROGRESS;
569
-				$msg = $this->l10n->t('In order to verify your Website, store the following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):');
570
-				$type = IAccountManager::PROPERTY_WEBSITE;
571
-				$accountData[IAccountManager::PROPERTY_WEBSITE]['signature'] = $signature;
572
-				break;
573
-			default:
574
-				return new DataResponse([], Http::STATUS_BAD_REQUEST);
575
-		}
576
-
577
-		if ($onlyVerificationCode === false) {
578
-			$accountData = $this->accountManager->updateUser($user, $accountData);
579
-			$data = $accountData[$type]['value'];
580
-
581
-			$this->jobList->add(VerifyUserData::class,
582
-				[
583
-					'verificationCode' => $code,
584
-					'data' => $data,
585
-					'type' => $type,
586
-					'uid' => $user->getUID(),
587
-					'try' => 0,
588
-					'lastRun' => $this->getCurrentTime()
589
-				]
590
-			);
591
-		}
592
-
593
-		return new DataResponse(['msg' => $msg, 'code' => $code]);
594
-	}
595
-
596
-	/**
597
-	 * get current timestamp
598
-	 *
599
-	 * @return int
600
-	 */
601
-	protected function getCurrentTime(): int {
602
-		return time();
603
-	}
604
-
605
-	/**
606
-	 * sign message with users private key
607
-	 *
608
-	 * @param IUser $user
609
-	 * @param string $message
610
-	 *
611
-	 * @return string base64 encoded signature
612
-	 */
613
-	protected function signMessage(IUser $user, string $message): string {
614
-		$privateKey = $this->keyManager->getKey($user)->getPrivate();
615
-		openssl_sign(json_encode($message), $signature, $privateKey, OPENSSL_ALGO_SHA512);
616
-		return base64_encode($signature);
617
-	}
73
+    /** @var UserManager */
74
+    private $userManager;
75
+    /** @var GroupManager */
76
+    private $groupManager;
77
+    /** @var IUserSession */
78
+    private $userSession;
79
+    /** @var IConfig */
80
+    private $config;
81
+    /** @var bool */
82
+    private $isAdmin;
83
+    /** @var IL10N */
84
+    private $l10n;
85
+    /** @var IMailer */
86
+    private $mailer;
87
+    /** @var Factory */
88
+    private $l10nFactory;
89
+    /** @var IAppManager */
90
+    private $appManager;
91
+    /** @var AccountManager */
92
+    private $accountManager;
93
+    /** @var Manager */
94
+    private $keyManager;
95
+    /** @var IJobList */
96
+    private $jobList;
97
+    /** @var IManager */
98
+    private $encryptionManager;
99
+    /** @var KnownUserService */
100
+    private $knownUserService;
101
+    /** @var IEventDispatcher */
102
+    private $dispatcher;
103
+
104
+
105
+    public function __construct(
106
+        string $appName,
107
+        IRequest $request,
108
+        IUserManager $userManager,
109
+        IGroupManager $groupManager,
110
+        IUserSession $userSession,
111
+        IConfig $config,
112
+        bool $isAdmin,
113
+        IL10N $l10n,
114
+        IMailer $mailer,
115
+        IFactory $l10nFactory,
116
+        IAppManager $appManager,
117
+        AccountManager $accountManager,
118
+        Manager $keyManager,
119
+        IJobList $jobList,
120
+        IManager $encryptionManager,
121
+        KnownUserService $knownUserService,
122
+        IEventDispatcher $dispatcher
123
+    ) {
124
+        parent::__construct($appName, $request);
125
+        $this->userManager = $userManager;
126
+        $this->groupManager = $groupManager;
127
+        $this->userSession = $userSession;
128
+        $this->config = $config;
129
+        $this->isAdmin = $isAdmin;
130
+        $this->l10n = $l10n;
131
+        $this->mailer = $mailer;
132
+        $this->l10nFactory = $l10nFactory;
133
+        $this->appManager = $appManager;
134
+        $this->accountManager = $accountManager;
135
+        $this->keyManager = $keyManager;
136
+        $this->jobList = $jobList;
137
+        $this->encryptionManager = $encryptionManager;
138
+        $this->knownUserService = $knownUserService;
139
+        $this->dispatcher = $dispatcher;
140
+    }
141
+
142
+
143
+    /**
144
+     * @NoCSRFRequired
145
+     * @NoAdminRequired
146
+     *
147
+     * Display users list template
148
+     *
149
+     * @return TemplateResponse
150
+     */
151
+    public function usersListByGroup(): TemplateResponse {
152
+        return $this->usersList();
153
+    }
154
+
155
+    /**
156
+     * @NoCSRFRequired
157
+     * @NoAdminRequired
158
+     *
159
+     * Display users list template
160
+     *
161
+     * @return TemplateResponse
162
+     */
163
+    public function usersList(): TemplateResponse {
164
+        $user = $this->userSession->getUser();
165
+        $uid = $user->getUID();
166
+
167
+        \OC::$server->getNavigationManager()->setActiveEntry('core_users');
168
+
169
+        /* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */
170
+        $sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT;
171
+        $isLDAPUsed = false;
172
+        if ($this->config->getSystemValue('sort_groups_by_name', false)) {
173
+            $sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
174
+        } else {
175
+            if ($this->appManager->isEnabledForUser('user_ldap')) {
176
+                $isLDAPUsed =
177
+                    $this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy');
178
+                if ($isLDAPUsed) {
179
+                    // LDAP user count can be slow, so we sort by group name here
180
+                    $sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
181
+                }
182
+            }
183
+        }
184
+
185
+        $canChangePassword = $this->canAdminChangeUserPasswords();
186
+
187
+        /* GROUPS */
188
+        $groupsInfo = new \OC\Group\MetaData(
189
+            $uid,
190
+            $this->isAdmin,
191
+            $this->groupManager,
192
+            $this->userSession
193
+        );
194
+
195
+        $groupsInfo->setSorting($sortGroupsBy);
196
+        [$adminGroup, $groups] = $groupsInfo->get();
197
+
198
+        if (!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) {
199
+            $isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) {
200
+                return $ldapFound || $backend instanceof User_Proxy;
201
+            });
202
+        }
203
+
204
+        $disabledUsers = -1;
205
+        $userCount = 0;
206
+
207
+        if (!$isLDAPUsed) {
208
+            if ($this->isAdmin) {
209
+                $disabledUsers = $this->userManager->countDisabledUsers();
210
+                $userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) {
211
+                    return $v + (int)$w;
212
+                }, 0);
213
+            } else {
214
+                // User is subadmin !
215
+                // Map group list to names to retrieve the countDisabledUsersOfGroups
216
+                $userGroups = $this->groupManager->getUserGroups($user);
217
+                $groupsNames = [];
218
+
219
+                foreach ($groups as $key => $group) {
220
+                    // $userCount += (int)$group['usercount'];
221
+                    array_push($groupsNames, $group['name']);
222
+                    // we prevent subadmins from looking up themselves
223
+                    // so we lower the count of the groups he belongs to
224
+                    if (array_key_exists($group['id'], $userGroups)) {
225
+                        $groups[$key]['usercount']--;
226
+                        $userCount -= 1; // we also lower from one the total count
227
+                    }
228
+                }
229
+                $userCount += $this->userManager->countUsersOfGroups($groupsInfo->getGroups());
230
+                $disabledUsers = $this->userManager->countDisabledUsersOfGroups($groupsNames);
231
+            }
232
+
233
+            $userCount -= $disabledUsers;
234
+        }
235
+
236
+        $disabledUsersGroup = [
237
+            'id' => 'disabled',
238
+            'name' => 'Disabled users',
239
+            'usercount' => $disabledUsers
240
+        ];
241
+
242
+        /* QUOTAS PRESETS */
243
+        $quotaPreset = $this->parseQuotaPreset($this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB'));
244
+        $defaultQuota = $this->config->getAppValue('files', 'default_quota', 'none');
245
+
246
+        $event = new BeforeTemplateRenderedEvent();
247
+        $this->dispatcher->dispatch('OC\Settings\Users::loadAdditionalScripts', $event);
248
+        $this->dispatcher->dispatchTyped($event);
249
+
250
+        /* LANGUAGES */
251
+        $languages = $this->l10nFactory->getLanguages();
252
+
253
+        /* FINAL DATA */
254
+        $serverData = [];
255
+        // groups
256
+        $serverData['groups'] = array_merge_recursive($adminGroup, [$disabledUsersGroup], $groups);
257
+        // Various data
258
+        $serverData['isAdmin'] = $this->isAdmin;
259
+        $serverData['sortGroups'] = $sortGroupsBy;
260
+        $serverData['quotaPreset'] = $quotaPreset;
261
+        $serverData['userCount'] = $userCount;
262
+        $serverData['languages'] = $languages;
263
+        $serverData['defaultLanguage'] = $this->config->getSystemValue('default_language', 'en');
264
+        $serverData['forceLanguage'] = $this->config->getSystemValue('force_language', false);
265
+        // Settings
266
+        $serverData['defaultQuota'] = $defaultQuota;
267
+        $serverData['canChangePassword'] = $canChangePassword;
268
+        $serverData['newUserGenerateUserID'] = $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes';
269
+        $serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes';
270
+        $serverData['newUserSendEmail'] = $this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes';
271
+
272
+        return new TemplateResponse('settings', 'settings-vue', ['serverData' => $serverData]);
273
+    }
274
+
275
+    /**
276
+     * @param string $key
277
+     * @param string $value
278
+     *
279
+     * @return JSONResponse
280
+     */
281
+    public function setPreference(string $key, string $value): JSONResponse {
282
+        $allowed = ['newUser.sendEmail'];
283
+        if (!in_array($key, $allowed, true)) {
284
+            return new JSONResponse([], Http::STATUS_FORBIDDEN);
285
+        }
286
+
287
+        $this->config->setAppValue('core', $key, $value);
288
+
289
+        return new JSONResponse([]);
290
+    }
291
+
292
+    /**
293
+     * Parse the app value for quota_present
294
+     *
295
+     * @param string $quotaPreset
296
+     * @return array
297
+     */
298
+    protected function parseQuotaPreset(string $quotaPreset): array {
299
+        // 1 GB, 5 GB, 10 GB => [1 GB, 5 GB, 10 GB]
300
+        $presets = array_filter(array_map('trim', explode(',', $quotaPreset)));
301
+        // Drop default and none, Make array indexes numerically
302
+        return array_values(array_diff($presets, ['default', 'none']));
303
+    }
304
+
305
+    /**
306
+     * check if the admin can change the users password
307
+     *
308
+     * The admin can change the passwords if:
309
+     *
310
+     *   - no encryption module is loaded and encryption is disabled
311
+     *   - encryption module is loaded but it doesn't require per user keys
312
+     *
313
+     * The admin can not change the passwords if:
314
+     *
315
+     *   - an encryption module is loaded and it uses per-user keys
316
+     *   - encryption is enabled but no encryption modules are loaded
317
+     *
318
+     * @return bool
319
+     */
320
+    protected function canAdminChangeUserPasswords(): bool {
321
+        $isEncryptionEnabled = $this->encryptionManager->isEnabled();
322
+        try {
323
+            $noUserSpecificEncryptionKeys = !$this->encryptionManager->getEncryptionModule()->needDetailedAccessList();
324
+            $isEncryptionModuleLoaded = true;
325
+        } catch (ModuleDoesNotExistsException $e) {
326
+            $noUserSpecificEncryptionKeys = true;
327
+            $isEncryptionModuleLoaded = false;
328
+        }
329
+        $canChangePassword = ($isEncryptionModuleLoaded && $noUserSpecificEncryptionKeys)
330
+            || (!$isEncryptionModuleLoaded && !$isEncryptionEnabled);
331
+
332
+        return $canChangePassword;
333
+    }
334
+
335
+    /**
336
+     * @NoAdminRequired
337
+     * @NoSubAdminRequired
338
+     * @PasswordConfirmationRequired
339
+     *
340
+     * @param string|null $avatarScope
341
+     * @param string|null $displayname
342
+     * @param string|null $displaynameScope
343
+     * @param string|null $phone
344
+     * @param string|null $phoneScope
345
+     * @param string|null $email
346
+     * @param string|null $emailScope
347
+     * @param string|null $website
348
+     * @param string|null $websiteScope
349
+     * @param string|null $address
350
+     * @param string|null $addressScope
351
+     * @param string|null $twitter
352
+     * @param string|null $twitterScope
353
+     *
354
+     * @return DataResponse
355
+     */
356
+    public function setUserSettings(?string $avatarScope = null,
357
+                                    ?string $displayname = null,
358
+                                    ?string $displaynameScope = null,
359
+                                    ?string $phone = null,
360
+                                    ?string $phoneScope = null,
361
+                                    ?string $email = null,
362
+                                    ?string $emailScope = null,
363
+                                    ?string $website = null,
364
+                                    ?string $websiteScope = null,
365
+                                    ?string $address = null,
366
+                                    ?string $addressScope = null,
367
+                                    ?string $twitter = null,
368
+                                    ?string $twitterScope = null
369
+    ) {
370
+        $user = $this->userSession->getUser();
371
+        if (!$user instanceof IUser) {
372
+            return new DataResponse(
373
+                [
374
+                    'status' => 'error',
375
+                    'data' => [
376
+                        'message' => $this->l10n->t('Invalid user')
377
+                    ]
378
+                ],
379
+                Http::STATUS_UNAUTHORIZED
380
+            );
381
+        }
382
+
383
+        $email = !is_null($email) ? strtolower($email) : $email;
384
+        if (!empty($email) && !$this->mailer->validateMailAddress($email)) {
385
+            return new DataResponse(
386
+                [
387
+                    'status' => 'error',
388
+                    'data' => [
389
+                        'message' => $this->l10n->t('Invalid mail address')
390
+                    ]
391
+                ],
392
+                Http::STATUS_UNPROCESSABLE_ENTITY
393
+            );
394
+        }
395
+
396
+        $data = $this->accountManager->getUser($user);
397
+        $beforeData = $data;
398
+        if (!is_null($avatarScope)) {
399
+            $data[IAccountManager::PROPERTY_AVATAR]['scope'] = $avatarScope;
400
+        }
401
+        if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
402
+            if (!is_null($displayname)) {
403
+                $data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $displayname;
404
+            }
405
+            if (!is_null($displaynameScope)) {
406
+                $data[IAccountManager::PROPERTY_DISPLAYNAME]['scope'] = $displaynameScope;
407
+            }
408
+            if (!is_null($email)) {
409
+                $data[IAccountManager::PROPERTY_EMAIL]['value'] = $email;
410
+            }
411
+            if (!is_null($emailScope)) {
412
+                $data[IAccountManager::PROPERTY_EMAIL]['scope'] = $emailScope;
413
+            }
414
+        }
415
+        if (!is_null($website)) {
416
+            $data[IAccountManager::PROPERTY_WEBSITE]['value'] = $website;
417
+        }
418
+        if (!is_null($websiteScope)) {
419
+            $data[IAccountManager::PROPERTY_WEBSITE]['scope'] = $websiteScope;
420
+        }
421
+        if (!is_null($address)) {
422
+            $data[IAccountManager::PROPERTY_ADDRESS]['value'] = $address;
423
+        }
424
+        if (!is_null($addressScope)) {
425
+            $data[IAccountManager::PROPERTY_ADDRESS]['scope'] = $addressScope;
426
+        }
427
+        if (!is_null($phone)) {
428
+            $data[IAccountManager::PROPERTY_PHONE]['value'] = $phone;
429
+        }
430
+        if (!is_null($phoneScope)) {
431
+            $data[IAccountManager::PROPERTY_PHONE]['scope'] = $phoneScope;
432
+        }
433
+        if (!is_null($twitter)) {
434
+            $data[IAccountManager::PROPERTY_TWITTER]['value'] = $twitter;
435
+        }
436
+        if (!is_null($twitterScope)) {
437
+            $data[IAccountManager::PROPERTY_TWITTER]['scope'] = $twitterScope;
438
+        }
439
+
440
+        try {
441
+            $data = $this->saveUserSettings($user, $data);
442
+            if ($beforeData[IAccountManager::PROPERTY_PHONE]['value'] !== $data[IAccountManager::PROPERTY_PHONE]['value']) {
443
+                $this->knownUserService->deleteByContactUserId($user->getUID());
444
+            }
445
+            return new DataResponse(
446
+                [
447
+                    'status' => 'success',
448
+                    'data' => [
449
+                        'userId' => $user->getUID(),
450
+                        'avatarScope' => $data[IAccountManager::PROPERTY_AVATAR]['scope'],
451
+                        'displayname' => $data[IAccountManager::PROPERTY_DISPLAYNAME]['value'],
452
+                        'displaynameScope' => $data[IAccountManager::PROPERTY_DISPLAYNAME]['scope'],
453
+                        'phone' => $data[IAccountManager::PROPERTY_PHONE]['value'],
454
+                        'phoneScope' => $data[IAccountManager::PROPERTY_PHONE]['scope'],
455
+                        'email' => $data[IAccountManager::PROPERTY_EMAIL]['value'],
456
+                        'emailScope' => $data[IAccountManager::PROPERTY_EMAIL]['scope'],
457
+                        'website' => $data[IAccountManager::PROPERTY_WEBSITE]['value'],
458
+                        'websiteScope' => $data[IAccountManager::PROPERTY_WEBSITE]['scope'],
459
+                        'address' => $data[IAccountManager::PROPERTY_ADDRESS]['value'],
460
+                        'addressScope' => $data[IAccountManager::PROPERTY_ADDRESS]['scope'],
461
+                        'twitter' => $data[IAccountManager::PROPERTY_TWITTER]['value'],
462
+                        'twitterScope' => $data[IAccountManager::PROPERTY_TWITTER]['scope'],
463
+                        'message' => $this->l10n->t('Settings saved')
464
+                    ]
465
+                ],
466
+                Http::STATUS_OK
467
+            );
468
+        } catch (ForbiddenException $e) {
469
+            return new DataResponse([
470
+                'status' => 'error',
471
+                'data' => [
472
+                    'message' => $e->getMessage()
473
+                ],
474
+            ]);
475
+        } catch (\InvalidArgumentException $e) {
476
+            return new DataResponse([
477
+                'status' => 'error',
478
+                'data' => [
479
+                    'message' => $e->getMessage()
480
+                ],
481
+            ]);
482
+        }
483
+    }
484
+    /**
485
+     * update account manager with new user data
486
+     *
487
+     * @param IUser $user
488
+     * @param array $data
489
+     * @return array
490
+     * @throws ForbiddenException
491
+     * @throws \InvalidArgumentException
492
+     */
493
+    protected function saveUserSettings(IUser $user, array $data): array {
494
+        // keep the user back-end up-to-date with the latest display name and email
495
+        // address
496
+        $oldDisplayName = $user->getDisplayName();
497
+        $oldDisplayName = is_null($oldDisplayName) ? '' : $oldDisplayName;
498
+        if (isset($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'])
499
+            && $oldDisplayName !== $data[IAccountManager::PROPERTY_DISPLAYNAME]['value']
500
+        ) {
501
+            $result = $user->setDisplayName($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']);
502
+            if ($result === false) {
503
+                throw new ForbiddenException($this->l10n->t('Unable to change full name'));
504
+            }
505
+        }
506
+
507
+        $oldEmailAddress = $user->getEMailAddress();
508
+        $oldEmailAddress = is_null($oldEmailAddress) ? '' : strtolower($oldEmailAddress);
509
+        if (isset($data[IAccountManager::PROPERTY_EMAIL]['value'])
510
+            && $oldEmailAddress !== $data[IAccountManager::PROPERTY_EMAIL]['value']
511
+        ) {
512
+            // this is the only permission a backend provides and is also used
513
+            // for the permission of setting a email address
514
+            if (!$user->canChangeDisplayName()) {
515
+                throw new ForbiddenException($this->l10n->t('Unable to change email address'));
516
+            }
517
+            $user->setEMailAddress($data[IAccountManager::PROPERTY_EMAIL]['value']);
518
+        }
519
+
520
+        try {
521
+            return $this->accountManager->updateUser($user, $data, true);
522
+        } catch (\InvalidArgumentException $e) {
523
+            if ($e->getMessage() === IAccountManager::PROPERTY_PHONE) {
524
+                throw new \InvalidArgumentException($this->l10n->t('Unable to set invalid phone number'));
525
+            }
526
+            if ($e->getMessage() === IAccountManager::PROPERTY_WEBSITE) {
527
+                throw new \InvalidArgumentException($this->l10n->t('Unable to set invalid website'));
528
+            }
529
+            throw new \InvalidArgumentException($this->l10n->t('Some account data was invalid'));
530
+        }
531
+    }
532
+
533
+    /**
534
+     * Set the mail address of a user
535
+     *
536
+     * @NoAdminRequired
537
+     * @NoSubAdminRequired
538
+     * @PasswordConfirmationRequired
539
+     *
540
+     * @param string $account
541
+     * @param bool $onlyVerificationCode only return verification code without updating the data
542
+     * @return DataResponse
543
+     */
544
+    public function getVerificationCode(string $account, bool $onlyVerificationCode): DataResponse {
545
+        $user = $this->userSession->getUser();
546
+
547
+        if ($user === null) {
548
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
549
+        }
550
+
551
+        $accountData = $this->accountManager->getUser($user);
552
+        $cloudId = $user->getCloudId();
553
+        $message = 'Use my Federated Cloud ID to share with me: ' . $cloudId;
554
+        $signature = $this->signMessage($user, $message);
555
+
556
+        $code = $message . ' ' . $signature;
557
+        $codeMd5 = $message . ' ' . md5($signature);
558
+
559
+        switch ($account) {
560
+            case 'verify-twitter':
561
+                $accountData[IAccountManager::PROPERTY_TWITTER]['verified'] = IAccountManager::VERIFICATION_IN_PROGRESS;
562
+                $msg = $this->l10n->t('In order to verify your Twitter account, post the following tweet on Twitter (please make sure to post it without any line breaks):');
563
+                $code = $codeMd5;
564
+                $type = IAccountManager::PROPERTY_TWITTER;
565
+                $accountData[IAccountManager::PROPERTY_TWITTER]['signature'] = $signature;
566
+                break;
567
+            case 'verify-website':
568
+                $accountData[IAccountManager::PROPERTY_WEBSITE]['verified'] = IAccountManager::VERIFICATION_IN_PROGRESS;
569
+                $msg = $this->l10n->t('In order to verify your Website, store the following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):');
570
+                $type = IAccountManager::PROPERTY_WEBSITE;
571
+                $accountData[IAccountManager::PROPERTY_WEBSITE]['signature'] = $signature;
572
+                break;
573
+            default:
574
+                return new DataResponse([], Http::STATUS_BAD_REQUEST);
575
+        }
576
+
577
+        if ($onlyVerificationCode === false) {
578
+            $accountData = $this->accountManager->updateUser($user, $accountData);
579
+            $data = $accountData[$type]['value'];
580
+
581
+            $this->jobList->add(VerifyUserData::class,
582
+                [
583
+                    'verificationCode' => $code,
584
+                    'data' => $data,
585
+                    'type' => $type,
586
+                    'uid' => $user->getUID(),
587
+                    'try' => 0,
588
+                    'lastRun' => $this->getCurrentTime()
589
+                ]
590
+            );
591
+        }
592
+
593
+        return new DataResponse(['msg' => $msg, 'code' => $code]);
594
+    }
595
+
596
+    /**
597
+     * get current timestamp
598
+     *
599
+     * @return int
600
+     */
601
+    protected function getCurrentTime(): int {
602
+        return time();
603
+    }
604
+
605
+    /**
606
+     * sign message with users private key
607
+     *
608
+     * @param IUser $user
609
+     * @param string $message
610
+     *
611
+     * @return string base64 encoded signature
612
+     */
613
+    protected function signMessage(IUser $user, string $message): string {
614
+        $privateKey = $this->keyManager->getKey($user)->getPrivate();
615
+        openssl_sign(json_encode($message), $signature, $privateKey, OPENSSL_ALGO_SHA512);
616
+        return base64_encode($signature);
617
+    }
618 618
 }
Please login to merge, or discard this patch.