Completed
Push — master ( 40aac8...cfd797 )
by Jörn Friedrich
10:18
created

SyncService::syncAccount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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