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