Passed
Push — master ( 456412...602de2 )
by Joas
13:17 queued 10s
created

AccountManager   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 453
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 211
c 1
b 0
f 0
dl 0
loc 453
rs 3.12
wmc 66

16 Methods

Rating   Name   Duplication   Size   Complexity  
A parsePhoneNumber() 0 22 6
A __construct() 0 10 1
A checkEmailVerification() 0 16 2
A addMissingDefaultValues() 0 8 3
A writeUserData() 0 18 3
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
A updateExistingUser() 0 11 1
C updateUser() 0 70 16
A insertNewUser() 0 15 1
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
	 * update user record
125
	 *
126
	 * @param IUser $user
127
	 * @param array $data
128
	 * @param bool $throwOnData Set to true if you can inform the user about invalid data
129
	 * @return array The potentially modified data (e.g. phone numbers are converted to E.164 format)
130
	 * @throws \InvalidArgumentException Message is the property that was invalid
131
	 */
132
	public function updateUser(IUser $user, array $data, bool $throwOnData = false): array {
133
		$userData = $this->getUser($user);
134
		$updated = true;
135
136
		if (isset($data[self::PROPERTY_PHONE]) && $data[self::PROPERTY_PHONE]['value'] !== '') {
137
			try {
138
				$data[self::PROPERTY_PHONE]['value'] = $this->parsePhoneNumber($data[self::PROPERTY_PHONE]['value']);
139
			} catch (\InvalidArgumentException $e) {
140
				if ($throwOnData) {
141
					throw $e;
142
				}
143
				$data[self::PROPERTY_PHONE]['value'] = '';
144
			}
145
		}
146
147
		$allowedScopes = [
148
			self::SCOPE_PRIVATE,
149
			self::SCOPE_LOCAL,
150
			self::SCOPE_FEDERATED,
151
			self::SCOPE_PUBLISHED,
152
			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

152
			/** @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...
153
			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

153
			/** @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...
154
			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

154
			/** @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...
155
		];
156
157
		// validate and convert scope values
158
		foreach ($data as $propertyName => $propertyData) {
159
			if (isset($propertyData['scope'])) {
160
				if ($throwOnData && !in_array($propertyData['scope'], $allowedScopes, true)) {
161
					throw new \InvalidArgumentException('scope');
162
				}
163
164
				if (
165
					$propertyData['scope'] === self::SCOPE_PRIVATE
166
					&& ($propertyName === self::PROPERTY_DISPLAYNAME || $propertyName === self::PROPERTY_EMAIL)
167
				) {
168
					if ($throwOnData) {
169
						// v2-private is not available for these fields
170
						throw new \InvalidArgumentException('scope');
171
					} else {
172
						// default to local
173
						$data[$propertyName]['scope'] = self::SCOPE_LOCAL;
174
					}
175
				} else {
176
					// migrate scope values to the new format
177
					// invalid scopes are mapped to a default value
178
					$data[$propertyName]['scope'] = AccountProperty::mapScopeToV2($propertyData['scope']);
179
				}
180
			}
181
		}
182
183
		if (empty($userData)) {
184
			$this->insertNewUser($user, $data);
185
		} elseif ($userData !== $data) {
186
			$data = $this->checkEmailVerification($userData, $data, $user);
187
			$data = $this->updateVerifyStatus($userData, $data);
188
			$this->updateExistingUser($user, $data);
189
		} else {
190
			// nothing needs to be done if new and old data set are the same
191
			$updated = false;
192
		}
193
194
		if ($updated) {
195
			$this->eventDispatcher->dispatch(
196
				'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

196
				/** @scrutinizer ignore-type */ 'OC\AccountManager::userUpdated',
Loading history...
197
				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

197
			$this->eventDispatcher->/** @scrutinizer ignore-call */ 
198
                           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...
198
			);
199
		}
200
201
		return $data;
202
	}
