Passed
Push — master ( 812bc8...7ac6ee )
by Roeland
17:22 queued 11s
created

AccountManager   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 415
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 190
c 1
b 0
f 0
dl 0
loc 415
rs 4.5599
wmc 58

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getUser() 0 27 5
A deleteUser() 0 8 1
A deleteUserData() 0 6 1
A parsePhoneNumber() 0 22 6
A __construct() 0 10 1
B updateUser() 0 34 8
A checkEmailVerification() 0 16 2
A addMissingDefaultValues() 0 8 3
A writeUserData() 0 18 3
F updateVerifyStatus() 0 46 19
A buildDefaultUserRecord() 0 41 1
A getAccount() 0 2 1
A parseAccountData() 0 6 2
A updateExistingUser() 0 11 1
A insertNewUser() 0 15 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
		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
		$chunks = array_chunk($values, 500);
233
		$query = $this->connection->getQueryBuilder();
234
		$query->select('*')
235
			->from($this->dataTable)
236
			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
237
			->andWhere($query->expr()->in('value', $query->createParameter('values')));
238
239
		$matches = [];
240
		foreach ($chunks as $chunk) {
241
			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
242
			$result = $query->execute();
243
244
			while ($row = $result->fetch()) {
245
				$matches[$row['value']] = $row['uid'];
246
			}
247
			$result->closeCursor();
248
		}
249
250
		return $matches;
251
	}
252
253
	/**
254
	 * check if we need to ask the server for email verification, if yes we create a cronjob
255
	 *
256
	 * @param $oldData
257
	 * @param $newData
258
	 * @param IUser $user
259
	 * @return array
260
	 */
261
	protected function checkEmailVerification($oldData, $newData, IUser $user) {
262
		if ($oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']) {
263
			$this->jobList->add(VerifyUserData::class,
264
				[
265
					'verificationCode' => '',
266
					'data' => $newData[self::PROPERTY_EMAIL]['value'],
267
					'type' => self::PROPERTY_EMAIL,
268
					'uid' => $user->getUID(),
269
					'try' => 0,
270
					'lastRun' => time()
271
				]
272
			);
273
			$newData[self::PROPERTY_EMAIL]['verified'] = self::VERIFICATION_IN_PROGRESS;
274
		}
275
276
		return $newData;
277
	}
278
279
	/**
280
	 * make sure that all expected data are set
281
	 *
282
	 * @param array $userData
283
	 * @return array
284
	 */
285
	protected function addMissingDefaultValues(array $userData) {
286
		foreach ($userData as $key => $value) {
287
			if (!isset($userData[$key]['verified'])) {
288
				$userData[$key]['verified'] = self::NOT_VERIFIED;
289
			}
290
		}
291
292
		return $userData;
293
	}
294
295
	/**
296
	 * reset verification status if personal data changed
297
	 *
298
	 * @param array $oldData
299
	 * @param array $newData
300
	 * @return array
301
	 */
302
	protected function updateVerifyStatus($oldData, $newData) {
303
304
		// which account was already verified successfully?
305
		$twitterVerified = isset($oldData[self::PROPERTY_TWITTER]['verified']) && $oldData[self::PROPERTY_TWITTER]['verified'] === self::VERIFIED;
306
		$websiteVerified = isset($oldData[self::PROPERTY_WEBSITE]['verified']) && $oldData[self::PROPERTY_WEBSITE]['verified'] === self::VERIFIED;
307
		$emailVerified = isset($oldData[self::PROPERTY_EMAIL]['verified']) && $oldData[self::PROPERTY_EMAIL]['verified'] === self::VERIFIED;
308
309
		// keep old verification status if we don't have a new one
310
		if (!isset($newData[self::PROPERTY_TWITTER]['verified'])) {
311
			// keep old verification status if value didn't changed and an old value exists
312
			$keepOldStatus = $newData[self::PROPERTY_TWITTER]['value'] === $oldData[self::PROPERTY_TWITTER]['value'] && isset($oldData[self::PROPERTY_TWITTER]['verified']);
313
			$newData[self::PROPERTY_TWITTER]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_TWITTER]['verified'] : self::NOT_VERIFIED;
314
		}
315
316
		if (!isset($newData[self::PROPERTY_WEBSITE]['verified'])) {
317
			// keep old verification status if value didn't changed and an old value exists
318
			$keepOldStatus = $newData[self::PROPERTY_WEBSITE]['value'] === $oldData[self::PROPERTY_WEBSITE]['value'] && isset($oldData[self::PROPERTY_WEBSITE]['verified']);
319
			$newData[self::PROPERTY_WEBSITE]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_WEBSITE]['verified'] : self::NOT_VERIFIED;
320
		}
321
322
		if (!isset($newData[self::PROPERTY_EMAIL]['verified'])) {
323
			// keep old verification status if value didn't changed and an old value exists
324
			$keepOldStatus = $newData[self::PROPERTY_EMAIL]['value'] === $oldData[self::PROPERTY_EMAIL]['value'] && isset($oldData[self::PROPERTY_EMAIL]['verified']);
325
			$newData[self::PROPERTY_EMAIL]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_EMAIL]['verified'] : self::VERIFICATION_IN_PROGRESS;
