Passed
Push — master ( f1e2fb...5938da )
by Robin
13:41 queued 10s
created
lib/private/User/Manager.php 1 patch
Indentation   +623 added lines, -623 removed lines patch added patch discarded remove patch
@@ -70,627 +70,627 @@
 block discarded – undo
70 70
  * @package OC\User
71 71
  */
72 72
 class Manager extends PublicEmitter implements IUserManager {
73
-	/**
74
-	 * @var \OCP\UserInterface[] $backends
75
-	 */
76
-	private $backends = [];
77
-
78
-	/**
79
-	 * @var \OC\User\User[] $cachedUsers
80
-	 */
81
-	private $cachedUsers = [];
82
-
83
-	/** @var IConfig */
84
-	private $config;
85
-
86
-	/** @var EventDispatcherInterface */
87
-	private $dispatcher;
88
-
89
-	/** @var ICache */
90
-	private $cache;
91
-
92
-	/** @var IEventDispatcher */
93
-	private $eventDispatcher;
94
-
95
-	public function __construct(IConfig $config,
96
-								EventDispatcherInterface $oldDispatcher,
97
-								ICacheFactory $cacheFactory,
98
-								IEventDispatcher $eventDispatcher) {
99
-		$this->config = $config;
100
-		$this->dispatcher = $oldDispatcher;
101
-		$this->cache = $cacheFactory->createDistributed('user_backend_map');
102
-		$cachedUsers = &$this->cachedUsers;
103
-		$this->listen('\OC\User', 'postDelete', function ($user) use (&$cachedUsers) {
104
-			/** @var \OC\User\User $user */
105
-			unset($cachedUsers[$user->getUID()]);
106
-		});
107
-		$this->eventDispatcher = $eventDispatcher;
108
-	}
109
-
110
-	/**
111
-	 * Get the active backends
112
-	 * @return \OCP\UserInterface[]
113
-	 */
114
-	public function getBackends() {
115
-		return $this->backends;
116
-	}
117
-
118
-	/**
119
-	 * register a user backend
120
-	 *
121
-	 * @param \OCP\UserInterface $backend
122
-	 */
123
-	public function registerBackend($backend) {
124
-		$this->backends[] = $backend;
125
-	}
126
-
127
-	/**
128
-	 * remove a user backend
129
-	 *
130
-	 * @param \OCP\UserInterface $backend
131
-	 */
132
-	public function removeBackend($backend) {
133
-		$this->cachedUsers = [];
134
-		if (($i = array_search($backend, $this->backends)) !== false) {
135
-			unset($this->backends[$i]);
136
-		}
137
-	}
138
-
139
-	/**
140
-	 * remove all user backends
141
-	 */
142
-	public function clearBackends() {
143
-		$this->cachedUsers = [];
144
-		$this->backends = [];
145
-	}
146
-
147
-	/**
148
-	 * get a user by user id
149
-	 *
150
-	 * @param string $uid
151
-	 * @return \OC\User\User|null Either the user or null if the specified user does not exist
152
-	 */
153
-	public function get($uid) {
154
-		if (is_null($uid) || $uid === '' || $uid === false) {
155
-			return null;
156
-		}
157
-		if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
158
-			return $this->cachedUsers[$uid];
159
-		}
160
-
161
-		$cachedBackend = $this->cache->get($uid);
162
-		if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
163
-			// Cache has the info of the user backend already, so ask that one directly
164
-			$backend = $this->backends[$cachedBackend];
165
-			if ($backend->userExists($uid)) {
166
-				return $this->getUserObject($uid, $backend);
167
-			}
168
-		}
169
-
170
-		foreach ($this->backends as $i => $backend) {
171
-			if ($i === $cachedBackend) {
172
-				// Tried that one already
173
-				continue;
174
-			}
175
-
176
-			if ($backend->userExists($uid)) {
177
-				$this->cache->set($uid, $i, 300);
178
-				return $this->getUserObject($uid, $backend);
179
-			}
180
-		}
181
-		return null;
182
-	}
183
-
184
-	/**
185
-	 * get or construct the user object
186
-	 *
187
-	 * @param string $uid
188
-	 * @param \OCP\UserInterface $backend
189
-	 * @param bool $cacheUser If false the newly created user object will not be cached
190
-	 * @return \OC\User\User
191
-	 */
192
-	protected function getUserObject($uid, $backend, $cacheUser = true) {
193
-		if ($backend instanceof IGetRealUIDBackend) {
194
-			$uid = $backend->getRealUID($uid);
195
-		}
196
-
197
-		if (isset($this->cachedUsers[$uid])) {
198
-			return $this->cachedUsers[$uid];
199
-		}
200
-
201
-		$user = new User($uid, $backend, $this->dispatcher, $this, $this->config);
202
-		if ($cacheUser) {
203
-			$this->cachedUsers[$uid] = $user;
204
-		}
205
-		return $user;
206
-	}
207
-
208
-	/**
209
-	 * check if a user exists
210
-	 *
211
-	 * @param string $uid
212
-	 * @return bool
213
-	 */
214
-	public function userExists($uid) {
215
-		$user = $this->get($uid);
216
-		return ($user !== null);
217
-	}
218
-
219
-	/**
220
-	 * Check if the password is valid for the user
221
-	 *
222
-	 * @param string $loginName
223
-	 * @param string $password
224
-	 * @return mixed the User object on success, false otherwise
225
-	 */
226
-	public function checkPassword($loginName, $password) {
227
-		$result = $this->checkPasswordNoLogging($loginName, $password);
228
-
229
-		if ($result === false) {
230
-			\OC::$server->getLogger()->warning('Login failed: \''. $loginName .'\' (Remote IP: \''. \OC::$server->getRequest()->getRemoteAddress(). '\')', ['app' => 'core']);
231
-		}
232
-
233
-		return $result;
234
-	}
235
-
236
-	/**
237
-	 * Check if the password is valid for the user
238
-	 *
239
-	 * @internal
240
-	 * @param string $loginName
241
-	 * @param string $password
242
-	 * @return IUser|false the User object on success, false otherwise
243
-	 */
244
-	public function checkPasswordNoLogging($loginName, $password) {
245
-		$loginName = str_replace("\0", '', $loginName);
246
-		$password = str_replace("\0", '', $password);
247
-
248
-		foreach ($this->backends as $backend) {
249
-			if ($backend->implementsActions(Backend::CHECK_PASSWORD)) {
250
-				$uid = $backend->checkPassword($loginName, $password);
251
-				if ($uid !== false) {
252
-					return $this->getUserObject($uid, $backend);
253
-				}
254
-			}
255
-		}
256
-
257
-		// since http basic auth doesn't provide a standard way of handling non ascii password we allow password to be urlencoded
258
-		// we only do this decoding after using the plain password fails to maintain compatibility with any password that happens
259
-		// to contains urlencoded patterns by "accident".
260
-		$password = urldecode($password);
261
-
262
-		foreach ($this->backends as $backend) {
263
-			if ($backend->implementsActions(Backend::CHECK_PASSWORD)) {
264
-				$uid = $backend->checkPassword($loginName, $password);
265
-				if ($uid !== false) {
266
-					return $this->getUserObject($uid, $backend);
267
-				}
268
-			}
269
-		}
270
-
271
-		return false;
272
-	}
273
-
274
-	/**
275
-	 * search by user id
276
-	 *
277
-	 * @param string $pattern
278
-	 * @param int $limit
279
-	 * @param int $offset
280
-	 * @return \OC\User\User[]
281
-	 */
282
-	public function search($pattern, $limit = null, $offset = null) {
283
-		$users = [];
284
-		foreach ($this->backends as $backend) {
285
-			$backendUsers = $backend->getUsers($pattern, $limit, $offset);
286
-			if (is_array($backendUsers)) {
287
-				foreach ($backendUsers as $uid) {
288
-					$users[$uid] = $this->getUserObject($uid, $backend);
289
-				}
290
-			}
291
-		}
292
-
293
-		uasort($users, function ($a, $b) {
294
-			/**
295
-			 * @var \OC\User\User $a
296
-			 * @var \OC\User\User $b
297
-			 */
298
-			return strcasecmp($a->getUID(), $b->getUID());
299
-		});
300
-		return $users;
301
-	}
302
-
303
-	/**
304
-	 * search by displayName
305
-	 *
306
-	 * @param string $pattern
307
-	 * @param int $limit
308
-	 * @param int $offset
309
-	 * @return \OC\User\User[]
310
-	 */
311
-	public function searchDisplayName($pattern, $limit = null, $offset = null) {
312
-		$users = [];
313
-		foreach ($this->backends as $backend) {
314
-			$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
315
-			if (is_array($backendUsers)) {
316
-				foreach ($backendUsers as $uid => $displayName) {
317
-					$users[] = $this->getUserObject($uid, $backend);
318
-				}
319
-			}
320
-		}
321
-
322
-		usort($users, function ($a, $b) {
323
-			/**
324
-			 * @var \OC\User\User $a
325
-			 * @var \OC\User\User $b
326
-			 */
327
-			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
328
-		});
329
-		return $users;
330
-	}
331
-
332
-	/**
333
-	 * @param string $uid
334
-	 * @param string $password
335
-	 * @throws \InvalidArgumentException
336
-	 * @return bool|IUser the created user or false
337
-	 */
338
-	public function createUser($uid, $password) {
339
-		// DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
340
-		if (\OC::$server->get(IRegistry::class)->delegateIsHardUserLimitReached()) {
341
-			$l = \OC::$server->getL10N('lib');
342
-			throw new HintException($l->t('The user limit has been reached and the user was not created.'));
343
-		}
344
-
345
-		$localBackends = [];
346
-		foreach ($this->backends as $backend) {
347
-			if ($backend instanceof Database) {
348
-				// First check if there is another user backend
349
-				$localBackends[] = $backend;
350
-				continue;
351
-			}
352
-
353
-			if ($backend->implementsActions(Backend::CREATE_USER)) {
354
-				return $this->createUserFromBackend($uid, $password, $backend);
355
-			}
356
-		}
357
-
358
-		foreach ($localBackends as $backend) {
359
-			if ($backend->implementsActions(Backend::CREATE_USER)) {
360
-				return $this->createUserFromBackend($uid, $password, $backend);
361
-			}
362
-		}
363
-
364
-		return false;
365
-	}
366
-
367
-	/**
368
-	 * @param string $uid
369
-	 * @param string $password
370
-	 * @param UserInterface $backend
371
-	 * @return IUser|null
372
-	 * @throws \InvalidArgumentException
373
-	 */
374
-	public function createUserFromBackend($uid, $password, UserInterface $backend) {
375
-		$l = \OC::$server->getL10N('lib');
376
-
377
-		// Check the name for bad characters
378
-		// Allowed are: "a-z", "A-Z", "0-9" and "_.@-'"
379
-		if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) {
380
-			throw new \InvalidArgumentException($l->t('Only the following characters are allowed in a username:'
381
-				. ' "a-z", "A-Z", "0-9", and "_.@-\'"'));
382
-		}
383
-
384
-		// No empty username
385
-		if (trim($uid) === '') {
386
-			throw new \InvalidArgumentException($l->t('A valid username must be provided'));
387
-		}
388
-
389
-		// No whitespace at the beginning or at the end
390
-		if (trim($uid) !== $uid) {
391
-			throw new \InvalidArgumentException($l->t('Username contains whitespace at the beginning or at the end'));
392
-		}
393
-
394
-		// Username only consists of 1 or 2 dots (directory traversal)
395
-		if ($uid === '.' || $uid === '..') {
396
-			throw new \InvalidArgumentException($l->t('Username must not consist of dots only'));
397
-		}
398
-
399
-		if (!$this->verifyUid($uid)) {
400
-			throw new \InvalidArgumentException($l->t('Username is invalid because files already exist for this user'));
401
-		}
402
-
403
-		// No empty password
404
-		if (trim($password) === '') {
405
-			throw new \InvalidArgumentException($l->t('A valid password must be provided'));
406
-		}
407
-
408
-		// Check if user already exists
409
-		if ($this->userExists($uid)) {
410
-			throw new \InvalidArgumentException($l->t('The username is already being used'));
411
-		}
412
-
413
-		/** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
414
-		$this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
415
-		$this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
416
-		$state = $backend->createUser($uid, $password);
417
-		if ($state === false) {
418
-			throw new \InvalidArgumentException($l->t('Could not create user'));
419
-		}
420
-		$user = $this->getUserObject($uid, $backend);
421
-		if ($user instanceof IUser) {
422
-			/** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
423
-			$this->emit('\OC\User', 'postCreateUser', [$user, $password]);
424
-			$this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
425
-		}
426
-		return $user;
427
-	}
428
-
429
-	/**
430
-	 * returns how many users per backend exist (if supported by backend)
431
-	 *
432
-	 * @param boolean $hasLoggedIn when true only users that have a lastLogin
433
-	 *                entry in the preferences table will be affected
434
-	 * @return array|int an array of backend class as key and count number as value
435
-	 *                if $hasLoggedIn is true only an int is returned
436
-	 */
437
-	public function countUsers($hasLoggedIn = false) {
438
-		if ($hasLoggedIn) {
439
-			return $this->countSeenUsers();
440
-		}
441
-		$userCountStatistics = [];
442
-		foreach ($this->backends as $backend) {
443
-			if ($backend->implementsActions(Backend::COUNT_USERS)) {
444
-				$backendUsers = $backend->countUsers();
445
-				if ($backendUsers !== false) {
446
-					if ($backend instanceof IUserBackend) {
447
-						$name = $backend->getBackendName();
448
-					} else {
449
-						$name = get_class($backend);
450
-					}
451
-					if (isset($userCountStatistics[$name])) {
452
-						$userCountStatistics[$name] += $backendUsers;
453
-					} else {
454
-						$userCountStatistics[$name] = $backendUsers;
455
-					}
456
-				}
457
-			}
458
-		}
459
-		return $userCountStatistics;
460
-	}
461
-
462
-	/**
463
-	 * returns how many users per backend exist in the requested groups (if supported by backend)
464
-	 *
465
-	 * @param IGroup[] $groups an array of gid to search in
466
-	 * @return array|int an array of backend class as key and count number as value
467
-	 *                if $hasLoggedIn is true only an int is returned
468
-	 */
469
-	public function countUsersOfGroups(array $groups) {
470
-		$users = [];
471
-		foreach ($groups as $group) {
472
-			$usersIds = array_map(function ($user) {
473
-				return $user->getUID();
474
-			}, $group->getUsers());
475
-			$users = array_merge($users, $usersIds);
476
-		}
477
-		return count(array_unique($users));
478
-	}
479
-
480
-	/**
481
-	 * The callback is executed for each user on each backend.
482
-	 * If the callback returns false no further users will be retrieved.
483
-	 *
484
-	 * @param \Closure $callback
485
-	 * @param string $search
486
-	 * @param boolean $onlySeen when true only users that have a lastLogin entry
487
-	 *                in the preferences table will be affected
488
-	 * @since 9.0.0
489
-	 */
490
-	public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
491
-		if ($onlySeen) {
492
-			$this->callForSeenUsers($callback);
493
-		} else {
494
-			foreach ($this->getBackends() as $backend) {
495
-				$limit = 500;
496
-				$offset = 0;
497
-				do {
498
-					$users = $backend->getUsers($search, $limit, $offset);
499
-					foreach ($users as $uid) {
500
-						if (!$backend->userExists($uid)) {
501
-							continue;
502
-						}
503
-						$user = $this->getUserObject($uid, $backend, false);
504
-						$return = $callback($user);
505
-						if ($return === false) {
506
-							break;
507
-						}
508
-					}
509
-					$offset += $limit;
510
-				} while (count($users) >= $limit);
511
-			}
512
-		}
513
-	}
514
-
515
-	/**
516
-	 * returns how many users are disabled
517
-	 *
518
-	 * @return int
519
-	 * @since 12.0.0
520
-	 */
521
-	public function countDisabledUsers(): int {
522
-		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
523
-		$queryBuilder->select($queryBuilder->func()->count('*'))
524
-			->from('preferences')
525
-			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
526
-			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
527
-			->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR));
528
-
529
-
530
-		$result = $queryBuilder->execute();
531
-		$count = $result->fetchOne();
532
-		$result->closeCursor();
533
-
534
-		if ($count !== false) {
535
-			$count = (int)$count;
536
-		} else {
537
-			$count = 0;
538
-		}
539
-
540
-		return $count;
541
-	}
542
-
543
-	/**
544
-	 * returns how many users are disabled in the requested groups
545
-	 *
546
-	 * @param array $groups groupids to search
547
-	 * @return int
548
-	 * @since 14.0.0
549
-	 */
550
-	public function countDisabledUsersOfGroups(array $groups): int {
551
-		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
552
-		$queryBuilder->select($queryBuilder->createFunction('COUNT(DISTINCT ' . $queryBuilder->getColumnName('uid') . ')'))
553
-			->from('preferences', 'p')
554
-			->innerJoin('p', 'group_user', 'g', $queryBuilder->expr()->eq('p.userid', 'g.uid'))
555
-			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
556
-			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
557
-			->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR))
558
-			->andWhere($queryBuilder->expr()->in('gid', $queryBuilder->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)));
559
-
560
-		$result = $queryBuilder->execute();
561
-		$count = $result->fetchOne();
562
-		$result->closeCursor();
563
-
564
-		if ($count !== false) {
565
-			$count = (int)$count;
566
-		} else {
567
-			$count = 0;
568
-		}
569
-
570
-		return $count;
571
-	}
572
-
573
-	/**
574
-	 * returns how many users have logged in once
575
-	 *
576
-	 * @return int
577
-	 * @since 11.0.0
578
-	 */
579
-	public function countSeenUsers() {
580
-		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
581
-		$queryBuilder->select($queryBuilder->func()->count('*'))
582
-			->from('preferences')
583
-			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('login')))
584
-			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('lastLogin')))
585
-			->andWhere($queryBuilder->expr()->isNotNull('configvalue'));
586
-
587
-		$query = $queryBuilder->execute();
588
-
589
-		$result = (int)$query->fetchOne();
590
-		$query->closeCursor();
591
-
592
-		return $result;
593
-	}
594
-
595
-	/**
596
-	 * @param \Closure $callback
597
-	 * @psalm-param \Closure(\OCP\IUser):?bool $callback
598
-	 * @since 11.0.0
599
-	 */
600
-	public function callForSeenUsers(\Closure $callback) {
601
-		$limit = 1000;
602
-		$offset = 0;
603
-		do {
604
-			$userIds = $this->getSeenUserIds($limit, $offset);
605
-			$offset += $limit;
606
-			foreach ($userIds as $userId) {
607
-				foreach ($this->backends as $backend) {
608
-					if ($backend->userExists($userId)) {
609
-						$user = $this->getUserObject($userId, $backend, false);
610
-						$return = $callback($user);
611
-						if ($return === false) {
612
-							return;
613
-						}
614
-						break;
615
-					}
616
-				}
617
-			}
618
-		} while (count($userIds) >= $limit);
619
-	}
620
-
621
-	/**
622
-	 * Getting all userIds that have a listLogin value requires checking the
623
-	 * value in php because on oracle you cannot use a clob in a where clause,
624
-	 * preventing us from doing a not null or length(value) > 0 check.
625
-	 *
626
-	 * @param int $limit
627
-	 * @param int $offset
628
-	 * @return string[] with user ids
629
-	 */
630
-	private function getSeenUserIds($limit = null, $offset = null) {
631
-		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
632
-		$queryBuilder->select(['userid'])
633
-			->from('preferences')
634
-			->where($queryBuilder->expr()->eq(
635
-				'appid', $queryBuilder->createNamedParameter('login'))
636
-			)
637
-			->andWhere($queryBuilder->expr()->eq(
638
-				'configkey', $queryBuilder->createNamedParameter('lastLogin'))
639
-			)
640
-			->andWhere($queryBuilder->expr()->isNotNull('configvalue')
641
-			);
642
-
643
-		if ($limit !== null) {
644
-			$queryBuilder->setMaxResults($limit);
645
-		}
646
-		if ($offset !== null) {
647
-			$queryBuilder->setFirstResult($offset);
648
-		}
649
-		$query = $queryBuilder->execute();
650
-		$result = [];
651
-
652
-		while ($row = $query->fetch()) {
653
-			$result[] = $row['userid'];
654
-		}
655
-
656
-		$query->closeCursor();
657
-
658
-		return $result;
659
-	}
660
-
661
-	/**
662
-	 * @param string $email
663
-	 * @return IUser[]
664
-	 * @since 9.1.0
665
-	 */
666
-	public function getByEmail($email) {
667
-		$userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email);
668
-
669
-		$users = array_map(function ($uid) {
670
-			return $this->get($uid);
671
-		}, $userIds);
672
-
673
-		return array_values(array_filter($users, function ($u) {
674
-			return ($u instanceof IUser);
675
-		}));
676
-	}
677
-
678
-	private function verifyUid(string $uid): bool {
679
-		$appdata = 'appdata_' . $this->config->getSystemValueString('instanceid');
680
-
681
-		if (\in_array($uid, [
682
-			'.htaccess',
683
-			'files_external',
684
-			'__groupfolders',
685
-			'.ocdata',
686
-			'owncloud.log',
687
-			'nextcloud.log',
688
-			$appdata], true)) {
689
-			return false;
690
-		}
691
-
692
-		$dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
693
-
694
-		return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid);
695
-	}
73
+    /**
74
+     * @var \OCP\UserInterface[] $backends
75
+     */
76
+    private $backends = [];
77
+
78
+    /**
79
+     * @var \OC\User\User[] $cachedUsers
80
+     */
81
+    private $cachedUsers = [];
82
+
83
+    /** @var IConfig */
84
+    private $config;
85
+
86
+    /** @var EventDispatcherInterface */
87
+    private $dispatcher;
88
+
89
+    /** @var ICache */
90
+    private $cache;
91
+
92
+    /** @var IEventDispatcher */
93
+    private $eventDispatcher;
94
+
95
+    public function __construct(IConfig $config,
96
+                                EventDispatcherInterface $oldDispatcher,
97
+                                ICacheFactory $cacheFactory,
98
+                                IEventDispatcher $eventDispatcher) {
99
+        $this->config = $config;
100
+        $this->dispatcher = $oldDispatcher;
101
+        $this->cache = $cacheFactory->createDistributed('user_backend_map');
102
+        $cachedUsers = &$this->cachedUsers;
103
+        $this->listen('\OC\User', 'postDelete', function ($user) use (&$cachedUsers) {
104
+            /** @var \OC\User\User $user */
105
+            unset($cachedUsers[$user->getUID()]);
106
+        });
107
+        $this->eventDispatcher = $eventDispatcher;
108
+    }
109
+
110
+    /**
111
+     * Get the active backends
112
+     * @return \OCP\UserInterface[]
113
+     */
114
+    public function getBackends() {
115
+        return $this->backends;
116
+    }
117
+
118
+    /**
119
+     * register a user backend
120
+     *
121
+     * @param \OCP\UserInterface $backend
122
+     */
123
+    public function registerBackend($backend) {
124
+        $this->backends[] = $backend;
125
+    }
126
+
127
+    /**
128
+     * remove a user backend
129
+     *
130
+     * @param \OCP\UserInterface $backend
131
+     */
132
+    public function removeBackend($backend) {
133
+        $this->cachedUsers = [];
134
+        if (($i = array_search($backend, $this->backends)) !== false) {
135
+            unset($this->backends[$i]);
136
+        }
137
+    }
138
+
139
+    /**
140
+     * remove all user backends
141
+     */
142
+    public function clearBackends() {
143
+        $this->cachedUsers = [];
144
+        $this->backends = [];
145
+    }
146
+
147
+    /**
148
+     * get a user by user id
149
+     *
150
+     * @param string $uid
151
+     * @return \OC\User\User|null Either the user or null if the specified user does not exist
152
+     */
153
+    public function get($uid) {
154
+        if (is_null($uid) || $uid === '' || $uid === false) {
155
+            return null;
156
+        }
157
+        if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
158
+            return $this->cachedUsers[$uid];
159
+        }
160
+
161
+        $cachedBackend = $this->cache->get($uid);
162
+        if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
163
+            // Cache has the info of the user backend already, so ask that one directly
164
+            $backend = $this->backends[$cachedBackend];
165
+            if ($backend->userExists($uid)) {
166
+                return $this->getUserObject($uid, $backend);
167
+            }
168
+        }
169
+
170
+        foreach ($this->backends as $i => $backend) {
171
+            if ($i === $cachedBackend) {
172
+                // Tried that one already
173
+                continue;
174
+            }
175
+
176
+            if ($backend->userExists($uid)) {
177
+                $this->cache->set($uid, $i, 300);
178
+                return $this->getUserObject($uid, $backend);
179
+            }
180
+        }
181
+        return null;
182
+    }
183
+
184
+    /**
185
+     * get or construct the user object
186
+     *
187
+     * @param string $uid
188
+     * @param \OCP\UserInterface $backend
189
+     * @param bool $cacheUser If false the newly created user object will not be cached
190
+     * @return \OC\User\User
191
+     */
192
+    protected function getUserObject($uid, $backend, $cacheUser = true) {
193
+        if ($backend instanceof IGetRealUIDBackend) {
194
+            $uid = $backend->getRealUID($uid);
195
+        }
196
+
197
+        if (isset($this->cachedUsers[$uid])) {
198
+            return $this->cachedUsers[$uid];
199
+        }
200
+
201
+        $user = new User($uid, $backend, $this->dispatcher, $this, $this->config);
202
+        if ($cacheUser) {
203
+            $this->cachedUsers[$uid] = $user;
204
+        }
205
+        return $user;
206
+    }
207
+
208
+    /**
209
+     * check if a user exists
210
+     *
211
+     * @param string $uid
212
+     * @return bool
213
+     */
214
+    public function userExists($uid) {
215
+        $user = $this->get($uid);
216
+        return ($user !== null);
217
+    }
218
+
219
+    /**
220
+     * Check if the password is valid for the user
221
+     *
222
+     * @param string $loginName
223
+     * @param string $password
224
+     * @return mixed the User object on success, false otherwise
225
+     */
226
+    public function checkPassword($loginName, $password) {
227
+        $result = $this->checkPasswordNoLogging($loginName, $password);
228
+
229
+        if ($result === false) {
230
+            \OC::$server->getLogger()->warning('Login failed: \''. $loginName .'\' (Remote IP: \''. \OC::$server->getRequest()->getRemoteAddress(). '\')', ['app' => 'core']);
231
+        }
232
+
233
+        return $result;
234
+    }
235
+
236
+    /**
237
+     * Check if the password is valid for the user
238
+     *
239
+     * @internal
240
+     * @param string $loginName
241
+     * @param string $password
242
+     * @return IUser|false the User object on success, false otherwise
243
+     */
244
+    public function checkPasswordNoLogging($loginName, $password) {
245
+        $loginName = str_replace("\0", '', $loginName);
246
+        $password = str_replace("\0", '', $password);
247
+
248
+        foreach ($this->backends as $backend) {
249
+            if ($backend->implementsActions(Backend::CHECK_PASSWORD)) {
250
+                $uid = $backend->checkPassword($loginName, $password);
251
+                if ($uid !== false) {
252
+                    return $this->getUserObject($uid, $backend);
253
+                }
254
+            }
255
+        }
256
+
257
+        // since http basic auth doesn't provide a standard way of handling non ascii password we allow password to be urlencoded
258
+        // we only do this decoding after using the plain password fails to maintain compatibility with any password that happens
259
+        // to contains urlencoded patterns by "accident".
260
+        $password = urldecode($password);
261
+
262
+        foreach ($this->backends as $backend) {
263
+            if ($backend->implementsActions(Backend::CHECK_PASSWORD)) {
264
+                $uid = $backend->checkPassword($loginName, $password);
265
+                if ($uid !== false) {
266
+                    return $this->getUserObject($uid, $backend);
267
+                }
268
+            }
269
+        }
270
+
271
+        return false;
272
+    }
273
+
274
+    /**
275
+     * search by user id
276
+     *
277
+     * @param string $pattern
278
+     * @param int $limit
279
+     * @param int $offset
280
+     * @return \OC\User\User[]
281
+     */
282
+    public function search($pattern, $limit = null, $offset = null) {
283
+        $users = [];
284
+        foreach ($this->backends as $backend) {
285
+            $backendUsers = $backend->getUsers($pattern, $limit, $offset);
286
+            if (is_array($backendUsers)) {
287
+                foreach ($backendUsers as $uid) {
288
+                    $users[$uid] = $this->getUserObject($uid, $backend);
289
+                }
290
+            }
291
+        }
292
+
293
+        uasort($users, function ($a, $b) {
294
+            /**
295
+             * @var \OC\User\User $a
296
+             * @var \OC\User\User $b
297
+             */
298
+            return strcasecmp($a->getUID(), $b->getUID());
299
+        });
300
+        return $users;
301
+    }
302
+
303
+    /**
304
+     * search by displayName
305
+     *
306
+     * @param string $pattern
307
+     * @param int $limit
308
+     * @param int $offset
309
+     * @return \OC\User\User[]
310
+     */
311
+    public function searchDisplayName($pattern, $limit = null, $offset = null) {
312
+        $users = [];
313
+        foreach ($this->backends as $backend) {
314
+            $backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
315
+            if (is_array($backendUsers)) {
316
+                foreach ($backendUsers as $uid => $displayName) {
317
+                    $users[] = $this->getUserObject($uid, $backend);
318
+                }
319
+            }
320
+        }
321
+
322
+        usort($users, function ($a, $b) {
323
+            /**
324
+             * @var \OC\User\User $a
325
+             * @var \OC\User\User $b
326
+             */
327
+            return strcasecmp($a->getDisplayName(), $b->getDisplayName());
328
+        });
329
+        return $users;
330
+    }
331
+
332
+    /**
333
+     * @param string $uid
334
+     * @param string $password
335
+     * @throws \InvalidArgumentException
336
+     * @return bool|IUser the created user or false
337
+     */
338
+    public function createUser($uid, $password) {
339
+        // DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
340
+        if (\OC::$server->get(IRegistry::class)->delegateIsHardUserLimitReached()) {
341
+            $l = \OC::$server->getL10N('lib');
342
+            throw new HintException($l->t('The user limit has been reached and the user was not created.'));
343
+        }
344
+
345
+        $localBackends = [];
346
+        foreach ($this->backends as $backend) {
347
+            if ($backend instanceof Database) {
348
+                // First check if there is another user backend
349
+                $localBackends[] = $backend;
350
+                continue;
351
+            }
352
+
353
+            if ($backend->implementsActions(Backend::CREATE_USER)) {
354
+                return $this->createUserFromBackend($uid, $password, $backend);
355
+            }
356
+        }
357
+
358
+        foreach ($localBackends as $backend) {
359
+            if ($backend->implementsActions(Backend::CREATE_USER)) {
360
+                return $this->createUserFromBackend($uid, $password, $backend);
361
+            }
362
+        }
363
+
364
+        return false;
365
+    }
366
+
367
+    /**
368
+     * @param string $uid
369
+     * @param string $password
370
+     * @param UserInterface $backend
371
+     * @return IUser|null
372
+     * @throws \InvalidArgumentException
373
+     */
374
+    public function createUserFromBackend($uid, $password, UserInterface $backend) {
375
+        $l = \OC::$server->getL10N('lib');
376
+
377
+        // Check the name for bad characters
378
+        // Allowed are: "a-z", "A-Z", "0-9" and "_.@-'"
379
+        if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) {
380
+            throw new \InvalidArgumentException($l->t('Only the following characters are allowed in a username:'
381
+                . ' "a-z", "A-Z", "0-9", and "_.@-\'"'));
382
+        }
383
+
384
+        // No empty username
385
+        if (trim($uid) === '') {
386
+            throw new \InvalidArgumentException($l->t('A valid username must be provided'));
387
+        }
388
+
389
+        // No whitespace at the beginning or at the end
390
+        if (trim($uid) !== $uid) {
391
+            throw new \InvalidArgumentException($l->t('Username contains whitespace at the beginning or at the end'));
392
+        }
393
+
394
+        // Username only consists of 1 or 2 dots (directory traversal)
395
+        if ($uid === '.' || $uid === '..') {
396
+            throw new \InvalidArgumentException($l->t('Username must not consist of dots only'));
397
+        }
398
+
399
+        if (!$this->verifyUid($uid)) {
400
+            throw new \InvalidArgumentException($l->t('Username is invalid because files already exist for this user'));
401
+        }
402
+
403
+        // No empty password
404
+        if (trim($password) === '') {
405
+            throw new \InvalidArgumentException($l->t('A valid password must be provided'));
406
+        }
407
+
408
+        // Check if user already exists
409
+        if ($this->userExists($uid)) {
410
+            throw new \InvalidArgumentException($l->t('The username is already being used'));
411
+        }
412
+
413
+        /** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
414
+        $this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
415
+        $this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
416
+        $state = $backend->createUser($uid, $password);
417
+        if ($state === false) {
418
+            throw new \InvalidArgumentException($l->t('Could not create user'));
419
+        }
420
+        $user = $this->getUserObject($uid, $backend);
421
+        if ($user instanceof IUser) {
422
+            /** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
423
+            $this->emit('\OC\User', 'postCreateUser', [$user, $password]);
424
+            $this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
425
+        }
426
+        return $user;
427
+    }
428
+
429
+    /**
430
+     * returns how many users per backend exist (if supported by backend)
431
+     *
432
+     * @param boolean $hasLoggedIn when true only users that have a lastLogin
433
+     *                entry in the preferences table will be affected
434
+     * @return array|int an array of backend class as key and count number as value
435
+     *                if $hasLoggedIn is true only an int is returned
436
+     */
437
+    public function countUsers($hasLoggedIn = false) {
438
+        if ($hasLoggedIn) {
439
+            return $this->countSeenUsers();
440
+        }
441
+        $userCountStatistics = [];
442
+        foreach ($this->backends as $backend) {
443
+            if ($backend->implementsActions(Backend::COUNT_USERS)) {
444
+                $backendUsers = $backend->countUsers();
445
+                if ($backendUsers !== false) {
446
+                    if ($backend instanceof IUserBackend) {
447
+                        $name = $backend->getBackendName();
448
+                    } else {
449
+                        $name = get_class($backend);
450
+                    }
451
+                    if (isset($userCountStatistics[$name])) {
452
+                        $userCountStatistics[$name] += $backendUsers;
453
+                    } else {
454
+                        $userCountStatistics[$name] = $backendUsers;
455
+                    }
456
+                }
457
+            }
458
+        }
459
+        return $userCountStatistics;
460
+    }
461
+
462
+    /**
463
+     * returns how many users per backend exist in the requested groups (if supported by backend)
464
+     *
465
+     * @param IGroup[] $groups an array of gid to search in
466
+     * @return array|int an array of backend class as key and count number as value
467
+     *                if $hasLoggedIn is true only an int is returned
468
+     */
469
+    public function countUsersOfGroups(array $groups) {
470
+        $users = [];
471
+        foreach ($groups as $group) {
472
+            $usersIds = array_map(function ($user) {
473
+                return $user->getUID();
474
+            }, $group->getUsers());
475
+            $users = array_merge($users, $usersIds);
476
+        }
477
+        return count(array_unique($users));
478
+    }
479
+
480
+    /**
481
+     * The callback is executed for each user on each backend.
482
+     * If the callback returns false no further users will be retrieved.
483
+     *
484
+     * @param \Closure $callback
485
+     * @param string $search
486
+     * @param boolean $onlySeen when true only users that have a lastLogin entry
487
+     *                in the preferences table will be affected
488
+     * @since 9.0.0
489
+     */
490
+    public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
491
+        if ($onlySeen) {
492
+            $this->callForSeenUsers($callback);
493
+        } else {
494
+            foreach ($this->getBackends() as $backend) {
495
+                $limit = 500;
496
+                $offset = 0;
497
+                do {
498
+                    $users = $backend->getUsers($search, $limit, $offset);
499
+                    foreach ($users as $uid) {
500
+                        if (!$backend->userExists($uid)) {
501
+                            continue;
502
+                        }
503
+                        $user = $this->getUserObject($uid, $backend, false);
504
+                        $return = $callback($user);
505
+                        if ($return === false) {
506
+                            break;
507
+                        }
508
+                    }
509
+                    $offset += $limit;
510
+                } while (count($users) >= $limit);
511
+            }
512
+        }
513
+    }
514
+
515
+    /**
516
+     * returns how many users are disabled
517
+     *
518
+     * @return int
519
+     * @since 12.0.0
520
+     */
521
+    public function countDisabledUsers(): int {
522
+        $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
523
+        $queryBuilder->select($queryBuilder->func()->count('*'))
524
+            ->from('preferences')
525
+            ->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
526
+            ->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
527
+            ->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR));
528
+
529
+
530
+        $result = $queryBuilder->execute();
531
+        $count = $result->fetchOne();
532
+        $result->closeCursor();
533
+
534
+        if ($count !== false) {
535
+            $count = (int)$count;
536
+        } else {
537
+            $count = 0;
538
+        }
539
+
540
+        return $count;
541
+    }
542
+
543
+    /**
544
+     * returns how many users are disabled in the requested groups
545
+     *
546
+     * @param array $groups groupids to search
547
+     * @return int
548
+     * @since 14.0.0
549
+     */
550
+    public function countDisabledUsersOfGroups(array $groups): int {
551
+        $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
552
+        $queryBuilder->select($queryBuilder->createFunction('COUNT(DISTINCT ' . $queryBuilder->getColumnName('uid') . ')'))
553
+            ->from('preferences', 'p')
554
+            ->innerJoin('p', 'group_user', 'g', $queryBuilder->expr()->eq('p.userid', 'g.uid'))
555
+            ->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
556
+            ->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
557
+            ->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR))
558
+            ->andWhere($queryBuilder->expr()->in('gid', $queryBuilder->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)));
559
+
560
+        $result = $queryBuilder->execute();
561
+        $count = $result->fetchOne();
562
+        $result->closeCursor();
563
+
564
+        if ($count !== false) {
565
+            $count = (int)$count;
566
+        } else {
567
+            $count = 0;
568
+        }
569
+
570
+        return $count;
571
+    }
572
+
573
+    /**
574
+     * returns how many users have logged in once
575
+     *
576
+     * @return int
577
+     * @since 11.0.0
578
+     */
579
+    public function countSeenUsers() {
580
+        $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
581
+        $queryBuilder->select($queryBuilder->func()->count('*'))
582
+            ->from('preferences')
583
+            ->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('login')))
584
+            ->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('lastLogin')))
585
+            ->andWhere($queryBuilder->expr()->isNotNull('configvalue'));
586
+
587
+        $query = $queryBuilder->execute();
588
+
589
+        $result = (int)$query->fetchOne();
590
+        $query->closeCursor();
591
+
592
+        return $result;
593
+    }
594
+
595
+    /**
596
+     * @param \Closure $callback
597
+     * @psalm-param \Closure(\OCP\IUser):?bool $callback
598
+     * @since 11.0.0
599
+     */
600
+    public function callForSeenUsers(\Closure $callback) {
601
+        $limit = 1000;
602
+        $offset = 0;
603
+        do {
604
+            $userIds = $this->getSeenUserIds($limit, $offset);
605
+            $offset += $limit;
606
+            foreach ($userIds as $userId) {
607
+                foreach ($this->backends as $backend) {
608
+                    if ($backend->userExists($userId)) {
609
+                        $user = $this->getUserObject($userId, $backend, false);
610
+                        $return = $callback($user);
611
+                        if ($return === false) {
612
+                            return;
613
+                        }
614
+                        break;
615
+                    }
616
+                }
617
+            }
618
+        } while (count($userIds) >= $limit);
619
+    }
620
+
621
+    /**
622
+     * Getting all userIds that have a listLogin value requires checking the
623
+     * value in php because on oracle you cannot use a clob in a where clause,
624
+     * preventing us from doing a not null or length(value) > 0 check.
625
+     *
626
+     * @param int $limit
627
+     * @param int $offset
628
+     * @return string[] with user ids
629
+     */
630
+    private function getSeenUserIds($limit = null, $offset = null) {
631
+        $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
632
+        $queryBuilder->select(['userid'])
633
+            ->from('preferences')
634
+            ->where($queryBuilder->expr()->eq(
635
+                'appid', $queryBuilder->createNamedParameter('login'))
636
+            )
637
+            ->andWhere($queryBuilder->expr()->eq(
638
+                'configkey', $queryBuilder->createNamedParameter('lastLogin'))
639
+            )
640
+            ->andWhere($queryBuilder->expr()->isNotNull('configvalue')
641
+            );
642
+
643
+        if ($limit !== null) {
644
+            $queryBuilder->setMaxResults($limit);
645
+        }
646
+        if ($offset !== null) {
647
+            $queryBuilder->setFirstResult($offset);
648
+        }
649
+        $query = $queryBuilder->execute();
650
+        $result = [];
651
+
652
+        while ($row = $query->fetch()) {
653
+            $result[] = $row['userid'];
654
+        }
655
+
656
+        $query->closeCursor();
657
+
658
+        return $result;
659
+    }
660
+
661
+    /**
662
+     * @param string $email
663
+     * @return IUser[]
664
+     * @since 9.1.0
665
+     */
666
+    public function getByEmail($email) {
667
+        $userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email);
668
+
669
+        $users = array_map(function ($uid) {
670
+            return $this->get($uid);
671
+        }, $userIds);
672
+
673
+        return array_values(array_filter($users, function ($u) {
674
+            return ($u instanceof IUser);
675
+        }));
676
+    }
677
+
678
+    private function verifyUid(string $uid): bool {
679
+        $appdata = 'appdata_' . $this->config->getSystemValueString('instanceid');
680
+
681
+        if (\in_array($uid, [
682
+            '.htaccess',
683
+            'files_external',
684
+            '__groupfolders',
685
+            '.ocdata',
686
+            'owncloud.log',
687
+            'nextcloud.log',
688
+            $appdata], true)) {
689
+            return false;
690
+        }
691
+
692
+        $dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
693
+
694
+        return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid);
695
+    }
696 696
 }
Please login to merge, or discard this patch.