203
204
	/**
205
	 * delete user from accounts table
206
	 *
207
	 * @param IUser $user
208
	 */
209
	public function deleteUser(IUser $user) {
210
		$uid = $user->getUID();
211
		$query = $this->connection->getQueryBuilder();
212
		$query->delete($this->table)
213
			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
214
			->execute();
215
216
		$this->deleteUserData($user);
217
	}
218
219
	/**
220
	 * delete user from accounts table
221
	 *
222
	 * @param IUser $user
223
	 */
224
	public function deleteUserData(IUser $user): void {
225
		$uid = $user->getUID();
226
		$query = $this->connection->getQueryBuilder();
227
		$query->delete($this->dataTable)
228
			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
229
			->execute();
230
	}
231
232
	/**
233
	 * get stored data from a given user
234
	 *
235
	 * @param IUser $user
236
	 * @return array
237
	 *
238
	 * @deprecated use getAccount instead to make sure migrated properties work correctly
239
	 */
240
	public function getUser(IUser $user) {
241
		$uid = $user->getUID();
242
		$query = $this->connection->getQueryBuilder();
243
		$query->select('data')
244
			->from($this->table)
245
			->where($query->expr()->eq('uid', $query->createParameter('uid')))
246
			->setParameter('uid', $uid);
247
		$result = $query->execute();
248
		$accountData = $result->fetchAll();
249
		$result->closeCursor();
250
251
		if (empty($accountData)) {
252
			$userData = $this->buildDefaultUserRecord($user);
253
			$this->insertNewUser($user, $userData);
254
			return $userData;
255
		}
256
257
		$userDataArray = json_decode($accountData[0]['data'], true);
258
		$jsonError = json_last_error();
259
		if ($userDataArray === null || $userDataArray === [] || $jsonError !== JSON_ERROR_NONE) {
260
			$this->logger->critical("User data of $uid contained invalid JSON (error $jsonError), hence falling back to a default user record");
261
			return $this->buildDefaultUserRecord($user);
262
		}
263
264
		$userDataArray = $this->addMissingDefaultValues($userDataArray);
265
266
		return $userDataArray;
267
	}
268
269
	public function searchUsers(string $property, array $values): array {
270
		$chunks = array_chunk($values, 500);
271
		$query = $this->connection->getQueryBuilder();
272
		$query->select('*')
273
			->from($this->dataTable)
274
			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
275
			->andWhere($query->expr()->in('value', $query->createParameter('values')));
276
277
		$matches = [];
278
		foreach ($chunks as $chunk) {
279
			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
280
			$result = $query->execute();
281
282
			while ($row = $result->fetch()) {
283
				$matches[$row['value']] = $row['uid'];
284
			}
285
			$result->closeCursor();
286
		}
287
288
		return $matches;
289
	}
290
291
	/**
292
	 * check if we need to ask the server for email verification, if yes we create a cronjob
293
	 *
294
	 * @param $oldData
295
	 * @param $newData
296
	 * @param IUser $user
297
	 * @return array
298
	 */
299
	protected function checkEmailVerification($oldData, $newData, IUser $user) {
300
		if ($oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']) {
301
			$this->jobList->add(VerifyUserData::class,
302
				[
303
					'verificationCode' => '',
304
					'data' => $newData[self::PROPERTY_EMAIL]['value'],
305
					'type' => self::PROPERTY_EMAIL,
306
					'uid' => $user->getUID(),
307
					'try' => 0,
308
					'lastRun' => time()
309
				]
310
			);
311
			$newData[self::PROPERTY_EMAIL]['verified'] = self::VERIFICATION_IN_PROGRESS;
312
		}
313
314
		return $newData;
315
	}
316
317
	/**
318
	 * make sure that all expected data are set
319
	 *
320
	 * @param array $userData
321
	 * @return array
322
	 */
