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

AccountManager   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 187
c 3
b 0
f 0
dl 0
loc 411
rs 5.04
wmc 57

16 Methods

Rating   Name   Duplication   Size   Complexity  
A checkEmailVerification() 0 16 2
A addMissingDefaultValues() 0 8 3
A parsePhoneNumber() 0 22 6
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 __construct() 0 10 1
A getAccount() 0 2 1
A parseAccountData() 0 6 2
A updateExistingUser() 0 11 1
B updateUser() 0 34 8
A insertNewUser() 0 15 1
A deleteUserData() 0 6 1
A searchUsers() 0 16 2

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
		if (empty($userData)) {
148
			$this->insertNewUser($user, $data);
149
		} elseif ($userData !== $data) {
150
			$data = $this->checkEmailVerification($userData, $data, $user);
151
			$data = $this->updateVerifyStatus($userData, $data);
152
			$this->updateExistingUser($user, $data);
153
		} else {
154
			// nothing needs to be done if new and old data set are the same
155
			$updated = false;
156
		}
157
158
		if ($updated) {
159
			$this->eventDispatcher->dispatch(
160
				'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

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

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