Completed
Push — master ( e4992c...6d0a35 )
by
unknown
10:42
created

SyncService::syncAccount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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