Passed
Push — master ( f016cb...bbc64c )
by Julius
13:29 queued 10s
created
lib/public/Accounts/IAccountManager.php 1 patch
Indentation   +92 added lines, -92 removed lines patch added patch discarded remove patch
@@ -38,96 +38,96 @@
 block discarded – undo
38 38
  */
39 39
 interface IAccountManager {
40 40
 
41
-	/**
42
-	 * Contact details visible locally only
43
-	 *
44
-	 * @since 21.0.1
45
-	 */
46
-	public const SCOPE_PRIVATE = 'v2-private';
47
-
48
-	/**
49
-	 * Contact details visible locally and through public link access on local instance
50
-	 *
51
-	 * @since 21.0.1
52
-	 */
53
-	public const SCOPE_LOCAL = 'v2-local';
54
-
55
-	/**
56
-	 * Contact details visible locally, through public link access and on trusted federated servers.
57
-	 *
58
-	 * @since 21.0.1
59
-	 */
60
-	public const SCOPE_FEDERATED = 'v2-federated';
61
-
62
-	/**
63
-	 * Contact details visible locally, through public link access, on trusted federated servers
64
-	 * and published to the public lookup server.
65
-	 *
66
-	 * @since 21.0.1
67
-	 */
68
-	public const SCOPE_PUBLISHED = 'v2-published';
69
-
70
-	/**
71
-	 * Contact details only visible locally
72
-	 *
73
-	 * @deprecated 21.0.1
74
-	 */
75
-	public const VISIBILITY_PRIVATE = 'private';
76
-
77
-	/**
78
-	 * Contact details visible on trusted federated servers.
79
-	 *
80
-	 * @deprecated 21.0.1
81
-	 */
82
-	public const VISIBILITY_CONTACTS_ONLY = 'contacts';
83
-
84
-	/**
85
-	 * Contact details visible on trusted federated servers and in the public lookup server.
86
-	 *
87
-	 * @deprecated 21.0.1
88
-	 */
89
-	public const VISIBILITY_PUBLIC = 'public';
90
-
91
-	public const PROPERTY_AVATAR = 'avatar';
92
-	public const PROPERTY_DISPLAYNAME = 'displayname';
93
-	public const PROPERTY_PHONE = 'phone';
94
-	public const PROPERTY_EMAIL = 'email';
95
-	public const PROPERTY_WEBSITE = 'website';
96
-	public const PROPERTY_ADDRESS = 'address';
97
-	public const PROPERTY_TWITTER = 'twitter';
98
-
99
-	public const NOT_VERIFIED = '0';
100
-	public const VERIFICATION_IN_PROGRESS = '1';
101
-	public const VERIFIED = '2';
102
-
103
-	/**
104
-	 * Get the account data for a given user
105
-	 *
106
-	 * @since 15.0.0
107
-	 *
108
-	 * @param IUser $user
109
-	 * @return IAccount
110
-	 */
111
-	public function getAccount(IUser $user): IAccount;
112
-
113
-	/**
114
-	 * Update the account data with for the user
115
-	 *
116
-	 * @since 21.0.1
117
-	 *
118
-	 * @param IAccount $account
119
-	 * @throws \InvalidArgumentException Message is the property that was invalid
120
-	 */
121
-	public function updateAccount(IAccount $account): void;
122
-
123
-	/**
124
-	 * Search for users based on account data
125
-	 *
126
-	 * @param string $property
127
-	 * @param string[] $values
128
-	 * @return array
129
-	 *
130
-	 * @since 21.0.0
131
-	 */
132
-	public function searchUsers(string $property, array $values): array;
41
+    /**
42
+     * Contact details visible locally only
43
+     *
44
+     * @since 21.0.1
45
+     */
46
+    public const SCOPE_PRIVATE = 'v2-private';
47
+
48
+    /**
49
+     * Contact details visible locally and through public link access on local instance
50
+     *
51
+     * @since 21.0.1
52
+     */
53
+    public const SCOPE_LOCAL = 'v2-local';
54
+
55
+    /**
56
+     * Contact details visible locally, through public link access and on trusted federated servers.
57
+     *
58
+     * @since 21.0.1
59
+     */
60
+    public const SCOPE_FEDERATED = 'v2-federated';
61
+
62
+    /**
63
+     * Contact details visible locally, through public link access, on trusted federated servers
64
+     * and published to the public lookup server.
65
+     *
66
+     * @since 21.0.1
67
+     */
68
+    public const SCOPE_PUBLISHED = 'v2-published';
69
+
70
+    /**
71
+     * Contact details only visible locally
72
+     *
73
+     * @deprecated 21.0.1
74
+     */
75
+    public const VISIBILITY_PRIVATE = 'private';
76
+
77
+    /**
78
+     * Contact details visible on trusted federated servers.
79
+     *
80
+     * @deprecated 21.0.1
81
+     */
82
+    public const VISIBILITY_CONTACTS_ONLY = 'contacts';
83
+
84
+    /**
85
+     * Contact details visible on trusted federated servers and in the public lookup server.
86
+     *
87
+     * @deprecated 21.0.1
88
+     */
89
+    public const VISIBILITY_PUBLIC = 'public';
90
+
91
+    public const PROPERTY_AVATAR = 'avatar';
92
+    public const PROPERTY_DISPLAYNAME = 'displayname';
93
+    public const PROPERTY_PHONE = 'phone';
94
+    public const PROPERTY_EMAIL = 'email';
95
+    public const PROPERTY_WEBSITE = 'website';
96
+    public const PROPERTY_ADDRESS = 'address';
97
+    public const PROPERTY_TWITTER = 'twitter';
98
+
99
+    public const NOT_VERIFIED = '0';
100
+    public const VERIFICATION_IN_PROGRESS = '1';
101
+    public const VERIFIED = '2';
102
+
103
+    /**
104
+     * Get the account data for a given user
105
+     *
106
+     * @since 15.0.0
107
+     *
108
+     * @param IUser $user
109
+     * @return IAccount
110
+     */
111
+    public function getAccount(IUser $user): IAccount;
112
+
113
+    /**
114
+     * Update the account data with for the user
115
+     *
116
+     * @since 21.0.1
117
+     *
118
+     * @param IAccount $account
119
+     * @throws \InvalidArgumentException Message is the property that was invalid
120
+     */
121
+    public function updateAccount(IAccount $account): void;
122
+
123
+    /**
124
+     * Search for users based on account data
125
+     *
126
+     * @param string $property
127
+     * @param string[] $values
128
+     * @return array
129
+     *
130
+     * @since 21.0.0
131
+     */
132
+    public function searchUsers(string $property, array $values): array;
133 133
 }
