SyncService::cleanPreferences()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
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 ($quota === null) {
227
			list($hasKey, $quota) = $this->readUserConfig($uid, 'files', 'quota');
228
			if ($hasKey) {
229
				$a->setQuota($quota);
230
			}
231
		}
232
		if (\array_key_exists('quota', $a->getUpdatedFields())) {
233
			$this->logger->debug(
234
				"Setting quota for <$uid> to <$quota>", ['app' => self::class]
235
			);
236
		}
237
	}
238
239
	/**
240
	 * @param Account $a
241
	 * @param UserInterface $backend
242
	 */
243
	private function syncHome(Account $a, UserInterface $backend) {
244
		// Fallback for backends that dont yet use the new interfaces
245
		$proividesHome = $backend instanceof IProvidesHomeBackend || $backend->implementsActions(\OC\User\Backend::GET_HOME);
246
		$uid = $a->getUserId();
247
		// Log when the backend returns a string that is a different home to the current value
248
		if ($proividesHome && \is_string($backend->getHome($uid)) && $a->getHome() !== $backend->getHome($uid)) {
249
			$existing = $a->getHome();
250
			$backendHome = $backend->getHome($uid);
251
			$class = \get_class($backend);
252
			if ($existing !== '') {
253
				$this->logger->error("User backend $class is returning home: $backendHome for user: $uid which differs from existing value: $existing");
254
			}
255
		}
256
		// Home is handled differently, it should only be set on account creation, when there is no home already set
257
		// Otherwise it could change on a sync and result in a new user folder being created
258
		if ($a->getHome() === null) {
259
			$home = false;
260
			if ($proividesHome) {
261
				$home = $backend->getHome($uid);
262
			}
263
			if (!\is_string($home) || $home[0] !== '/') {
264
				$home = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . "/$uid";
265
				$this->logger->debug(
266
					'User backend ' .\get_class($backend)." provided no home for <$uid>",
267
					['app' => self::class]
268
				);
269
			}
270
			// This will set the home if not provided by the backend
271
			$a->setHome($home);
272
			if (\array_key_exists('home', $a->getUpdatedFields())) {
273
				$this->logger->debug(
274
					"Setting home for <$uid> to <$home>", ['app' => self::class]
275
				);
276
			}
277
		}
278
	}
279
280
	/**
281
	 * @param Account $a
282
	 * @param UserInterface $backend
283
	 */
284
	private function syncDisplayName(Account $a, UserInterface $backend) {
285
		$uid = $a->getUserId();
286
		if ($backend instanceof IProvidesDisplayNameBackend || $backend->implementsActions(\OC\User\Backend::GET_DISPLAYNAME)) {
287
			$displayName = $backend->getDisplayName($uid);
288
			$a->setDisplayName($displayName);
289
			if (\array_key_exists('displayName', $a->getUpdatedFields())) {
290
				$this->logger->debug(
291
					"Setting displayName for <$uid> to <$displayName>", ['app' => self::class]
292
				);
293
			}
294
		}
295
	}
296
297
	/**
298
	 * TODO store username in account table instead of user preferences
299
	 *
300
	 * @param Account $a
301
	 * @param UserInterface $backend
302
	 */
303
	private function syncUserName(Account $a, UserInterface $backend) {
304
		$uid = $a->getUserId();
305
		if ($backend instanceof IProvidesUserNameBackend) {
306
			$userName = $backend->getUserName($uid);
307
			$currentUserName = $this->config->getUserValue($uid, 'core', 'username', null);
308
			if ($userName !== $currentUserName) {
309
				try {
310
					$this->config->setUserValue($uid, 'core', 'username', $userName);
311
				} catch (PreConditionNotMetException $e) {
312
					// ignore, because precondition is empty
313
				}
314
				$this->logger->debug(
315
					"Setting userName for <$uid> from <$currentUserName> to <$userName>", ['app' => self::class]
316
				);
317
			}
318
		}
319
	}
320
321
	/**
322
	 * @param Account $a
323
	 * @param UserInterface $backend
324
	 */
