Completed
Pull Request — master (#32731)
by Tom
11:14
created

SyncService   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 382
Duplicated Lines 6.54 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 25
loc 382
rs 4.5599
c 0
b 0
f 0
wmc 58
lcom 1
cbo 6

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A setAccountMapper() 0 3 1
A analyzeExistingUsers() 0 13 1
A checkIfAccountReappeared() 0 17 4
A run() 0 19 3
A syncState() 0 22 5
A syncLastLogin() 0 12 3
A syncEmail() 0 18 4
A syncQuota() 0 15 4
B syncHome() 0 36 11
A syncDisplayName() 12 12 4
A syncUserName() 0 17 4
A syncSearchTerms() 13 13 3
A syncAccount() 0 11 1
A createOrSyncAccount() 0 30 5
A createNewAccount() 0 8 1
A readUserConfig() 0 8 2
A cleanPreferences() 0 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SyncService 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 SyncService, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Jörn Friedrich Dreyer <[email protected]>
4
 * @author Thomas Müller <[email protected]>
5
 *
6
 * @copyright Copyright (c) 2018, ownCloud GmbH
7
 * @license AGPL-3.0
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
namespace OC\User;
23
24
use OCP\AppFramework\Db\DoesNotExistException;
25
use OCP\IConfig;
26
use OCP\ILogger;
27
use OCP\PreConditionNotMetException;
28
use OCP\User\IProvidesDisplayNameBackend;
29
use OCP\User\IProvidesEMailBackend;
30
use OCP\User\IProvidesExtendedSearchBackend;
31
use OCP\User\IProvidesHomeBackend;
32
use OCP\User\IProvidesQuotaBackend;
33
use OCP\User\IProvidesUserNameBackend;
34
use OCP\UserInterface;
35
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
36
37
/**
38
 * Class SyncService
39
 *
40
 * All users in a user backend are transferred into the account table.
41
 * In case a user is know all preferences will be transferred from the table
42
 * oc_preferences into the account table.
43
 *
44
 * @package OC\User
45
 */
46
class SyncService {
47
48
	/** @var IConfig */
49
	private $config;
50
	/** @var ILogger */
51
	private $logger;
52
	/** @var AccountMapper */
53
	private $mapper;
54
55
	/**
56
	 * SyncService constructor.
57
	 *
58
	 * @param IConfig $config
59
	 * @param ILogger $logger
60
	 * @param AccountMapper $mapper
61
	 */
62
	public function __construct(IConfig $config,
63
								ILogger $logger,
64
								AccountMapper $mapper) {
65
		$this->config = $config;
66
		$this->logger = $logger;
67
		$this->mapper = $mapper;
68
	}
69
70
	/**
71
	 * For unit tests
72
	 * @param AccountMapper $mapper
73
	 */
74
	public function setAccountMapper(AccountMapper $mapper) {
75
		$this->mapper = $mapper;
76
	}
77
78
	/**
79
	 * @param UserInterface $backend the backend to check
80
	 * @param \Closure $callback is called for every user to allow progress display
81
	 * @return array[] the first array contains a uid => account map of users that were removed in the external backend
82
	 *                 the second array contains a uid => account map of users that are not enabled in oc, but are available in the external backend
83
	 */
84
	public function analyzeExistingUsers(UserInterface $backend, \Closure $callback) {
85
		$removed = [];
86
		$reappeared = [];
87
		$backendClass = \get_class($backend);
88
		$this->mapper->callForAllUsers(function (Account $a) use (&$removed, &$reappeared, $backend, $backendClass, $callback) {
89
			// Check if the backend matches handles this user
90
			list($wasRemoved, $didReappear) = $this->checkIfAccountReappeared($a, $backend, $backendClass);
91
			$removed = \array_merge($removed, $wasRemoved);
92
			$reappeared = \array_merge($reappeared, $didReappear);
93
			$callback($a);
94
		}, '', false);
95
		return [$removed, $reappeared];
96
	}
97
98
	/**
99
	 * Checks a backend to see if a user reappeared relative to the accounts table
100
	 * @param Account $a
101
	 * @param UserInterface $backend
102
	 * @param $backendClass
103
	 * @return array
104
	 */
105
	private function checkIfAccountReappeared(Account $a, UserInterface $backend, $backendClass) {
106
		$removed = [];
107
		$reappeared = [];
108
		if ($a->getBackend() === $backendClass) {
109
			// Does the backend have this user still
110
			if ($backend->userExists($a->getUserId())) {
111
				// Is the user not enabled currently?
112
				if ($a->getState() !== Account::STATE_ENABLED) {
113
					$reappeared[$a->getUserId()] = $a;
114
				}
115
			} else {
116
				// The backend no longer has this user
117
				$removed[$a->getUserId()] = $a;
118
			}
119
		}
120
		return [$removed, $reappeared];
121
	}
122
123
	/**
124
	 * @param UserInterface $backend to sync
125
	 * @param \Traversable $userIds of users
126
	 * @param \Closure $callback is called for every user to progress display
127
	 */
128
	public function run(UserInterface $backend, \Traversable $userIds, \Closure $callback) {
129
		// update existing and insert new users
130
		foreach ($userIds as $uid) {
131
			try {
132
				$account = $this->createOrSyncAccount($uid, $backend);
133
				$uid = $account->getUserId(); // get correct case
134
				// clean the user's preferences
135
				$this->cleanPreferences($uid);
136
			} catch (\Exception $e) {
137
				// Error syncing this user
138
				$backendClass = \get_class($backend);
139
				$this->logger->error("Error syncing user with uid: $uid and backend: $backendClass");
140
				$this->logger->logException($e);
141
			}
142
143
			// call the callback
144
			$callback($uid);
145
		}
146
	}
147
148
	/**
149
	 * @param Account $a
150
	 */
151
	private function syncState(Account $a) {
152
		$uid = $a->getUserId();
153
		list($hasKey, $value) = $this->readUserConfig($uid, 'core', 'enabled');
154
		if ($hasKey) {
155
			if ($value === 'true') {
156
				$a->setState(Account::STATE_ENABLED);
157
			} else {
158
				$a->setState(Account::STATE_DISABLED);
159
			}
160
			if (\array_key_exists('state', $a->getUpdatedFields())) {
161
				if ($value === 'true') {
162
					$this->logger->debug(
163
						"Enabling <$uid>", ['app' => self::class]
164
					);
165
				} else {
166
					$this->logger->debug(
167
						"Disabling <$uid>", ['app' => self::class]
168
					);
169
				}
170
			}
171
		}
172
	}
173
174
	/**
175
	 * @param Account $a
176
	 */
177
	private function syncLastLogin(Account $a) {
178
		$uid = $a->getUserId();
179
		list($hasKey, $value) = $this->readUserConfig($uid, 'login', 'lastLogin');
180
		if ($hasKey) {
181
			$a->setLastLogin($value);
182
			if (\array_key_exists('lastLogin', $a->getUpdatedFields())) {
183
				$this->logger->debug(
184
					"Setting lastLogin for <$uid> to <$value>", ['app' => self::class]
185
				);
186
			}
187
		}
188
	}
189
190
	/**
191
	 * @param Account $a
192
	 * @param UserInterface $backend
193
	 */
194
	private function syncEmail(Account $a, UserInterface $backend) {
195
		$uid = $a->getUserId();
196
		$email = null;
197
		if ($backend instanceof IProvidesEMailBackend) {
198
			$email = $backend->getEMailAddress($uid);
199
			$a->setEmail($email);
200
		} else {
201
			list($hasKey, $email) = $this->readUserConfig($uid, 'settings', 'email');
202
			if ($hasKey) {
203
				$a->setEmail($email);
204
			}
205
		}
206
		if (\array_key_exists('email', $a->getUpdatedFields())) {
207
			$this->logger->debug(
208
				"Setting email for <$uid> to <$email>", ['app' => self::class]
209
			);
210
		}
211
	}
212
213
	/**
214
	 * @param Account $a
215
	 * @param UserInterface $backend
216
	 */
217
	private function syncQuota(Account $a, UserInterface $backend) {
218
		$uid = $a->getUserId();
219
		$quota = null;
220
		if ($backend instanceof IProvidesQuotaBackend) {
221
			$quota = $backend->getQuota($uid);
222
			if ($quota !== null) {
223
				$a->setQuota($quota);
224
			}
225
		}
226
		if (\array_key_exists('quota', $a->getUpdatedFields())) {
227
			$this->logger->debug(
228
				"Setting quota for <$uid> to <$quota>", ['app' => self::class]
229
			);
230
		}
231
	}
232
233
	/**
234
	 * @param Account $a
235
	 * @param UserInterface $backend
236
	 */
237
	private function syncHome(Account $a, UserInterface $backend) {
238
		// Fallback for backends that dont yet use the new interfaces
239
		$proividesHome = $backend instanceof IProvidesHomeBackend || $backend->implementsActions(\OC_User_Backend::GET_HOME);
240
		$uid = $a->getUserId();
241
		// Log when the backend returns a string that is a different home to the current value
242
		if ($proividesHome && \is_string($backend->getHome($uid)) && $a->getHome() !== $backend->getHome($uid)) {
243
			$existing = $a->getHome();
244
			$backendHome = $backend->getHome($uid);
245
			$class = \get_class($backend);
246
			if ($existing !== '') {
247
				$this->logger->error("User backend $class is returning home: $backendHome for user: $uid which differs from existing value: $existing");
248
			}
249
		}
250
		// Home is handled differently, it should only be set on account creation, when there is no home already set
251
		// Otherwise it could change on a sync and result in a new user folder being created
252
		if ($a->getHome() === null) {
253
			$home = false;
254
			if ($proividesHome) {
255
				$home = $backend->getHome($uid);
256
			}
257
			if (!\is_string($home) || $home[0] !== '/') {
258
				$home = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . "/$uid";
259
				$this->logger->debug(
260
					'User backend ' .\get_class($backend)." provided no home for <$uid>",
261
					['app' => self::class]
262
				);
263
			}
264
			// This will set the home if not provided by the backend
265
			$a->setHome($home);
266
			if (\array_key_exists('home', $a->getUpdatedFields())) {
267
				$this->logger->debug(
268
					"Setting home for <$uid> to <$home>", ['app' => self::class]
269
				);
270
			}
271
		}
272
	}
273
274
	/**
275
	 * @param Account $a
276
	 * @param UserInterface $backend
277
	 */
278 View Code Duplication
	private function syncDisplayName(Account $a, UserInterface $backend) {
279
		$uid = $a->getUserId();
280
		if ($backend instanceof IProvidesDisplayNameBackend || $backend->implementsActions(\OC_User_Backend::GET_DISPLAYNAME)) {
281
			$displayName = $backend->getDisplayName($uid);
282
			$a->setDisplayName($displayName);
283
			if (\array_key_exists('displayName', $a->getUpdatedFields())) {
284
				$this->logger->debug(
285
					"Setting displayName for <$uid> to <$displayName>", ['app' => self::class]
286
				);
287
			}
288
		}
289
	}
290
291
	/**
292
	 * TODO store username in account table instead of user preferences
293
	 *
294
	 * @param Account $a
295
	 * @param UserInterface $backend
296
	 */
297
	private function syncUserName(Account $a, UserInterface $backend) {
298
		$uid = $a->getUserId();
299
		if ($backend instanceof IProvidesUserNameBackend) {
300
			$userName = $backend->getUserName($uid);
301
			$currentUserName = $this->config->getUserValue($uid, 'core', 'username', null);
302
			if ($userName !== $currentUserName) {
303
				try {
304
					$this->config->setUserValue($uid, 'core', 'username', $userName);
305
				} catch (PreConditionNotMetException $e) {
306
					// ignore, because precondition is empty
307
				}
308
				$this->logger->debug(
309
					"Setting userName for <$uid> from <$currentUserName> to <$userName>", ['app' => self::class]
310
				);
311
			}
312
		}
313
	}
314
315
	/**
316
	 * @param Account $a
317
	 * @param UserInterface $backend
318
	 */
319 View Code Duplication
	private function syncSearchTerms(Account $a, UserInterface $backend) {
320
		$uid = $a->getUserId();
321
		if ($backend instanceof IProvidesExtendedSearchBackend) {
322
			$searchTerms = $backend->getSearchTerms($uid);
323
			$a->setSearchTerms($searchTerms);
324
			if ($a->haveTermsChanged()) {
325
				$logTerms = \implode('|', $searchTerms);
326
				$this->logger->debug(
327
					"Setting searchTerms for <$uid> to <$logTerms>", ['app' => self::class]
328
				);
329
			}
330
		}
331
	}
332
333
	/**
334
	 * @param Account $a
335
	 * @param UserInterface $backend of the user
336
	 * @return Account
337
	 */
338
	public function syncAccount(Account $a, UserInterface $backend) {
339
		$this->syncState($a);
340
		$this->syncLastLogin($a);
341
		$this->syncEmail($a, $backend);
342
		$this->syncQuota($a, $backend);
343
		$this->syncHome($a, $backend);
344
		$this->syncDisplayName($a, $backend);
345
		$this->syncUserName($a, $backend);
346
		$this->syncSearchTerms($a, $backend);
347
		return $a;
348
	}
349
350
	/**
351
	 * @param $uid
352
	 * @param UserInterface $backend
353
	 * @return Account
354
	 * @throws \Exception
355
	 * @throws \InvalidArgumentException if you try to sync with a backend
356
	 * that doesnt match an existing account
357
	 */
358
	public function createOrSyncAccount($uid, UserInterface $backend) {
359
		// Try to find the account based on the uid
360
		try {
361
			$account = $this->mapper->getByUid($uid);
362
			// Check the backend matches
363
			$existingAccountBackend = \get_class($backend);
364
			if ($account->getBackend() !== $existingAccountBackend) {
365
				$this->logger->warning(
366
					"User <$uid> already provided by another backend({$account->getBackend()} !== $existingAccountBackend), skipping.",
367
					['app' => self::class]
368
				);
369
				throw new \InvalidArgumentException('Returned account has different backend to the requested backend for sync');
370
			}
371
		} catch (DoesNotExistException $e) {
372
			// Create a new account for this uid and backend pairing and sync
373
			$account = $this->createNewAccount(\get_class($backend), $uid);
374
		} catch (MultipleObjectsReturnedException $e) {
375
			throw new \Exception("The database returned multiple accounts for this uid: $uid");
376
		}
377
378
		// The account exists, sync
379
		$account = $this->syncAccount($account, $backend);
380
		if ($account->getId() === null) {
381
			// New account, insert
382
			$this->mapper->insert($account);
383
		} else {
384
			$this->mapper->update($account);
385
		}
386
		return $account;
387
	}
388
389
	/**
390
	 * @param string $backend of the user
391
	 * @param string $uid of the user
392
	 * @return Account
393
	 */
394
	public function createNewAccount($backend, $uid) {
395
		$this->logger->info("Creating new account with UID $uid and backend $backend");
396
		$a = new Account();
397
		$a->setUserId($uid);
398
		$a->setState(Account::STATE_ENABLED);
399
		$a->setBackend($backend);
400
		return $a;
401
	}
402
403
	/**
404
	 * @param string $uid
405
	 * @param string $app
406
	 * @param string $key
407
	 * @return array
408
	 */
409
	private function readUserConfig($uid, $app, $key) {
410
		$keys = $this->config->getUserKeys($uid, $app);
411
		if (\in_array($key, $keys, true)) {
412
			$enabled = $this->config->getUserValue($uid, $app, $key);
413
			return [true, $enabled];
414
		}
415
		return [false, null];
416
	}
417
418
	/**
419
	 * @param string $uid
420
	 */
421
	private function cleanPreferences($uid) {
422
		$this->config->deleteUserValue($uid, 'core', 'enabled');
423
		$this->config->deleteUserValue($uid, 'login', 'lastLogin');
424
		$this->config->deleteUserValue($uid, 'settings', 'email');
425
		$this->config->deleteUserValue($uid, 'files', 'quota');
426
	}
427
}
428