Passed
Push — master ( 365569...3ab659 )
by Roeland
25:34 queued 11s
created

AccountManager::updateUser()   F

Complexity

Conditions 21
Paths 238

Size

Total Lines 81
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 21
eloc 49
c 1
b 0
f 0
nc 238
nop 3
dl 0
loc 81
rs 2.8583

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		// set a max length
148
		foreach ($data as $propertyName => $propertyData) {
149
			if (isset($data[$propertyName]) && isset($data[$propertyName]['value']) && strlen($data[$propertyName]['value']) > 2048) {
150
				if ($throwOnData) {
151
					throw new \InvalidArgumentException($propertyName);
152
				} else {
153
					$data[$propertyName]['value'] = '';
154
				}
155
			}
156
		}
157
158
		$allowedScopes = [
159
			self::SCOPE_PRIVATE,
160
			self::SCOPE_LOCAL,
161
			self::SCOPE_FEDERATED,
162
			self::SCOPE_PUBLISHED,
163
			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

163
			/** @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...
164
			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

164
			/** @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...
165
			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

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

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

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