Completed
Push — master ( bedfb5...151b1b )
by Tom
19:02 queued 08:28
created

SyncService::run()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 5
nop 3
dl 0
loc 19
rs 9.4285
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 View Code Duplication
	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 View Code Duplication
	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
		} else {
200
			list($hasKey, $quota) = $this->readUserConfig($uid, 'files', 'quota');
201
			if ($hasKey) {
202
				$a->setQuota($quota);
203
			}
204
		}
205
		if (array_key_exists('quota', $a->getUpdatedFields())) {
206
			$this->logger->debug(
207
				"Setting quota for <$uid> to <$quota>", ['app' => self::class]
208
			);
209
		}
210
	}
211
212
	/**
213
	 * @param Account $a
214
	 * @param UserInterface $backend
215
	 */
216
	private function syncHome(Account $a, UserInterface $backend) {
217
		// Fallback for backends that dont yet use the new interfaces
218
		$proividesHome = $backend instanceof IProvidesHomeBackend || $backend->implementsActions(\OC_User_Backend::GET_HOME);
219
		$uid = $a->getUserId();
220
		// Log when the backend returns a string that is a different home to the current value
221
		if($proividesHome && is_string($backend->getHome($uid)) && $a->getHome() !== $backend->getHome($uid)) {
222
			$existing = $a->getHome();
223
			$backendHome = $backend->getHome($uid);
224
			$class = get_class($backend);
225
			if ($existing !== '') {
226
				$this->logger->error("User backend $class is returning home: $backendHome for user: $uid which differs from existing value: $existing");
227
			}
228
		}
229
		// Home is handled differently, it should only be set on account creation, when there is no home already set
230
		// Otherwise it could change on a sync and result in a new user folder being created
231
		if($a->getHome() === null) {
232
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
334
		// The account exists, sync
335
		$account = $this->syncAccount($account, $backend);
336
		if($account->getId() === null) {
337
			// New account, insert
338
			$this->mapper->insert($account);
339
		} else {
340
			$this->mapper->update($account);
341
		}
342
		return $account;
343
	}
344
345
	/**
346
	 * @param string $backend of the user
347
	 * @param string $uid of the user
348
	 * @return Account
349
	 */
350
	public function createNewAccount($backend, $uid) {
351
		$this->logger->info("Creating new account with UID $uid and backend $backend");
352
		$a = new Account();
353
		$a->setUserId($uid);
354
		$a->setState(Account::STATE_ENABLED);
355
		$a->setBackend($backend);
356
		return $a;
357
	}
358
359
	/**
360
	 * @param string $uid
361
	 * @param string $app
362
	 * @param string $key
363
	 * @return array
364
	 */
365
	private function readUserConfig($uid, $app, $key) {
366
		$keys = $this->config->getUserKeys($uid, $app);
367
		if (in_array($key, $keys, true)) {
368
			$enabled = $this->config->getUserValue($uid, $app, $key);
369
			return [true, $enabled];
370
		}
371
		return [false, null];
372
	}
373
374
	/**
375
	 * @param string $uid
376
	 */
377
	private function cleanPreferences($uid) {
378
		$this->config->deleteUserValue($uid, 'core', 'enabled');
379
		$this->config->deleteUserValue($uid, 'login', 'lastLogin');
380
		$this->config->deleteUserValue($uid, 'settings', 'email');
381
		$this->config->deleteUserValue($uid, 'files', 'quota');
382
	}
383
384
}
385