323
	protected function addMissingDefaultValues(array $userData) {
324
		foreach ($userData as $key => $value) {
325
			if (!isset($userData[$key]['verified'])) {
326
				$userData[$key]['verified'] = self::NOT_VERIFIED;
327
			}
328
		}
329
330
		return $userData;
331
	}
332
333
	/**
334
	 * reset verification status if personal data changed
335
	 *
336
	 * @param array $oldData
337
	 * @param array $newData
338
	 * @return array
339
	 */
340
	protected function updateVerifyStatus($oldData, $newData) {
341
342
		// which account was already verified successfully?
343
		$twitterVerified = isset($oldData[self::PROPERTY_TWITTER]['verified']) && $oldData[self::PROPERTY_TWITTER]['verified'] === self::VERIFIED;
344
		$websiteVerified = isset($oldData[self::PROPERTY_WEBSITE]['verified']) && $oldData[self::PROPERTY_WEBSITE]['verified'] === self::VERIFIED;
345
		$emailVerified = isset($oldData[self::PROPERTY_EMAIL]['verified']) && $oldData[self::PROPERTY_EMAIL]['verified'] === self::VERIFIED;
346
347
		// keep old verification status if we don't have a new one
348
		if (!isset($newData[self::PROPERTY_TWITTER]['verified'])) {
349
			// keep old verification status if value didn't changed and an old value exists
350
			$keepOldStatus = $newData[self::PROPERTY_TWITTER]['value'] === $oldData[self::PROPERTY_TWITTER]['value'] && isset($oldData[self::PROPERTY_TWITTER]['verified']);
351
			$newData[self::PROPERTY_TWITTER]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_TWITTER]['verified'] : self::NOT_VERIFIED;
352
		}
353
354
		if (!isset($newData[self::PROPERTY_WEBSITE]['verified'])) {
355
			// keep old verification status if value didn't changed and an old value exists
356
			$keepOldStatus = $newData[self::PROPERTY_WEBSITE]['value'] === $oldData[self::PROPERTY_WEBSITE]['value'] && isset($oldData[self::PROPERTY_WEBSITE]['verified']);
357
			$newData[self::PROPERTY_WEBSITE]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_WEBSITE]['verified'] : self::NOT_VERIFIED;
358
		}
359
360
		if (!isset($newData[self::PROPERTY_EMAIL]['verified'])) {
361
			// keep old verification status if value didn't changed and an old value exists
362
			$keepOldStatus = $newData[self::PROPERTY_EMAIL]['value'] === $oldData[self::PROPERTY_EMAIL]['value'] && isset($oldData[self::PROPERTY_EMAIL]['verified']);
363
			$newData[self::PROPERTY_EMAIL]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_EMAIL]['verified'] : self::VERIFICATION_IN_PROGRESS;
364
		}
365
366
		// reset verification status if a value from a previously verified data was changed
367
		if ($twitterVerified &&
368
			$oldData[self::PROPERTY_TWITTER]['value'] !== $newData[self::PROPERTY_TWITTER]['value']
369
		) {
370
			$newData[self::PROPERTY_TWITTER]['verified'] = self::NOT_VERIFIED;
371
		}
372
373
		if ($websiteVerified &&
374
			$oldData[self::PROPERTY_WEBSITE]['value'] !== $newData[self::PROPERTY_WEBSITE]['value']
375
		) {
376
			$newData[self::PROPERTY_WEBSITE]['verified'] = self::NOT_VERIFIED;
377
		}
378
379
		if ($emailVerified &&
380
			$oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']
381
		) {
382
			$newData[self::PROPERTY_EMAIL]['verified'] = self::NOT_VERIFIED;
383
		}
384
385
		return $newData;
386
	}
387
388
	/**
389
	 * add new user to accounts table
390
	 *
391
	 * @param IUser $user
392
	 * @param array $data
393
	 */