326
		}
327
328
		// reset verification status if a value from a previously verified data was changed
329
		if ($twitterVerified &&
330
			$oldData[self::PROPERTY_TWITTER]['value'] !== $newData[self::PROPERTY_TWITTER]['value']
331
		) {
332
			$newData[self::PROPERTY_TWITTER]['verified'] = self::NOT_VERIFIED;
333
		}
334
335
		if ($websiteVerified &&
336
			$oldData[self::PROPERTY_WEBSITE]['value'] !== $newData[self::PROPERTY_WEBSITE]['value']
337
		) {
338
			$newData[self::PROPERTY_WEBSITE]['verified'] = self::NOT_VERIFIED;
339
		}
340
341
		if ($emailVerified &&
342
			$oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']
343
		) {
344
			$newData[self::PROPERTY_EMAIL]['verified'] = self::NOT_VERIFIED;
345
		}
346
347
		return $newData;
348
	}
349
350
	/**
351
	 * add new user to accounts table
352
	 *
353
	 * @param IUser $user
354
	 * @param array $data
355
	 */
356
	protected function insertNewUser(IUser $user, array $data): void {
357
		$uid = $user->getUID();
358
		$jsonEncodedData = json_encode($data);
359
		$query = $this->connection->getQueryBuilder();
360
		$query->insert($this->table)
361
			->values(
362
				[
363
					'uid' => $query->createNamedParameter($uid),
364
					'data' => $query->createNamedParameter($jsonEncodedData),
365
				]
366
			)
367
			->execute();
368
369
		$this->deleteUserData($user);
370
		$this->writeUserData($user, $data);
371
	}
372
373
	/**
374
	 * update existing user in accounts table
375
	 *
376
	 * @param IUser $user
377
	 * @param array $data
378
	 */
379
	protected function updateExistingUser(IUser $user, array $data): void {
380
		$uid = $user->getUID();
381
		$jsonEncodedData = json_encode($data);
382
		$query = $this->connection->getQueryBuilder();
383
		$query->update($this->table)
384
			->set('data', $query->createNamedParameter($jsonEncodedData))
385
			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
386
			->execute();
387
388
		$this->deleteUserData($user);
389
		$this->writeUserData($user, $data);
390
	}
391
392
	protected function writeUserData(IUser $user, array $data): void {
393
		$query = $this->connection->getQueryBuilder();
394
		$query->insert($this->dataTable)
395
			->values(
396
				[
397
					'uid' => $query->createNamedParameter($user->getUID()),
398
					'name' => $query->createParameter('name'),
399
					'value' => $query->createParameter('value'),
400
				]
401
			);
402
		foreach ($data as $propertyName => $property) {
403
			if ($propertyName === self::PROPERTY_AVATAR) {
404
				continue;
405
			}
406
407
			$query->setParameter('name', $propertyName)
408
				->setParameter('value', $property['value']);
409
			$query->execute();
410
		}
411
	}
412
413
	/**
414
	 * build default user record in case not data set exists yet
415
	 *
416
	 * @param IUser $user
417
	 * @return array
418
	 */
419
	protected function buildDefaultUserRecord(IUser $user) {
420
		return [
421
			self::PROPERTY_DISPLAYNAME =>
422
				[
423
					'value' => $user->getDisplayName(),
424
					'scope' => self::VISIBILITY_CONTACTS_ONLY,
425
					'verified' => self::NOT_VERIFIED,
426
				],
427
			self::PROPERTY_ADDRESS =>
428
				[
429
					'value' => '',
430
					'scope' => self::VISIBILITY_PRIVATE,
431
					'verified' => self::NOT_VERIFIED,
432
				],
433
			self::PROPERTY_WEBSITE =>
434
				[
435
					'value' => '',
436
					'scope' => self::VISIBILITY_PRIVATE,
437
					'verified' => self::NOT_VERIFIED,
438
				],
439
			self::PROPERTY_EMAIL =>
440
				[
441
					'value' => $user->getEMailAddress(),
442
					'scope' => self::VISIBILITY_CONTACTS_ONLY,
443
					'verified' => self::NOT_VERIFIED,
444
				],
445
			self::PROPERTY_AVATAR =>
446
				[
447
					'scope' => self::VISIBILITY_CONTACTS_ONLY
448
				],
449
			self::PROPERTY_PHONE =>
450
				[
451
					'value' => '',
452
					'scope' => self::VISIBILITY_PRIVATE,
453
					'verified' => self::NOT_VERIFIED,
454
				],
455
			self::PROPERTY_TWITTER =>
456
				[
457
					'value' => '',
458
					'scope' => self::VISIBILITY_PRIVATE,
459
					'verified' => self::NOT_VERIFIED,
460
				],
461
		];
462
	}
463
464
	private function parseAccountData(IUser $user, $data): Account {
465
		$account = new Account($user);
466
		foreach ($data as $property => $accountData) {
467
			$account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::VISIBILITY_PRIVATE, $accountData['verified'] ?? self::NOT_VERIFIED);
468
		}
469
		return $account;
470
	}
471
472
	public function getAccount(IUser $user): IAccount {
473
		return $this->parseAccountData($user, $this->getUser($user));
474
	}
475
}
476