Please login to merge, or discard this patch.
lib/private/Accounts/AccountManager.php 1 patch
Indentation   +466 added lines, -466 removed lines patch added patch discarded remove patch
@@ -58,470 +58,470 @@
 block discarded – undo
58 58
  */
59 59
 class AccountManager implements IAccountManager {
60 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) {
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,
153
-			self::VISIBILITY_CONTACTS_ONLY,
154
-			self::VISIBILITY_PUBLIC,
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',
197
-				new GenericEvent($user, $data)
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
-	public function updateAccount(IAccount $account): void {
515
-		$data = [];
516
-
517
-		foreach ($account->getProperties() as $property) {
518
-			$data[$property->getName()] = [
519
-				'value' => $property->getValue(),
520
-				'scope' => $property->getScope(),
521
-				'verified' => $property->getVerified(),
522
-			];
523
-		}
524
-
525
-		$this->updateUser($account->getUser(), $data, true);
526
-	}
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) {
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,
153
+            self::VISIBILITY_CONTACTS_ONLY,
154
+            self::VISIBILITY_PUBLIC,
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',
197
+                new GenericEvent($user, $data)
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
+    public function updateAccount(IAccount $account): void {
515
+        $data = [];
516
+
517
+        foreach ($account->getProperties() as $property) {
518
+            $data[$property->getName()] = [
519
+                'value' => $property->getValue(),
520
+                'scope' => $property->getScope(),
521
+                'verified' => $property->getVerified(),
522
+            ];
523
+        }
524
+
525
+        $this->updateUser($account->getUser(), $data, true);
526
+    }
527 527
 }
Please login to merge, or discard this patch.