Passed
Push — master ( d011df...0d46fa )
by Joas
15:48 queued 11s
created

Database::deleteUser()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 4
nop 1
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 *
8
 * @author adrien <[email protected]>
9
 * @author Aldo "xoen" Giambelluca <[email protected]>
10
 * @author Arthur Schiwon <[email protected]>
11
 * @author Bart Visscher <[email protected]>
12
 * @author Bjoern Schiessle <[email protected]>
13
 * @author Björn Schießle <[email protected]>
14
 * @author Christoph Wurst <[email protected]>
15
 * @author Daniel Calviño Sánchez <[email protected]>
16
 * @author fabian <[email protected]>
17
 * @author Georg Ehrke <[email protected]>
18
 * @author Jakob Sack <[email protected]>
19
 * @author Joas Schilling <[email protected]>
20
 * @author Jörn Friedrich Dreyer <[email protected]>
21
 * @author Loki3000 <[email protected]>
22
 * @author Lukas Reschke <[email protected]>
23
 * @author Morris Jobke <[email protected]>
24
 * @author nishiki <[email protected]>
25
 * @author Robin Appelman <[email protected]>
26
 * @author Robin McCorkell <[email protected]>
27
 * @author Roeland Jago Douma <[email protected]>
28
 * @author Thomas Müller <[email protected]>
29
 * @author Vincent Petry <[email protected]>
30
 *
31
 * @license AGPL-3.0
32
 *
33
 * This code is free software: you can redistribute it and/or modify
34
 * it under the terms of the GNU Affero General Public License, version 3,
35
 * as published by the Free Software Foundation.
36
 *
37
 * This program is distributed in the hope that it will be useful,
38
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
39
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
40
 * GNU Affero General Public License for more details.
41
 *
42
 * You should have received a copy of the GNU Affero General Public License, version 3,
43
 * along with this program. If not, see <http://www.gnu.org/licenses/>
44
 *
45
 */
46
47
/*
48
 *
49
 * The following SQL statement is just a help for developers and will not be
50
 * executed!
51
 *
52
 * CREATE TABLE `users` (
53
 *   `uid` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
54
 *   `password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
55
 *   PRIMARY KEY (`uid`)
56
 * ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
57
 *
58
 */
59
60
namespace OC\User;
61
62
use OC\Cache\CappedMemoryCache;
63
use OCP\EventDispatcher\IEventDispatcher;
64
use OCP\IDBConnection;
65
use OCP\Security\Events\ValidatePasswordPolicyEvent;
66
use OCP\User\Backend\ABackend;
67
use OCP\User\Backend\ICheckPasswordBackend;
68
use OCP\User\Backend\ICountUsersBackend;
69
use OCP\User\Backend\ICreateUserBackend;
70
use OCP\User\Backend\IGetDisplayNameBackend;
71
use OCP\User\Backend\IGetHomeBackend;
72
use OCP\User\Backend\IGetRealUIDBackend;
73
use OCP\User\Backend\ISearchKnownUsersBackend;
74
use OCP\User\Backend\ISetDisplayNameBackend;
75
use OCP\User\Backend\ISetPasswordBackend;
76
77
/**
78
 * Class for user management in a SQL Database (e.g. MySQL, SQLite)
79
 */
80
class Database extends ABackend implements
81
	ICreateUserBackend,
82
			   ISetPasswordBackend,
83
			   ISetDisplayNameBackend,
84
			   IGetDisplayNameBackend,
85
			   ICheckPasswordBackend,
86
			   IGetHomeBackend,
87
			   ICountUsersBackend,
88
			   ISearchKnownUsersBackend,
