Passed
Push — master ( e1a300...aa651f )
by Joas
15:36 queued 12s
created

AccountManager   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 511
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 238
c 3
b 0
f 0
dl 0
loc 511
rs 2
wmc 83

18 Methods

Rating   Name   Duplication   Size   Complexity  
A parsePhoneNumber() 0 22 6
A __construct() 0 10 1
A addMissingDefaultValues() 0 8 3
A checkEmailVerification() 0 16 2
A writeUserData() 0 18 3
A updateAccount() 0 12 2
F updateVerifyStatus() 0 46 19
A getUser() 0 27 5
A buildDefaultUserRecord() 0 41 1
A deleteUser() 0 8 1
A getAccount() 0 2 1
A parseAccountData() 0 6 2
F updateUser() 0 95 25
A updateExistingUser() 0 11 1
A insertNewUser() 0 15 1
A parseWebsite() 0 11 6
A deleteUserData() 0 6 1
A searchUsers() 0 20 3

How to fix   Complexity   

Complex Class

Complex classes like AccountManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AccountManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2016, Björn Schießle
5
 *
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Daniel Kesselberg <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Julius Härtl <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\Accounts;
32
33
use libphonenumber\NumberParseException;
34
use libphonenumber\PhoneNumber;
35
use libphonenumber\PhoneNumberFormat;
36
use libphonenumber\PhoneNumberUtil;
37
use OCA\Settings\BackgroundJobs\VerifyUserData;
38
use OCP\Accounts\IAccount;
39
use OCP\Accounts\IAccountManager;
40
use OCP\BackgroundJob\IJobList;
41
use OCP\DB\QueryBuilder\IQueryBuilder;
42
use OCP\IConfig;
43
use OCP\IDBConnection;
44
use OCP\IUser;
45
use Psr\Log\LoggerInterface;
46
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
47
use Symfony\Component\EventDispatcher\GenericEvent;
48
use function json_decode;
49
use function json_last_error;
50
51
/**
52
 * Class AccountManager
53
 *
54
 * Manage system accounts table
55
 *
56
 * @group DB
57
 * @package OC\Accounts
58
 */
59
class AccountManager implements IAccountManager {
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) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
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,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\Accounts\IAccountManager::VISIBILITY_PRIVATE has been deprecated: 21.0.1 ( Ignorable by Annotation )

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

196
			/** @scrutinizer ignore-deprecated */ self::VISIBILITY_PRIVATE,

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

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

Loading history...
197
			self::VISIBILITY_CONTACTS_ONLY,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\Accounts\IAccountMan...ISIBILITY_CONTACTS_ONLY has been deprecated: 21.0.1 ( Ignorable by Annotation )

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

197
			/** @scrutinizer ignore-deprecated */ self::VISIBILITY_CONTACTS_ONLY,

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

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

Loading history...
198
			self::VISIBILITY_PUBLIC,
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\Accounts\IAccountManager::VISIBILITY_PUBLIC has been deprecated: 21.0.1 ( Ignorable by Annotation )

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

198
			/** @scrutinizer ignore-deprecated */ self::VISIBILITY_PUBLIC,

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

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

Loading history...
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',
0 ignored issues
show
Bug introduced by
'OC\AccountManager::userUpdated' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

240
				/** @scrutinizer ignore-type */ 'OC\AccountManager::userUpdated',
Loading history...
241
				new GenericEvent($user, $data)
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ericEvent($user, $data). ( Ignorable by Annotation )

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

241
			$this->eventDispatcher->/** @scrutinizer ignore-call */ 
242
                           dispatch(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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
}
572