Passed
Push — master ( fff26a...fde178 )
by John
14:33 queued 17s
created
lib/private/Accounts/AccountManager.php 1 patch
Indentation   +744 added lines, -744 removed lines patch added patch discarded remove patch
@@ -79,748 +79,748 @@
 block discarded – undo
79 79
  * @package OC\Accounts
80 80
  */
81 81
 class AccountManager implements IAccountManager {
82
-	use TAccountsHelper;
83
-
84
-	use TProfileHelper;
85
-
86
-	/** @var  IDBConnection database connection */
87
-	private $connection;
88
-
89
-	/** @var IConfig */
90
-	private $config;
91
-
92
-	/** @var string table name */
93
-	private $table = 'accounts';
94
-
95
-	/** @var string table name */
96
-	private $dataTable = 'accounts_data';
97
-
98
-	/** @var EventDispatcherInterface */
99
-	private $eventDispatcher;
100
-
101
-	/** @var IJobList */
102
-	private $jobList;
103
-
104
-	/** @var LoggerInterface */
105
-	private $logger;
106
-	/** @var IVerificationToken */
107
-	private $verificationToken;
108
-	/** @var IMailer */
109
-	private $mailer;
110
-	/** @var Defaults */
111
-	private $defaults;
112
-	/** @var IL10N */
113
-	private $l10n;
114
-	/** @var IURLGenerator */
115
-	private $urlGenerator;
116
-	/** @var ICrypto */
117
-	private $crypto;
118
-	/** @var IFactory */
119
-	private $l10nfactory;
120
-	private CappedMemoryCache $internalCache;
121
-
122
-	public function __construct(
123
-		IDBConnection            $connection,
124
-		IConfig                  $config,
125
-		EventDispatcherInterface $eventDispatcher,
126
-		IJobList                 $jobList,
127
-		LoggerInterface          $logger,
128
-		IVerificationToken       $verificationToken,
129
-		IMailer                  $mailer,
130
-		Defaults                 $defaults,
131
-		IFactory                 $factory,
132
-		IURLGenerator            $urlGenerator,
133
-		ICrypto                  $crypto
134
-	) {
135
-		$this->connection = $connection;
136
-		$this->config = $config;
137
-		$this->eventDispatcher = $eventDispatcher;
138
-		$this->jobList = $jobList;
139
-		$this->logger = $logger;
140
-		$this->verificationToken = $verificationToken;
141
-		$this->mailer = $mailer;
142
-		$this->defaults = $defaults;
143
-		$this->urlGenerator = $urlGenerator;
144
-		$this->crypto = $crypto;
145
-		// DIing IL10N results in a dependency loop
146
-		$this->l10nfactory = $factory;
147
-		$this->internalCache = new CappedMemoryCache();
148
-	}
149
-
150
-	/**
151
-	 * @param string $input
152
-	 * @return string Provided phone number in E.164 format when it was a valid number
153
-	 * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
154
-	 */
155
-	protected function parsePhoneNumber(string $input): string {
156
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
157
-
158
-		if ($defaultRegion === '') {
159
-			// When no default region is set, only +49… numbers are valid
160
-			if (strpos($input, '+') !== 0) {
161
-				throw new InvalidArgumentException(self::PROPERTY_PHONE);
162
-			}
163
-
164
-			$defaultRegion = 'EN';
165
-		}
166
-
167
-		$phoneUtil = PhoneNumberUtil::getInstance();
168
-		try {
169
-			$phoneNumber = $phoneUtil->parse($input, $defaultRegion);
170
-			if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) {
171
-				return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
172
-			}
173
-		} catch (NumberParseException $e) {
174
-		}
175
-
176
-		throw new InvalidArgumentException(self::PROPERTY_PHONE);
177
-	}
178
-
179
-	/**
180
-	 *
181
-	 * @param string $input
182
-	 * @return string
183
-	 * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
184
-	 */
185
-	protected function parseWebsite(string $input): string {
186
-		$parts = parse_url($input);
187
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
188
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
189
-		}
190
-
191
-		if (!isset($parts['host']) || $parts['host'] === '') {
192
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
193
-		}
194
-
195
-		return $input;
196
-	}
197
-
198
-	/**
199
-	 * @param IAccountProperty[] $properties
200
-	 */
201
-	protected function testValueLengths(array $properties, bool $throwOnData = false): void {
202
-		foreach ($properties as $property) {
203
-			if (strlen($property->getValue()) > 2048) {
204
-				if ($throwOnData) {
205
-					throw new InvalidArgumentException();
206
-				} else {
207
-					$property->setValue('');
208
-				}
209
-			}
210
-		}
211
-	}
212
-
213
-	protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
214
-		if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
215
-			throw new InvalidArgumentException('scope');
216
-		}
217
-
218
-		if (
219
-			$property->getScope() === self::SCOPE_PRIVATE
220
-			&& in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
221
-		) {
222
-			if ($throwOnData) {
223
-				// v2-private is not available for these fields
224
-				throw new InvalidArgumentException('scope');
225
-			} else {
226
-				// default to local
227
-				$property->setScope(self::SCOPE_LOCAL);
228
-			}
229
-		} else {
230
-			// migrate scope values to the new format
231
-			// invalid scopes are mapped to a default value
232
-			$property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
233
-		}
234
-	}
235
-
236
-	protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) {
237
-		if ($property->getName() !== self::PROPERTY_PHONE) {
238
-			if ($throwOnData) {
239
-				throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName()));
240
-			}
241
-			return;
242
-		}
243
-		if ($property->getValue() === '') {
244
-			return;
245
-		}
246
-		try {
247
-			$property->setValue($this->parsePhoneNumber($property->getValue()));
248
-		} catch (InvalidArgumentException $e) {
249
-			if ($throwOnData) {
250
-				throw $e;
251
-			}
252
-			$property->setValue('');
253
-		}
254
-	}
255
-
256
-	protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) {
257
-		if ($property->getName() !== self::PROPERTY_WEBSITE) {
258
-			if ($throwOnData) {
259
-				throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName()));
260
-			}
261
-		}
262
-		try {
263
-			$property->setValue($this->parseWebsite($property->getValue()));
264
-		} catch (InvalidArgumentException $e) {
265
-			if ($throwOnData) {
266
-				throw $e;
267
-			}
268
-			$property->setValue('');
269
-		}
270
-	}
271
-
272
-	protected function updateUser(IUser $user, array $data, bool $throwOnData = false): array {
273
-		$oldUserData = $this->getUser($user, false);
274
-		$updated = true;
275
-
276
-		if ($oldUserData !== $data) {
277
-			$this->updateExistingUser($user, $data);
278
-		} else {
279
-			// nothing needs to be done if new and old data set are the same
280
-			$updated = false;
281
-		}
282
-
283
-		if ($updated) {
284
-			$this->eventDispatcher->dispatch(
285
-				'OC\AccountManager::userUpdated',
286
-				new GenericEvent($user, $data)
287
-			);
288
-		}
289
-
290
-		return $data;
291
-	}
292
-
293
-	/**
294
-	 * delete user from accounts table
295
-	 *
296
-	 * @param IUser $user
297
-	 */
298
-	public function deleteUser(IUser $user) {
299
-		$uid = $user->getUID();
300
-		$query = $this->connection->getQueryBuilder();
301
-		$query->delete($this->table)
302
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
303
-			->execute();
304
-
305
-		$this->deleteUserData($user);
306
-	}
307
-
308
-	/**
309
-	 * delete user from accounts table
310
-	 *
311
-	 * @param IUser $user
312
-	 */
313
-	public function deleteUserData(IUser $user): void {
314
-		$uid = $user->getUID();
315
-		$query = $this->connection->getQueryBuilder();
316
-		$query->delete($this->dataTable)
317
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
318
-			->execute();
319
-	}
320
-
321
-	/**
322
-	 * get stored data from a given user
323
-	 */
324
-	protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
325
-		$uid = $user->getUID();
326
-		$query = $this->connection->getQueryBuilder();
327
-		$query->select('data')
328
-			->from($this->table)
329
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
330
-			->setParameter('uid', $uid);
331
-		$result = $query->executeQuery();
332
-		$accountData = $result->fetchAll();
333
-		$result->closeCursor();
334
-
335
-		if (empty($accountData)) {
336
-			$userData = $this->buildDefaultUserRecord($user);
337
-			if ($insertIfNotExists) {
338
-				$this->insertNewUser($user, $userData);
339
-			}
340
-			return $userData;
341
-		}
342
-
343
-		$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
344
-		if ($userDataArray === null || $userDataArray === []) {
345
-			return $this->buildDefaultUserRecord($user);
346
-		}
347
-
348
-		return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
349
-	}
350
-
351
-	public function searchUsers(string $property, array $values): array {
352
-		// the value col is limited to 255 bytes. It is used for searches only.
353
-		$values = array_map(function (string $value) {
354
-			return Util::shortenMultibyteString($value, 255);
355
-		}, $values);
356
-		$chunks = array_chunk($values, 500);
357
-		$query = $this->connection->getQueryBuilder();
358
-		$query->select('*')
359
-			->from($this->dataTable)
360
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
361
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
362
-
363
-		$matches = [];
364
-		foreach ($chunks as $chunk) {
365
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
366
-			$result = $query->executeQuery();
367
-
368
-			while ($row = $result->fetch()) {
369
-				$matches[$row['uid']] = $row['value'];
370
-			}
371
-			$result->closeCursor();
372
-		}
373
-
374
-		$result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
375
-
376
-		return array_flip($result);
377
-	}
378
-
379
-	protected function searchUsersForRelatedCollection(string $property, array $values): array {
380
-		switch ($property) {
381
-			case IAccountManager::PROPERTY_EMAIL:
382
-				return array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values));
383
-			default:
384
-				return [];
385
-		}
386
-	}
387
-
388
-	/**
389
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
390
-	 */
391
-	protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
392
-		try {
393
-			$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
394
-		} catch (PropertyDoesNotExistException $e) {
395
-			return;
396
-		}
397
-
398
-		$oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
399
-		$oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
400
-
401
-		if ($oldMail !== $property->getValue()) {
402
-			$this->jobList->add(
403
-				VerifyUserData::class,
404
-				[
405
-					'verificationCode' => '',
406
-					'data' => $property->getValue(),
407
-					'type' => self::PROPERTY_EMAIL,
408
-					'uid' => $updatedAccount->getUser()->getUID(),
409
-					'try' => 0,
410
-					'lastRun' => time()
411
-				]
412
-			);
413
-
414
-			$property->setVerified(self::VERIFICATION_IN_PROGRESS);
415
-		}
416
-	}
417
-
418
-	protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
419
-		$mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
420
-		foreach ($mailCollection->getProperties() as $property) {
421
-			if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
422
-				continue;
423
-			}
424
-			if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
425
-				$property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
426
-			}
427
-		}
428
-	}
429
-
430
-	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
431
-		$ref = \substr(hash('sha256', $email), 0, 8);
432
-		$key = $this->crypto->encrypt($email);
433
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
434
-
435
-		$link = $this->urlGenerator->linkToRouteAbsolute(
436
-			'provisioning_api.Verification.verifyMail',
437
-			[
438
-				'userId' => $user->getUID(),
439
-				'token' => $token,
440
-				'key' => $key
441
-			]
442
-		);
443
-
444
-		$emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
445
-			'link' => $link,
446
-		]);
447
-
448
-		if (!$this->l10n) {
449
-			$this->l10n = $this->l10nfactory->get('core');
450
-		}
451
-
452
-		$emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
453
-		$emailTemplate->addHeader();
454
-		$emailTemplate->addHeading($this->l10n->t('Email verification'));
455
-
456
-		$emailTemplate->addBodyText(
457
-			htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
458
-			$this->l10n->t('Click the following link to confirm your email.')
459
-		);
460
-
461
-		$emailTemplate->addBodyButton(
462
-			htmlspecialchars($this->l10n->t('Confirm your email')),
463
-			$link,
464
-			false
465
-		);
466
-		$emailTemplate->addFooter();
467
-
468
-		try {
469
-			$message = $this->mailer->createMessage();
470
-			$message->setTo([$email => $user->getDisplayName()]);
471
-			$message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
472
-			$message->useTemplate($emailTemplate);
473
-			$this->mailer->send($message);
474
-		} catch (Exception $e) {
475
-			// Log the exception and continue
476
-			$this->logger->info('Failed to send verification mail', [
477
-				'app' => 'core',
478
-				'exception' => $e
479
-			]);
480
-			return false;
481
-		}
482
-		return true;
483
-	}
484
-
485
-	/**
486
-	 * Make sure that all expected data are set
487
-	 */
488
-	protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
489
-		foreach ($defaultUserData as $defaultDataItem) {
490
-			// If property does not exist, initialize it
491
-			$userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
492
-			if ($userDataIndex === false) {
493
-				$userData[] = $defaultDataItem;
494
-				continue;
495
-			}
496
-
497
-			// Merge and extend default missing values
498
-			$userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
499
-		}
500
-
501
-		return $userData;
502
-	}
503
-
504
-	protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
505
-		static $propertiesVerifiableByLookupServer = [
506
-			self::PROPERTY_TWITTER,
507
-			self::PROPERTY_WEBSITE,
508
-			self::PROPERTY_EMAIL,
509
-		];
510
-
511
-		foreach ($propertiesVerifiableByLookupServer as $propertyName) {
512
-			try {
513
-				$property = $updatedAccount->getProperty($propertyName);
514
-			} catch (PropertyDoesNotExistException $e) {
515
-				continue;
516
-			}
517
-			$wasVerified = isset($oldData[$propertyName])
518
-				&& isset($oldData[$propertyName]['verified'])
519
-				&& $oldData[$propertyName]['verified'] === self::VERIFIED;
520
-			if ((!isset($oldData[$propertyName])
521
-					|| !isset($oldData[$propertyName]['value'])
522
-					|| $property->getValue() !== $oldData[$propertyName]['value'])
523
-				&& ($property->getVerified() !== self::NOT_VERIFIED
524
-					|| $wasVerified)
525
-			) {
526
-				$property->setVerified(self::NOT_VERIFIED);
527
-			}
528
-		}
529
-	}
530
-
531
-	/**
532
-	 * add new user to accounts table
533
-	 *
534
-	 * @param IUser $user
535
-	 * @param array $data
536
-	 */
537
-	protected function insertNewUser(IUser $user, array $data): void {
538
-		$uid = $user->getUID();
539
-		$jsonEncodedData = $this->prepareJson($data);
540
-		$query = $this->connection->getQueryBuilder();
541
-		$query->insert($this->table)
542
-			->values(
543
-				[
544
-					'uid' => $query->createNamedParameter($uid),
545
-					'data' => $query->createNamedParameter($jsonEncodedData),
546
-				]
547
-			)
548
-			->executeStatement();
549
-
550
-		$this->deleteUserData($user);
551
-		$this->writeUserData($user, $data);
552
-	}
553
-
554
-	protected function prepareJson(array $data): string {
555
-		$preparedData = [];
556
-		foreach ($data as $dataRow) {
557
-			$propertyName = $dataRow['name'];
558
-			unset($dataRow['name']);
559
-
560
-			if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
561
-				// do not write default value, save DB space
562
-				unset($dataRow['locallyVerified']);
563
-			}
564
-
565
-			if (!$this->isCollection($propertyName)) {
566
-				$preparedData[$propertyName] = $dataRow;
567
-				continue;
568
-			}
569
-			if (!isset($preparedData[$propertyName])) {
570
-				$preparedData[$propertyName] = [];
571
-			}
572
-			$preparedData[$propertyName][] = $dataRow;
573
-		}
574
-		return json_encode($preparedData);
575
-	}
576
-
577
-	protected function importFromJson(string $json, string $userId): ?array {
578
-		$result = [];
579
-		$jsonArray = json_decode($json, true);
580
-		$jsonError = json_last_error();
581
-		if ($jsonError !== JSON_ERROR_NONE) {
582
-			$this->logger->critical(
583
-				'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
584
-				[
585
-					'uid' => $userId,
586
-					'json_error' => $jsonError
587
-				]
588
-			);
589
-			return null;
590
-		}
591
-		foreach ($jsonArray as $propertyName => $row) {
592
-			if (!$this->isCollection($propertyName)) {
593
-				$result[] = array_merge($row, ['name' => $propertyName]);
594
-				continue;
595
-			}
596
-			foreach ($row as $singleRow) {
597
-				$result[] = array_merge($singleRow, ['name' => $propertyName]);
598
-			}
599
-		}
600
-		return $result;
601
-	}
602
-
603
-	/**
604
-	 * update existing user in accounts table
605
-	 *
606
-	 * @param IUser $user
607
-	 * @param array $data
608
-	 */
609
-	protected function updateExistingUser(IUser $user, array $data): void {
610
-		$uid = $user->getUID();
611
-		$jsonEncodedData = $this->prepareJson($data);
612
-		$query = $this->connection->getQueryBuilder();
613
-		$query->update($this->table)
614
-			->set('data', $query->createNamedParameter($jsonEncodedData))
615
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
616
-			->executeStatement();
617
-
618
-		$this->deleteUserData($user);
619
-		$this->writeUserData($user, $data);
620
-	}
621
-
622
-	protected function writeUserData(IUser $user, array $data): void {
623
-		$query = $this->connection->getQueryBuilder();
624
-		$query->insert($this->dataTable)
625
-			->values(
626
-				[
627
-					'uid' => $query->createNamedParameter($user->getUID()),
628
-					'name' => $query->createParameter('name'),
629
-					'value' => $query->createParameter('value'),
630
-				]
631
-			);
632
-		$this->writeUserDataProperties($query, $data);
633
-	}
634
-
635
-	protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
636
-		foreach ($data as $property) {
637
-			if ($property['name'] === self::PROPERTY_AVATAR) {
638
-				continue;
639
-			}
640
-
641
-			// the value col is limited to 255 bytes. It is used for searches only.
642
-			$value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
643
-
644
-			$query->setParameter('name', $property['name'])
645
-				->setParameter('value', $value);
646
-			$query->executeStatement();
647
-		}
648
-	}
649
-
650
-	/**
651
-	 * build default user record in case not data set exists yet
652
-	 *
653
-	 * @param IUser $user
654
-	 * @return array
655
-	 */
656
-	protected function buildDefaultUserRecord(IUser $user) {
657
-		return [
658
-			[
659
-				'name' => self::PROPERTY_DISPLAYNAME,
660
-				'value' => $user->getDisplayName(),
661
-				'scope' => self::SCOPE_FEDERATED,
662
-				'verified' => self::NOT_VERIFIED,
663
-			],
664
-
665
-			[
666
-				'name' => self::PROPERTY_ADDRESS,
667
-				'value' => '',
668
-				'scope' => self::SCOPE_LOCAL,
669
-				'verified' => self::NOT_VERIFIED,
670
-			],
671
-
672
-			[
673
-				'name' => self::PROPERTY_WEBSITE,
674
-				'value' => '',
675
-				'scope' => self::SCOPE_LOCAL,
676
-				'verified' => self::NOT_VERIFIED,
677
-			],
678
-
679
-			[
680
-				'name' => self::PROPERTY_EMAIL,
681
-				'value' => $user->getEMailAddress(),
682
-				'scope' => self::SCOPE_FEDERATED,
683
-				'verified' => self::NOT_VERIFIED,
684
-			],
685
-
686
-			[
687
-				'name' => self::PROPERTY_AVATAR,
688
-				'scope' => self::SCOPE_FEDERATED
689
-			],
690
-
691
-			[
692
-				'name' => self::PROPERTY_PHONE,
693
-				'value' => '',
694
-				'scope' => self::SCOPE_LOCAL,
695
-				'verified' => self::NOT_VERIFIED,
696
-			],
697
-
698
-			[
699
-				'name' => self::PROPERTY_TWITTER,
700
-				'value' => '',
701
-				'scope' => self::SCOPE_LOCAL,
702
-				'verified' => self::NOT_VERIFIED,
703
-			],
704
-
705
-			[
706
-				'name' => self::PROPERTY_ORGANISATION,
707
-				'value' => '',
708
-				'scope' => self::SCOPE_LOCAL,
709
-			],
710
-
711
-			[
712
-				'name' => self::PROPERTY_ROLE,
713
-				'value' => '',
714
-				'scope' => self::SCOPE_LOCAL,
715
-			],
716
-
717
-			[
718
-				'name' => self::PROPERTY_HEADLINE,
719
-				'value' => '',
720
-				'scope' => self::SCOPE_LOCAL,
721
-			],
722
-
723
-			[
724
-				'name' => self::PROPERTY_BIOGRAPHY,
725
-				'value' => '',
726
-				'scope' => self::SCOPE_LOCAL,
727
-			],
728
-
729
-			[
730
-				'name' => self::PROPERTY_PROFILE_ENABLED,
731
-				'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
732
-			],
733
-		];
734
-	}
735
-
736
-	private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
737
-		$collection = $account->getPropertyCollection($data['name']);
738
-
739
-		$p = new AccountProperty(
740
-			$data['name'],
741
-			$data['value'] ?? '',
742
-			$data['scope'] ?? self::SCOPE_LOCAL,
743
-			$data['verified'] ?? self::NOT_VERIFIED,
744
-			''
745
-		);
746
-		$p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
747
-		$collection->addProperty($p);
748
-
749
-		return $collection;
750
-	}
751
-
752
-	private function parseAccountData(IUser $user, $data): Account {
753
-		$account = new Account($user);
754
-		foreach ($data as $accountData) {
755
-			if ($this->isCollection($accountData['name'])) {
756
-				$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
757
-			} else {
758
-				$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
759
-				if (isset($accountData['locallyVerified'])) {
760
-					$property = $account->getProperty($accountData['name']);
761
-					$property->setLocallyVerified($accountData['locallyVerified']);
762
-				}
763
-			}
764
-		}
765
-		return $account;
766
-	}
767
-
768
-	public function getAccount(IUser $user): IAccount {
769
-		if ($this->internalCache->hasKey($user->getUID())) {
770
-			return $this->internalCache->get($user->getUID());
771
-		}
772
-		$account = $this->parseAccountData($user, $this->getUser($user));
773
-		$this->internalCache->set($user->getUID(), $account);
774
-		return $account;
775
-	}
776
-
777
-	public function updateAccount(IAccount $account): void {
778
-		$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
779
-		try {
780
-			$property = $account->getProperty(self::PROPERTY_PHONE);
781
-			$this->sanitizePhoneNumberValue($property);
782
-		} catch (PropertyDoesNotExistException $e) {
783
-			//  valid case, nothing to do
784
-		}
785
-
786
-		try {
787
-			$property = $account->getProperty(self::PROPERTY_WEBSITE);
788
-			$this->sanitizeWebsite($property);
789
-		} catch (PropertyDoesNotExistException $e) {
790
-			//  valid case, nothing to do
791
-		}
792
-
793
-		static $allowedScopes = [
794
-			self::SCOPE_PRIVATE,
795
-			self::SCOPE_LOCAL,
796
-			self::SCOPE_FEDERATED,
797
-			self::SCOPE_PUBLISHED,
798
-			self::VISIBILITY_PRIVATE,
799
-			self::VISIBILITY_CONTACTS_ONLY,
800
-			self::VISIBILITY_PUBLIC,
801
-		];
802
-		foreach ($account->getAllProperties() as $property) {
803
-			$this->testPropertyScope($property, $allowedScopes, true);
804
-		}
805
-
806
-		$oldData = $this->getUser($account->getUser(), false);
807
-		$this->updateVerificationStatus($account, $oldData);
808
-		$this->checkEmailVerification($account, $oldData);
809
-		$this->checkLocalEmailVerification($account, $oldData);
810
-
811
-		$data = [];
812
-		foreach ($account->getAllProperties() as $property) {
813
-			/** @var IAccountProperty $property */
814
-			$data[] = [
815
-				'name' => $property->getName(),
816
-				'value' => $property->getValue(),
817
-				'scope' => $property->getScope(),
818
-				'verified' => $property->getVerified(),
819
-				'locallyVerified' => $property->getLocallyVerified(),
820
-			];
821
-		}
822
-
823
-		$this->updateUser($account->getUser(), $data, true);
824
-		$this->internalCache->set($account->getUser()->getUID(), $account);
825
-	}
82
+    use TAccountsHelper;
83
+
84
+    use TProfileHelper;
85
+
86
+    /** @var  IDBConnection database connection */
87
+    private $connection;
88
+
89
+    /** @var IConfig */
90
+    private $config;
91
+
92
+    /** @var string table name */
93
+    private $table = 'accounts';
94
+
95
+    /** @var string table name */
96
+    private $dataTable = 'accounts_data';
97
+
98
+    /** @var EventDispatcherInterface */
99
+    private $eventDispatcher;
100
+
101
+    /** @var IJobList */
102
+    private $jobList;
103
+
104
+    /** @var LoggerInterface */
105
+    private $logger;
106
+    /** @var IVerificationToken */
107
+    private $verificationToken;
108
+    /** @var IMailer */
109
+    private $mailer;
110
+    /** @var Defaults */
111
+    private $defaults;
112
+    /** @var IL10N */
113
+    private $l10n;
114
+    /** @var IURLGenerator */
115
+    private $urlGenerator;
116
+    /** @var ICrypto */
117
+    private $crypto;
118
+    /** @var IFactory */
119
+    private $l10nfactory;
120
+    private CappedMemoryCache $internalCache;
121
+
122
+    public function __construct(
123
+        IDBConnection            $connection,
124
+        IConfig                  $config,
125
+        EventDispatcherInterface $eventDispatcher,
126
+        IJobList                 $jobList,
127
+        LoggerInterface          $logger,
128
+        IVerificationToken       $verificationToken,
129
+        IMailer                  $mailer,
130
+        Defaults                 $defaults,
131
+        IFactory                 $factory,
132
+        IURLGenerator            $urlGenerator,
133
+        ICrypto                  $crypto
134
+    ) {
135
+        $this->connection = $connection;
136
+        $this->config = $config;
137
+        $this->eventDispatcher = $eventDispatcher;
138
+        $this->jobList = $jobList;
139
+        $this->logger = $logger;
140
+        $this->verificationToken = $verificationToken;
141
+        $this->mailer = $mailer;
142
+        $this->defaults = $defaults;
143
+        $this->urlGenerator = $urlGenerator;
144
+        $this->crypto = $crypto;
145
+        // DIing IL10N results in a dependency loop
146
+        $this->l10nfactory = $factory;
147
+        $this->internalCache = new CappedMemoryCache();
148
+    }
149
+
150
+    /**
151
+     * @param string $input
152
+     * @return string Provided phone number in E.164 format when it was a valid number
153
+     * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
154
+     */
155
+    protected function parsePhoneNumber(string $input): string {
156
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
157
+
158
+        if ($defaultRegion === '') {
159
+            // When no default region is set, only +49… numbers are valid
160
+            if (strpos($input, '+') !== 0) {
161
+                throw new InvalidArgumentException(self::PROPERTY_PHONE);
162
+            }
163
+
164
+            $defaultRegion = 'EN';
165
+        }
166
+
167
+        $phoneUtil = PhoneNumberUtil::getInstance();
168
+        try {
169
+            $phoneNumber = $phoneUtil->parse($input, $defaultRegion);
170
+            if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) {
171
+                return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
172
+            }
173
+        } catch (NumberParseException $e) {
174
+        }
175
+
176
+        throw new InvalidArgumentException(self::PROPERTY_PHONE);
177
+    }
178
+
179
+    /**
180
+     *
181
+     * @param string $input
182
+     * @return string
183
+     * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
184
+     */
185
+    protected function parseWebsite(string $input): string {
186
+        $parts = parse_url($input);
187
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
188
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
189
+        }
190
+
191
+        if (!isset($parts['host']) || $parts['host'] === '') {
192
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
193
+        }
194
+
195
+        return $input;
196
+    }
197
+
198
+    /**
199
+     * @param IAccountProperty[] $properties
200
+     */
201
+    protected function testValueLengths(array $properties, bool $throwOnData = false): void {
202
+        foreach ($properties as $property) {
203
+            if (strlen($property->getValue()) > 2048) {
204
+                if ($throwOnData) {
205
+                    throw new InvalidArgumentException();
206
+                } else {
207
+                    $property->setValue('');
208
+                }
209
+            }
210
+        }
211
+    }
212
+
213
+    protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
214
+        if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
215
+            throw new InvalidArgumentException('scope');
216
+        }
217
+
218
+        if (
219
+            $property->getScope() === self::SCOPE_PRIVATE
220
+            && in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
221
+        ) {
222
+            if ($throwOnData) {
223
+                // v2-private is not available for these fields
224
+                throw new InvalidArgumentException('scope');
225
+            } else {
226
+                // default to local
227
+                $property->setScope(self::SCOPE_LOCAL);
228
+            }
229
+        } else {
230
+            // migrate scope values to the new format
231
+            // invalid scopes are mapped to a default value
232
+            $property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
233
+        }
234
+    }
235
+
236
+    protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) {
237
+        if ($property->getName() !== self::PROPERTY_PHONE) {
238
+            if ($throwOnData) {
239
+                throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName()));
240
+            }
241
+            return;
242
+        }
243
+        if ($property->getValue() === '') {
244
+            return;
245
+        }
246
+        try {
247
+            $property->setValue($this->parsePhoneNumber($property->getValue()));
248
+        } catch (InvalidArgumentException $e) {
249
+            if ($throwOnData) {
250
+                throw $e;
251
+            }
252
+            $property->setValue('');
253
+        }
254
+    }
255
+
256
+    protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) {
257
+        if ($property->getName() !== self::PROPERTY_WEBSITE) {
258
+            if ($throwOnData) {
259
+                throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName()));
260
+            }
261
+        }
262
+        try {
263
+            $property->setValue($this->parseWebsite($property->getValue()));
264
+        } catch (InvalidArgumentException $e) {
265
+            if ($throwOnData) {
266
+                throw $e;
267
+            }
268
+            $property->setValue('');
269
+        }
270
+    }
271
+
272
+    protected function updateUser(IUser $user, array $data, bool $throwOnData = false): array {
273
+        $oldUserData = $this->getUser($user, false);
274
+        $updated = true;
275
+
276
+        if ($oldUserData !== $data) {
277
+            $this->updateExistingUser($user, $data);
278
+        } else {
279
+            // nothing needs to be done if new and old data set are the same
280
+            $updated = false;
281
+        }
282
+
283
+        if ($updated) {
284
+            $this->eventDispatcher->dispatch(
285
+                'OC\AccountManager::userUpdated',
286
+                new GenericEvent($user, $data)
287
+            );
288
+        }
289
+
290
+        return $data;
291
+    }
292
+
293
+    /**
294
+     * delete user from accounts table
295
+     *
296
+     * @param IUser $user
297
+     */
298
+    public function deleteUser(IUser $user) {
299
+        $uid = $user->getUID();
300
+        $query = $this->connection->getQueryBuilder();
301
+        $query->delete($this->table)
302
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
303
+            ->execute();
304
+
305
+        $this->deleteUserData($user);
306
+    }
307
+
308
+    /**
309
+     * delete user from accounts table
310
+     *
311
+     * @param IUser $user
312
+     */
313
+    public function deleteUserData(IUser $user): void {
314
+        $uid = $user->getUID();
315
+        $query = $this->connection->getQueryBuilder();
316
+        $query->delete($this->dataTable)
317
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
318
+            ->execute();
319
+    }
320
+
321
+    /**
322
+     * get stored data from a given user
323
+     */
324
+    protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
325
+        $uid = $user->getUID();
326
+        $query = $this->connection->getQueryBuilder();
327
+        $query->select('data')
328
+            ->from($this->table)
329
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
330
+            ->setParameter('uid', $uid);
331
+        $result = $query->executeQuery();
332
+        $accountData = $result->fetchAll();
333
+        $result->closeCursor();
334
+
335
+        if (empty($accountData)) {
336
+            $userData = $this->buildDefaultUserRecord($user);
337
+            if ($insertIfNotExists) {
338
+                $this->insertNewUser($user, $userData);
339
+            }
340
+            return $userData;
341
+        }
342
+
343
+        $userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
344
+        if ($userDataArray === null || $userDataArray === []) {
345
+            return $this->buildDefaultUserRecord($user);
346
+        }
347
+
348
+        return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
349
+    }
350
+
351
+    public function searchUsers(string $property, array $values): array {
352
+        // the value col is limited to 255 bytes. It is used for searches only.
353
+        $values = array_map(function (string $value) {
354
+            return Util::shortenMultibyteString($value, 255);
355
+        }, $values);
356
+        $chunks = array_chunk($values, 500);
357
+        $query = $this->connection->getQueryBuilder();
358
+        $query->select('*')
359
+            ->from($this->dataTable)
360
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
361
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
362
+
363
+        $matches = [];
364
+        foreach ($chunks as $chunk) {
365
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
366
+            $result = $query->executeQuery();
367
+
368
+            while ($row = $result->fetch()) {
369
+                $matches[$row['uid']] = $row['value'];
370
+            }
371
+            $result->closeCursor();
372
+        }
373
+
374
+        $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
375
+
376
+        return array_flip($result);
377
+    }
378
+
379
+    protected function searchUsersForRelatedCollection(string $property, array $values): array {
380
+        switch ($property) {
381
+            case IAccountManager::PROPERTY_EMAIL:
382
+                return array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values));
383
+            default:
384
+                return [];
385
+        }
386
+    }
387
+
388
+    /**
389
+     * check if we need to ask the server for email verification, if yes we create a cronjob
390
+     */
391
+    protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
392
+        try {
393
+            $property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
394
+        } catch (PropertyDoesNotExistException $e) {
395
+            return;
396
+        }
397
+
398
+        $oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
399
+        $oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
400
+
401
+        if ($oldMail !== $property->getValue()) {
402
+            $this->jobList->add(
403
+                VerifyUserData::class,
404
+                [
405
+                    'verificationCode' => '',
406
+                    'data' => $property->getValue(),
407
+                    'type' => self::PROPERTY_EMAIL,
408
+                    'uid' => $updatedAccount->getUser()->getUID(),
409
+                    'try' => 0,
410
+                    'lastRun' => time()
411
+                ]
412
+            );
413
+
414
+            $property->setVerified(self::VERIFICATION_IN_PROGRESS);
415
+        }
416
+    }
417
+
418
+    protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
419
+        $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
420
+        foreach ($mailCollection->getProperties() as $property) {
421
+            if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
422
+                continue;
423
+            }
424
+            if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
425
+                $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
426
+            }
427
+        }
428
+    }
429
+
430
+    protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
431
+        $ref = \substr(hash('sha256', $email), 0, 8);
432
+        $key = $this->crypto->encrypt($email);
433
+        $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
434
+
435
+        $link = $this->urlGenerator->linkToRouteAbsolute(
436
+            'provisioning_api.Verification.verifyMail',
437
+            [
438
+                'userId' => $user->getUID(),
439
+                'token' => $token,
440
+                'key' => $key
441
+            ]
442
+        );
443
+
444
+        $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
445
+            'link' => $link,
446
+        ]);
447
+
448
+        if (!$this->l10n) {
449
+            $this->l10n = $this->l10nfactory->get('core');
450
+        }
451
+
452
+        $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
453
+        $emailTemplate->addHeader();
454
+        $emailTemplate->addHeading($this->l10n->t('Email verification'));
455
+
456
+        $emailTemplate->addBodyText(
457
+            htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
458
+            $this->l10n->t('Click the following link to confirm your email.')
459
+        );
460
+
461
+        $emailTemplate->addBodyButton(
462
+            htmlspecialchars($this->l10n->t('Confirm your email')),
463
+            $link,
464
+            false
465
+        );
466
+        $emailTemplate->addFooter();
467
+
468
+        try {
469
+            $message = $this->mailer->createMessage();
470
+            $message->setTo([$email => $user->getDisplayName()]);
471
+            $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
472
+            $message->useTemplate($emailTemplate);
473
+            $this->mailer->send($message);
474
+        } catch (Exception $e) {
475
+            // Log the exception and continue
476
+            $this->logger->info('Failed to send verification mail', [
477
+                'app' => 'core',
478
+                'exception' => $e
479
+            ]);
480
+            return false;
481
+        }
482
+        return true;
483
+    }
484
+
485
+    /**
486
+     * Make sure that all expected data are set
487
+     */
488
+    protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
489
+        foreach ($defaultUserData as $defaultDataItem) {
490
+            // If property does not exist, initialize it
491
+            $userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
492
+            if ($userDataIndex === false) {
493
+                $userData[] = $defaultDataItem;
494
+                continue;
495
+            }
496
+
497
+            // Merge and extend default missing values
498
+            $userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
499
+        }
500
+
501
+        return $userData;
502
+    }
503
+
504
+    protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
505
+        static $propertiesVerifiableByLookupServer = [
506
+            self::PROPERTY_TWITTER,
507
+            self::PROPERTY_WEBSITE,
508
+            self::PROPERTY_EMAIL,
509
+        ];
510
+
511
+        foreach ($propertiesVerifiableByLookupServer as $propertyName) {
512
+            try {
513
+                $property = $updatedAccount->getProperty($propertyName);
514
+            } catch (PropertyDoesNotExistException $e) {
515
+                continue;
516
+            }
517
+            $wasVerified = isset($oldData[$propertyName])
518
+                && isset($oldData[$propertyName]['verified'])
519
+                && $oldData[$propertyName]['verified'] === self::VERIFIED;
520
+            if ((!isset($oldData[$propertyName])
521
+                    || !isset($oldData[$propertyName]['value'])
522
+                    || $property->getValue() !== $oldData[$propertyName]['value'])
523
+                && ($property->getVerified() !== self::NOT_VERIFIED
524
+                    || $wasVerified)
525
+            ) {
526
+                $property->setVerified(self::NOT_VERIFIED);
527
+            }
528
+        }
529
+    }
530
+
531
+    /**
532
+     * add new user to accounts table
533
+     *
534
+     * @param IUser $user
535
+     * @param array $data
536
+     */
537
+    protected function insertNewUser(IUser $user, array $data): void {
538
+        $uid = $user->getUID();
539
+        $jsonEncodedData = $this->prepareJson($data);
540
+        $query = $this->connection->getQueryBuilder();
541
+        $query->insert($this->table)
542
+            ->values(
543
+                [
544
+                    'uid' => $query->createNamedParameter($uid),
545
+                    'data' => $query->createNamedParameter($jsonEncodedData),
546
+                ]
547
+            )
548
+            ->executeStatement();
549
+
550
+        $this->deleteUserData($user);
551
+        $this->writeUserData($user, $data);
552
+    }
553
+
554
+    protected function prepareJson(array $data): string {
555
+        $preparedData = [];
556
+        foreach ($data as $dataRow) {
557
+            $propertyName = $dataRow['name'];
558
+            unset($dataRow['name']);
559
+
560
+            if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
561
+                // do not write default value, save DB space
562
+                unset($dataRow['locallyVerified']);
563
+            }
564
+
565
+            if (!$this->isCollection($propertyName)) {
566
+                $preparedData[$propertyName] = $dataRow;
567
+                continue;
568
+            }
569
+            if (!isset($preparedData[$propertyName])) {
570
+                $preparedData[$propertyName] = [];
571
+            }
572
+            $preparedData[$propertyName][] = $dataRow;
573
+        }
574
+        return json_encode($preparedData);
575
+    }
576
+
577
+    protected function importFromJson(string $json, string $userId): ?array {
578
+        $result = [];
579
+        $jsonArray = json_decode($json, true);
580
+        $jsonError = json_last_error();
581
+        if ($jsonError !== JSON_ERROR_NONE) {
582
+            $this->logger->critical(
583
+                'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
584
+                [
585
+                    'uid' => $userId,
586
+                    'json_error' => $jsonError
587
+                ]
588
+            );
589
+            return null;
590
+        }
591
+        foreach ($jsonArray as $propertyName => $row) {
592
+            if (!$this->isCollection($propertyName)) {
593
+                $result[] = array_merge($row, ['name' => $propertyName]);
594
+                continue;
595
+            }
596
+            foreach ($row as $singleRow) {
597
+                $result[] = array_merge($singleRow, ['name' => $propertyName]);
598
+            }
599
+        }
600
+        return $result;
601
+    }
602
+
603
+    /**
604
+     * update existing user in accounts table
605
+     *
606
+     * @param IUser $user
607
+     * @param array $data
608
+     */
609
+    protected function updateExistingUser(IUser $user, array $data): void {
610
+        $uid = $user->getUID();
611
+        $jsonEncodedData = $this->prepareJson($data);
612
+        $query = $this->connection->getQueryBuilder();
613
+        $query->update($this->table)
614
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
615
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
616
+            ->executeStatement();
617
+
618
+        $this->deleteUserData($user);
619
+        $this->writeUserData($user, $data);
620
+    }
621
+
622
+    protected function writeUserData(IUser $user, array $data): void {
623
+        $query = $this->connection->getQueryBuilder();
624
+        $query->insert($this->dataTable)
625
+            ->values(
626
+                [
627
+                    'uid' => $query->createNamedParameter($user->getUID()),
628
+                    'name' => $query->createParameter('name'),
629
+                    'value' => $query->createParameter('value'),
630
+                ]
631
+            );
632
+        $this->writeUserDataProperties($query, $data);
633
+    }
634
+
635
+    protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
636
+        foreach ($data as $property) {
637
+            if ($property['name'] === self::PROPERTY_AVATAR) {
638
+                continue;
639
+            }
640
+
641
+            // the value col is limited to 255 bytes. It is used for searches only.
642
+            $value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
643
+
644
+            $query->setParameter('name', $property['name'])
645
+                ->setParameter('value', $value);
646
+            $query->executeStatement();
647
+        }
648
+    }
649
+
650
+    /**
651
+     * build default user record in case not data set exists yet
652
+     *
653
+     * @param IUser $user
654
+     * @return array
655
+     */
656
+    protected function buildDefaultUserRecord(IUser $user) {
657
+        return [
658
+            [
659
+                'name' => self::PROPERTY_DISPLAYNAME,
660
+                'value' => $user->getDisplayName(),
661
+                'scope' => self::SCOPE_FEDERATED,
662
+                'verified' => self::NOT_VERIFIED,
663
+            ],
664
+
665
+            [
666
+                'name' => self::PROPERTY_ADDRESS,
667
+                'value' => '',
668
+                'scope' => self::SCOPE_LOCAL,
669
+                'verified' => self::NOT_VERIFIED,
670
+            ],
671
+
672
+            [
673
+                'name' => self::PROPERTY_WEBSITE,
674
+                'value' => '',
675
+                'scope' => self::SCOPE_LOCAL,
676
+                'verified' => self::NOT_VERIFIED,
677
+            ],
678
+
679
+            [
680
+                'name' => self::PROPERTY_EMAIL,
681
+                'value' => $user->getEMailAddress(),
682
+                'scope' => self::SCOPE_FEDERATED,
683
+                'verified' => self::NOT_VERIFIED,
684
+            ],
685
+
686
+            [
687
+                'name' => self::PROPERTY_AVATAR,
688
+                'scope' => self::SCOPE_FEDERATED
689
+            ],
690
+
691
+            [
692
+                'name' => self::PROPERTY_PHONE,
693
+                'value' => '',
694
+                'scope' => self::SCOPE_LOCAL,
695
+                'verified' => self::NOT_VERIFIED,
696
+            ],
697
+
698
+            [
699
+                'name' => self::PROPERTY_TWITTER,
700
+                'value' => '',
701
+                'scope' => self::SCOPE_LOCAL,
702
+                'verified' => self::NOT_VERIFIED,
703
+            ],
704
+
705
+            [
706
+                'name' => self::PROPERTY_ORGANISATION,
707
+                'value' => '',
708
+                'scope' => self::SCOPE_LOCAL,
709
+            ],
710
+
711
+            [
712
+                'name' => self::PROPERTY_ROLE,
713
+                'value' => '',
714
+                'scope' => self::SCOPE_LOCAL,
715
+            ],
716
+
717
+            [
718
+                'name' => self::PROPERTY_HEADLINE,
719
+                'value' => '',
720
+                'scope' => self::SCOPE_LOCAL,
721
+            ],
722
+
723
+            [
724
+                'name' => self::PROPERTY_BIOGRAPHY,
725
+                'value' => '',
726
+                'scope' => self::SCOPE_LOCAL,
727
+            ],
728
+
729
+            [
730
+                'name' => self::PROPERTY_PROFILE_ENABLED,
731
+                'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
732
+            ],
733
+        ];
734
+    }
735
+
736
+    private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
737
+        $collection = $account->getPropertyCollection($data['name']);
738
+
739
+        $p = new AccountProperty(
740
+            $data['name'],
741
+            $data['value'] ?? '',
742
+            $data['scope'] ?? self::SCOPE_LOCAL,
743
+            $data['verified'] ?? self::NOT_VERIFIED,
744
+            ''
745
+        );
746
+        $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
747
+        $collection->addProperty($p);
748
+
749
+        return $collection;
750
+    }
751
+
752
+    private function parseAccountData(IUser $user, $data): Account {
753
+        $account = new Account($user);
754
+        foreach ($data as $accountData) {
755
+            if ($this->isCollection($accountData['name'])) {
756
+                $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
757
+            } else {
758
+                $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
759
+                if (isset($accountData['locallyVerified'])) {
760
+                    $property = $account->getProperty($accountData['name']);
761
+                    $property->setLocallyVerified($accountData['locallyVerified']);
762
+                }
763
+            }
764
+        }
765
+        return $account;
766
+    }
767
+
768
+    public function getAccount(IUser $user): IAccount {
769
+        if ($this->internalCache->hasKey($user->getUID())) {
770
+            return $this->internalCache->get($user->getUID());
771
+        }
772
+        $account = $this->parseAccountData($user, $this->getUser($user));
773
+        $this->internalCache->set($user->getUID(), $account);
774
+        return $account;
775
+    }
776
+
777
+    public function updateAccount(IAccount $account): void {
778
+        $this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
779
+        try {
780
+            $property = $account->getProperty(self::PROPERTY_PHONE);
781
+            $this->sanitizePhoneNumberValue($property);
782
+        } catch (PropertyDoesNotExistException $e) {
783
+            //  valid case, nothing to do
784
+        }
785
+
786
+        try {
787
+            $property = $account->getProperty(self::PROPERTY_WEBSITE);
788
+            $this->sanitizeWebsite($property);
789
+        } catch (PropertyDoesNotExistException $e) {
790
+            //  valid case, nothing to do
791
+        }
792
+
793
+        static $allowedScopes = [
794
+            self::SCOPE_PRIVATE,
795
+            self::SCOPE_LOCAL,
796
+            self::SCOPE_FEDERATED,
797
+            self::SCOPE_PUBLISHED,
798
+            self::VISIBILITY_PRIVATE,
799
+            self::VISIBILITY_CONTACTS_ONLY,
800
+            self::VISIBILITY_PUBLIC,
801
+        ];
802
+        foreach ($account->getAllProperties() as $property) {
803
+            $this->testPropertyScope($property, $allowedScopes, true);
804
+        }
805
+
806
+        $oldData = $this->getUser($account->getUser(), false);
807
+        $this->updateVerificationStatus($account, $oldData);
808
+        $this->checkEmailVerification($account, $oldData);
809
+        $this->checkLocalEmailVerification($account, $oldData);
810
+
811
+        $data = [];
812
+        foreach ($account->getAllProperties() as $property) {
813
+            /** @var IAccountProperty $property */
814
+            $data[] = [
815
+                'name' => $property->getName(),
816
+                'value' => $property->getValue(),
817
+                'scope' => $property->getScope(),
818
+                'verified' => $property->getVerified(),
819
+                'locallyVerified' => $property->getLocallyVerified(),
820
+            ];
821
+        }
822
+
823
+        $this->updateUser($account->getUser(), $data, true);
824
+        $this->internalCache->set($account->getUser()->getUID(), $account);
825
+    }
826 826
 }
Please login to merge, or discard this patch.