89
			   IGetRealUIDBackend {
90
	/** @var CappedMemoryCache */
91
	private $cache;
92
93
	/** @var IEventDispatcher */
94
	private $eventDispatcher;
95
96
	/** @var IDBConnection */
97
	private $dbConn;
98
99
	/** @var string */
100
	private $table;
101
102
	/**
103
	 * \OC\User\Database constructor.
104
	 *
105
	 * @param IEventDispatcher $eventDispatcher
106
	 * @param string $table
107
	 */
108
	public function __construct($eventDispatcher = null, $table = 'users') {
109
		$this->cache = new CappedMemoryCache();
110
		$this->table = $table;
111
		$this->eventDispatcher = $eventDispatcher ? $eventDispatcher : \OC::$server->query(IEventDispatcher::class);
112
	}
113
114
	/**
115
	 * FIXME: This function should not be required!
116
	 */
117
	private function fixDI() {
118
		if ($this->dbConn === null) {
119
			$this->dbConn = \OC::$server->getDatabaseConnection();
120
		}
121
	}
122
123
	/**
124
	 * Create a new user
125
	 *
126
	 * @param string $uid The username of the user to create
127
	 * @param string $password The password of the new user
128
	 * @return bool
129
	 *
130
	 * Creates a new user. Basic checking of username is done in OC_User
131
	 * itself, not in its subclasses.
132
	 */
133
	public function createUser(string $uid, string $password): bool {
134
		$this->fixDI();
135
136
		if (!$this->userExists($uid)) {
137
			$this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
138
139
			$qb = $this->dbConn->getQueryBuilder();
140
			$qb->insert($this->table)
141
				->values([
142
					'uid' => $qb->createNamedParameter($uid),
143
					'password' => $qb->createNamedParameter(\OC::$server->getHasher()->hash($password)),
144
					'uid_lower' => $qb->createNamedParameter(mb_strtolower($uid)),
145
				]);
146
147
			$result = $qb->execute();
148
149
			// Clear cache
150
			unset($this->cache[$uid]);
151
152
			return $result ? true : false;
153
		}
154
155
		return false;
156
	}
157
158
	/**
159
	 * delete a user
160
	 *
161
	 * @param string $uid The username of the user to delete
162
	 * @return bool
163
	 *
164
	 * Deletes a user
165
	 */
166
	public function deleteUser($uid) {
167
		$this->fixDI();
168
169
		// Delete user-group-relation
170
		$query = $this->dbConn->getQueryBuilder();
171
		$query->delete($this->table)
172
			->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
173
		$result = $query->execute();
174
175
		if (isset($this->cache[$uid])) {
176
			unset($this->cache[$uid]);
177
		}
178
179
		return $result ? true : false;
180
	}
181
182
	private function updatePassword(string $uid, string $passwordHash): bool {
183
		$query = $this->dbConn->getQueryBuilder();
184
		$query->update($this->table)
185
			->set('password', $query->createNamedParameter($passwordHash))
186
			->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
187
		$result = $query->execute();
188
189
		return $result ? true : false;
190
	}
191
192
	/**
193
	 * Set password
194
	 *
195
	 * @param string $uid The username
196
	 * @param string $password The new password
197
	 * @return bool
198
	 *
199
	 * Change the password of a user
200
	 */
201
	public function setPassword(string $uid, string $password): bool {
202
		$this->fixDI();
203
204
		if ($this->userExists($uid)) {
205
			$this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
206
207
			$hasher = \OC::$server->getHasher();
208
			$hashedPassword = $hasher->hash($password);
209
210
			return $this->updatePassword($uid, $hashedPassword);
211
		}
212
213
		return false;
214
	}
215
216
	/**
217
	 * Set display name
218
	 *
219
	 * @param string $uid The username
220
	 * @param string $displayName The new display name
221
	 * @return bool
222
	 *
223
	 * Change the display name of a user
224
	 */
225
	public function setDisplayName(string $uid, string $displayName): bool {
226
		$this->fixDI();
227
228
		if ($this->userExists($uid)) {
229
			$query = $this->dbConn->getQueryBuilder();
230
			$query->update($this->table)
231
				->set('displayname', $query->createNamedParameter($displayName))
232
				->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
233
			$query->execute();
234
235
			$this->cache[$uid]['displayname'] = $displayName;
236
237
			return true;
238
		}
239
240
		return false;
241
	}
242
243
	/**
244
	 * get display name of the user
245
	 *
246
	 * @param string $uid user ID of the user
247
	 * @return string display name
248
	 */
249
	public function getDisplayName($uid): string {
250
		$uid = (string)$uid;
251
		$this->loadUser($uid);
252
		return empty($this->cache[$uid]['displayname']) ? $uid : $this->cache[$uid]['displayname'];
253
	}
254
255
	/**
256
	 * Get a list of all display names and user ids.
257
	 *
258
	 * @param string $search
259
	 * @param int|null $limit
260
	 * @param int|null $offset
261
	 * @return array an array of all displayNames (value) and the corresponding uids (key)
262
	 */
263
	public function getDisplayNames($search = '', $limit = null, $offset = null) {
264
		$limit = $this->fixLimit($limit);
265
266
		$this->fixDI();
267
268
		$query = $this->dbConn->getQueryBuilder();
269
270
		$query->select('uid', 'displayname')
271
			->from($this->table, 'u')
272
			->leftJoin('u', 'preferences', 'p', $query->expr()->andX(
273
				$query->expr()->eq('userid', 'uid'),
274
				$query->expr()->eq('appid', $query->expr()->literal('settings')),
275
				$query->expr()->eq('configkey', $query->expr()->literal('email')))
276
			)
277
			// sqlite doesn't like re-using a single named parameter here
278
			->where($query->expr()->iLike('uid', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
279
			->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
280
			->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
281
			->orderBy($query->func()->lower('displayname'), 'ASC')
282
			->addOrderBy('uid_lower', 'ASC')
283
			->setMaxResults($limit)
284
			->setFirstResult($offset);
285
286
		$result = $query->execute();
287
		$displayNames = [];
288
		while ($row = $result->fetch()) {
289
			$displayNames[(string)$row['uid']] = (string)$row['displayname'];
290
		}
291
292
		return $displayNames;
293
	}
294
295
	/**
296
	 * @param string $searcher
297
	 * @param string $pattern
298
	 * @param int|null $limit
299
	 * @param int|null $offset
300
	 * @return array
301
	 * @since 21.0.1
302
	 */
303
	public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
304
		$limit = $this->fixLimit($limit);
305
306
		$this->fixDI();
307
308
		$query = $this->dbConn->getQueryBuilder();
309
310
		$query->select('u.uid', 'u.displayname')
311
			->from($this->table, 'u')
312
			->leftJoin('u', 'known_users', 'k', $query->expr()->andX(
313
				$query->expr()->eq('k.known_user', 'u.uid'),
314
				$query->expr()->eq('k.known_to', $query->createNamedParameter($searcher))
315
			))
316
			->where($query->expr()->eq('k.known_to', $query->createNamedParameter($searcher)))
317
			->andWhere($query->expr()->orX(
318
				$query->expr()->iLike('u.uid', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%')),
319
				$query->expr()->iLike('u.displayname', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%'))
320
			))
321
			->orderBy('u.displayname', 'ASC')
322
			->addOrderBy('u.uid_lower', 'ASC')
323
			->setMaxResults($limit)
324
			->setFirstResult($offset);
325
326
		$result = $query->execute();
327
		$displayNames = [];
328
		while ($row = $result->fetch()) {
329
			$displayNames[(string)$row['uid']] = (string)$row['displayname'];
330
		}
331
332
		return $displayNames;
333
	}
334
335
	/**
336
	 * Check if the password is correct
337
	 *
338
	 * @param string $loginName The loginname
339
	 * @param string $password The password
340
	 * @return string
341
	 *
342
	 * Check if the password is correct without logging in the user
343
	 * returns the user id or false
344
	 */
345
	public function checkPassword(string $loginName, string $password) {
346
		$this->fixDI();
347
348
		$qb = $this->dbConn->getQueryBuilder();
349
		$qb->select('uid', 'password')
350
			->from($this->table)
351
			->where(
352
				$qb->expr()->eq(
353
					'uid_lower', $qb->createNamedParameter(mb_strtolower($loginName))
354
				)
355
			);
356
		$result = $qb->execute();
357
		$row = $result->fetch();
358
		$result->closeCursor();
359
360
		if ($row) {
361
			$storedHash = $row['password'];
362
			$newHash = '';
363
			if (\OC::$server->getHasher()->verify($password, $storedHash, $newHash)) {
364
				if (!empty($newHash)) {
365
					$this->updatePassword($loginName, $newHash);
366
				}
367
				return (string)$row['uid'];
368
			}
369
		}
370
371
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
372
	}
373
374
	/**
375
	 * Load an user in the cache
376
	 *
377
	 * @param string $uid the username
378
	 * @return boolean true if user was found, false otherwise
379
	 */
380
	private function loadUser($uid) {
381
		$this->fixDI();
382
383
		$uid = (string)$uid;
384
		if (!isset($this->cache[$uid])) {
385
			//guests $uid could be NULL or ''
386
			if ($uid === '') {
387
				$this->cache[$uid] = false;
388
				return true;
389
			}
390
391
			$qb = $this->dbConn->getQueryBuilder();
392
			$qb->select('uid', 'displayname')
393
				->from($this->table)
394
				->where(
395
					$qb->expr()->eq(
396
						'uid_lower', $qb->createNamedParameter(mb_strtolower($uid))
397
					)
398
				);
399
			$result = $qb->execute();
400
			$row = $result->fetch();
401
			$result->closeCursor();
402
403
			$this->cache[$uid] = false;
404
405
			// "uid" is primary key, so there can only be a single result
406
			if ($row !== false) {
407
				$this->cache[$uid]['uid'] = (string)$row['uid'];
408
				$this->cache[$uid]['displayname'] = (string)$row['displayname'];
409
			} else {
410
				return false;
411
			}
412
		}
413
414
		return true;
415
	}
416
417
	/**
418
	 * Get a list of all users
419
	 *
420
	 * @param string $search
421
	 * @param null|int $limit
422
	 * @param null|int $offset
423
	 * @return string[] an array of all uids
424
	 */
425
	public function getUsers($search = '', $limit = null, $offset = null) {
426
		$limit = $this->fixLimit($limit);
427
428
		$users = $this->getDisplayNames($search, $limit, $offset);
429
		$userIds = array_map(function ($uid) {
430
			return (string)$uid;
431
		}, array_keys($users));
432
		sort($userIds, SORT_STRING | SORT_FLAG_CASE);
433
		return $userIds;
434
	}
435
436
	/**
437
	 * check if a user exists
438
	 *
439
	 * @param string $uid the username
440
	 * @return boolean
441
	 */
442
	public function userExists($uid) {
443
		$this->loadUser($uid);
444
		return $this->cache[$uid] !== false;
445
	}
446
447
	/**
448
	 * get the user's home directory
449
	 *
450
	 * @param string $uid the username
451
	 * @return string|false
452
	 */
453
	public function getHome(string $uid) {
454
		if ($this->userExists($uid)) {
455
			return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $uid;
456
		}
457
458
		return false;
459
	}
460
461
	/**
462
	 * @return bool
463
	 */
464
	public function hasUserListings() {
465
		return true;
466
	}
467
468
	/**
469
	 * counts the users in the database
470
	 *
471
	 * @return int|bool
472
	 */
473
	public function countUsers() {
474
		$this->fixDI();
475
476
		$query = $this->dbConn->getQueryBuilder();
477
		$query->select($query->func()->count('uid'))
478
			->from($this->table);
479
		$result = $query->execute();
480
481
		return $result->fetchOne();
482
	}
483
484
	/**
485
	 * returns the username for the given login name in the correct casing
486
	 *
487
	 * @param string $loginName
488
	 * @return string|false
489
	 */
490
	public function loginName2UserName($loginName) {
491
		if ($this->userExists($loginName)) {
492
			return $this->cache[$loginName]['uid'];
493
		}
494
495
		return false;
496
	}
497
498
	/**
499
	 * Backend name to be shown in user management
500
	 *
501
	 * @return string the name of the backend to be shown
502
	 */
503
	public function getBackendName() {
504
		return 'Database';
505
	}
506
507
	public static function preLoginNameUsedAsUserName($param) {
508
		if (!isset($param['uid'])) {
509
			throw new \Exception('key uid is expected to be set in $param');
510
		}
511
512
		$backends = \OC::$server->getUserManager()->getBackends();
513
		foreach ($backends as $backend) {
514
			if ($backend instanceof Database) {
515
				/** @var \OC\User\Database $backend */
516
				$uid = $backend->loginName2UserName($param['uid']);
517
				if ($uid !== false) {
518
					$param['uid'] = $uid;
519
					return;
520
				}
521
			}
522
		}
523
	}
524
525
	public function getRealUID(string $uid): string {
526
		if (!$this->userExists($uid)) {
527
			throw new \RuntimeException($uid . ' does not exist');
528
		}
529
530
		return $this->cache[$uid]['uid'];
531
	}
532
533
	private function fixLimit($limit) {
534
		if (is_int($limit) && $limit >= 0) {
535
			return $limit;
536
		}
537
538
		return null;
539
	}
540
}
541