325
	private function syncSearchTerms(Account $a, UserInterface $backend) {
326
		$uid = $a->getUserId();
327
		if ($backend instanceof IProvidesExtendedSearchBackend) {
328
			$searchTerms = $backend->getSearchTerms($uid);
329
			$a->setSearchTerms($searchTerms);
330
			if ($a->haveTermsChanged()) {
331
				$logTerms = \implode('|', $searchTerms);
332
				$this->logger->debug(
333
					"Setting searchTerms for <$uid> to <$logTerms>", ['app' => self::class]
334
				);
335
			}
336
		}
337
	}
338
339
	/**
340
	 * @param Account $a
341
	 * @param UserInterface $backend of the user
342
	 * @return Account
343
	 */
344
	public function syncAccount(Account $a, UserInterface $backend) {
345
		$this->syncState($a);
346
		$this->syncLastLogin($a);
347
		$this->syncEmail($a, $backend);
348
		$this->syncQuota($a, $backend);
349
		$this->syncHome($a, $backend);
350
		$this->syncDisplayName($a, $backend);
351
		$this->syncUserName($a, $backend);
352
		$this->syncSearchTerms($a, $backend);
353
		return $a;
354
	}
355
356
	/**
357
	 * @param $uid
358
	 * @param UserInterface $backend
359
	 * @return Account
360
	 * @throws \Exception
361
	 * @throws \InvalidArgumentException if you try to sync with a backend
362
	 * that doesnt match an existing account
363
	 */
364
	public function createOrSyncAccount($uid, UserInterface $backend) {
365
		// Try to find the account based on the uid
366
		try {
367
			$account = $this->mapper->getByUid($uid);
368
			// Check the backend matches
369
			$existingAccountBackend = \get_class($backend);
370
			if ($account->getBackend() !== $existingAccountBackend) {
371
				$this->logger->warning(
372
					"User <$uid> already provided by another backend({$account->getBackend()} !== $existingAccountBackend), skipping.",
373
					['app' => self::class]
374
				);
375
				throw new \InvalidArgumentException('Returned account has different backend to the requested backend for sync');
376
			}
377
		} catch (DoesNotExistException $e) {
378
			// Create a new account for this uid and backend pairing and sync
379
			$account = $this->createNewAccount(\get_class($backend), $uid);
380
		} catch (MultipleObjectsReturnedException $e) {
381
			throw new \Exception("The database returned multiple accounts for this uid: $uid");
382
		}
383
384
		// The account exists, sync
385
		$account = $this->syncAccount($account, $backend);
386
		if ($account->getId() === null) {
387
			// New account, insert
388
			$this->mapper->insert($account);
389
		} else {
390
			$this->mapper->update($account);
391
		}
392
		return $account;
393
	}
394
395
	/**
396
	 * @param string $backend of the user
397
	 * @param string $uid of the user
398
	 * @return Account
399
	 */
400
	public function createNewAccount($backend, $uid) {
401
		$this->logger->info("Creating new account with UID $uid and backend $backend");
402
		$a = new Account();
403
		$a->setUserId($uid);
404
		$a->setState(Account::STATE_ENABLED);
405
		$a->setBackend($backend);
406
		return $a;
407
	}
408
409
	/**
410
	 * @param string $uid
411
	 * @param string $app
412
	 * @param string $key
413
	 * @return array
414
	 */
415
	private function readUserConfig($uid, $app, $key) {
416
		$keys = $this->config->getUserKeys($uid, $app);
417
		if (\in_array($key, $keys, true)) {
418
			$enabled = $this->config->getUserValue($uid, $app, $key);
419
			return [true, $enabled];
420
		}
421
		return [false, null];
422
	}
423
424
	/**
425
	 * @param string $uid
426
	 */
427
	private function cleanPreferences($uid) {
428
		$this->config->deleteUserValue($uid, 'core', 'enabled');
429
		$this->config->deleteUserValue($uid, 'login', 'lastLogin');
430
		$this->config->deleteUserValue($uid, 'settings', 'email');
431
		$this->config->deleteUserValue($uid, 'files', 'quota');
432
	}
433
}
434