394
	protected function insertNewUser(IUser $user, array $data): void {
395
		$uid = $user->getUID();
396
		$jsonEncodedData = json_encode($data);
397
		$query = $this->connection->getQueryBuilder();
398
		$query->insert($this->table)
399
			->values(
400
				[
401
					'uid' => $query->createNamedParameter($uid),
402
					'data' => $query->createNamedParameter($jsonEncodedData),
403
				]
404
			)
405
			->execute();
406
407
		$this->deleteUserData($user);
408
		$this->writeUserData($user, $data);
409
	}
410
411
	/**
412
	 * update existing user in accounts table
413
	 *
414
	 * @param IUser $user
415
	 * @param array $data
416
	 */
417
	protected function updateExistingUser(IUser $user, array $data): void {
418
		$uid = $user->getUID();
419
		$jsonEncodedData = json_encode($data);
420
		$query = $this->connection->getQueryBuilder();
421
		$query->update($this->table)
422
			->set('data', $query->createNamedParameter($jsonEncodedData))
423
			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
424
			->execute();
425
426
		$this->deleteUserData($user);
427
		$this->writeUserData($user, $data);
428
	}
429
430
	protected function writeUserData(IUser $user, array $data): void {
431
		$query = $this->connection->getQueryBuilder();
432
		$query->insert($this->dataTable)
433
			->values(
434
				[
435
					'uid' => $query->createNamedParameter($user->getUID()),
436
					'name' => $query->createParameter('name'),
437
					'value' => $query->createParameter('value'),
438
				]
439
			);
440
		foreach ($data as $propertyName => $property) {
441
			if ($propertyName === self::PROPERTY_AVATAR) {
442
				continue;
443
			}
444
445
			$query->setParameter('name', $propertyName)
446
				->setParameter('value', $property['value'] ?? '');
447
			$query->execute();
448
		}
449
	}
450
451
	/**
452
	 * build default user record in case not data set exists yet
453
	 *
454
	 * @param IUser $user
455
	 * @return array
456
	 */
457
	protected function buildDefaultUserRecord(IUser $user) {
458
		return [
459
			self::PROPERTY_DISPLAYNAME =>
460
				[
461
					'value' => $user->getDisplayName(),
462
					'scope' => self::SCOPE_FEDERATED,
463
					'verified' => self::NOT_VERIFIED,
464
				],
465
			self::PROPERTY_ADDRESS =>
466
				[
467
					'value' => '',
468
					'scope' => self::SCOPE_LOCAL,
469
					'verified' => self::NOT_VERIFIED,
470
				],
471
			self::PROPERTY_WEBSITE =>
472
				[
473
					'value' => '',
474
					'scope' => self::SCOPE_LOCAL,
475
					'verified' => self::NOT_VERIFIED,
476
				],
477
			self::PROPERTY_EMAIL =>
478
				[
479
					'value' => $user->getEMailAddress(),
480
					'scope' => self::SCOPE_FEDERATED,
481
					'verified' => self::NOT_VERIFIED,
482
				],
483
			self::PROPERTY_AVATAR =>
484
				[
485
					'scope' => self::SCOPE_FEDERATED
486
				],
487
			self::PROPERTY_PHONE =>
488
				[
489
					'value' => '',
490
					'scope' => self::SCOPE_LOCAL,
491
					'verified' => self::NOT_VERIFIED,
492
				],
493
			self::PROPERTY_TWITTER =>
494
				[
495
					'value' => '',
496
					'scope' => self::SCOPE_LOCAL,
497
					'verified' => self::NOT_VERIFIED,
498
				],
499
		];
500
	}
501
502
	private function parseAccountData(IUser $user, $data): Account {
503
		$account = new Account($user);
504
		foreach ($data as $property => $accountData) {
505
			$account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
506
		}
507
		return $account;
508
	}
509
510
	public function getAccount(IUser $user): IAccount {
511
		return $this->parseAccountData($user, $this->getUser($user));
512
	}
513
}
514