Completed
Push — master ( 838909...74db22 )
by Phil
44s
created

SyncService::setAccountMapper()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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