Completed
Push — master ( 926271...fa5020 )
by Thomas
14:41
created

SyncService::checkIfAccountReappeared()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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