Passed
Push — master ( d682fe...3a99ef )
by Roeland
10:22
created

Database::getBackendName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author adrien <[email protected]>
7
 * @author Aldo "xoen" Giambelluca <[email protected]>
8
 * @author Arthur Schiwon <[email protected]>
9
 * @author Bart Visscher <[email protected]>
10
 * @author Bjoern Schiessle <[email protected]>
11
 * @author Björn Schießle <[email protected]>
12
 * @author fabian <[email protected]>
13
 * @author Georg Ehrke <[email protected]>
14
 * @author Jakob Sack <[email protected]>
15
 * @author Joas Schilling <[email protected]>
16
 * @author Jörn Friedrich Dreyer <[email protected]>
17
 * @author Loki3000 <[email protected]>
18
 * @author Lukas Reschke <[email protected]>
19
 * @author Michael Gapczynski <[email protected]>
20
 * @author michag86 <[email protected]>
21
 * @author Morris Jobke <[email protected]>
22
 * @author nishiki <[email protected]>
23
 * @author Robin Appelman <[email protected]>
24
 * @author Robin McCorkell <[email protected]>
25
 * @author Roeland Jago Douma <[email protected]>
26
 * @author Thomas Müller <[email protected]>
27
 * @author Vincent Petry <[email protected]>
28
 *
29
 * @license AGPL-3.0
30
 *
31
 * This code is free software: you can redistribute it and/or modify
32
 * it under the terms of the GNU Affero General Public License, version 3,
33
 * as published by the Free Software Foundation.
34
 *
35
 * This program is distributed in the hope that it will be useful,
36
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
37
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38
 * GNU Affero General Public License for more details.
39
 *
40
 * You should have received a copy of the GNU Affero General Public License, version 3,
41
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
42
 *
43
 */
44
45
/*
46
 *
47
 * The following SQL statement is just a help for developers and will not be
48
 * executed!
49
 *
50
 * CREATE TABLE `users` (
51
 *   `uid` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
52
 *   `password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
53
 *   PRIMARY KEY (`uid`)
54
 * ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
55
 *
56
 */
57
58
namespace OC\User;
59
60
use OC\Cache\CappedMemoryCache;
61
use OCP\IDBConnection;
62
use OCP\User\Backend\ABackend;
63
use OCP\User\Backend\ICheckPasswordBackend;
64
use OCP\User\Backend\ICountUsersBackend;
65
use OCP\User\Backend\ICreateUserBackend;
66
use OCP\User\Backend\IGetDisplayNameBackend;
67
use OCP\User\Backend\IGetHomeBackend;
68
use OCP\User\Backend\IGetRealUIDBackend;
69
use OCP\User\Backend\ISetDisplayNameBackend;
70
use OCP\User\Backend\ISetPasswordBackend;
71
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
72
use Symfony\Component\EventDispatcher\GenericEvent;
73
74
/**
75
 * Class for user management in a SQL Database (e.g. MySQL, SQLite)
76
 */
77
class Database extends ABackend
78
	implements ICreateUserBackend,
79
	           ISetPasswordBackend,
80
	           ISetDisplayNameBackend,
81
	           IGetDisplayNameBackend,
82
	           ICheckPasswordBackend,
83
	           IGetHomeBackend,
84
	           ICountUsersBackend,
85
	           IGetRealUIDBackend {
86
	/** @var CappedMemoryCache */
87
	private $cache;
88
89
	/** @var EventDispatcherInterface */
90
	private $eventDispatcher;
91
92
	/** @var IDBConnection */
93
	private $dbConn;
94
95
	/** @var string */
96
	private $table;
97
98
	/**
99
	 * \OC\User\Database constructor.
100
	 *
101
	 * @param EventDispatcherInterface $eventDispatcher
102
	 * @param string $table
103
	 */
104
	public function __construct($eventDispatcher = null, $table = 'users') {
105
		$this->cache = new CappedMemoryCache();
106
		$this->table = $table;
107
		$this->eventDispatcher = $eventDispatcher ? $eventDispatcher : \OC::$server->getEventDispatcher();
108
	}
109
110
	/**
111
	 * FIXME: This function should not be required!
112
	 */
113
	private function fixDI() {
114
		if ($this->dbConn === null) {
115
			$this->dbConn = \OC::$server->getDatabaseConnection();
116
		}
117
	}
118
119
	/**
120
	 * Create a new user
121
	 *
122
	 * @param string $uid The username of the user to create
123
	 * @param string $password The password of the new user
124
	 * @return bool
125
	 *
126
	 * Creates a new user. Basic checking of username is done in OC_User
127
	 * itself, not in its subclasses.
128
	 */
129
	public function createUser(string $uid, string $password): bool {
130
		$this->fixDI();
131
132
		if (!$this->userExists($uid)) {
133
			$event = new GenericEvent($password);
134
			$this->eventDispatcher->dispatch('OCP\PasswordPolicy::validate', $event);
135
136
			$qb = $this->dbConn->getQueryBuilder();
137
			$qb->insert($this->table)
138
				->values([
139
					'uid' => $qb->createNamedParameter($uid),
140
					'password' => $qb->createNamedParameter(\OC::$server->getHasher()->hash($password)),
141
					'uid_lower' => $qb->createNamedParameter(mb_strtolower($uid)),
142
				]);
143
144
			$result = $qb->execute();
145
146
			// Clear cache
147
			unset($this->cache[$uid]);
148
149
			return $result ? true : false;
150
		}
151
152
		return false;
153
	}
154
155
	/**
156
	 * delete a user
157
	 *
158
	 * @param string $uid The username of the user to delete
159
	 * @return bool
160
	 *
161
	 * Deletes a user
162
	 */
163
	public function deleteUser($uid) {
164
		$this->fixDI();
165
166
		// Delete user-group-relation
167
		$query = $this->dbConn->getQueryBuilder();
168
		$query->delete($this->table)
169
			->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
170
		$result = $query->execute();
171
172
		if (isset($this->cache[$uid])) {
173
			unset($this->cache[$uid]);
174
		}
175
176
		return $result ? true : false;
177
	}
178
179
	private function updatePassword(string $uid, string $passwordHash): bool {
180
		$query = $this->dbConn->getQueryBuilder();
181
		$query->update($this->table)
182
			->set('password', $query->createNamedParameter($passwordHash))
183
			->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
184
		$result = $query->execute();
185
186
		return $result ? true : false;
187
	}
188
189
	/**
190
	 * Set password
191
	 *
192
	 * @param string $uid The username
193
	 * @param string $password The new password
194
	 * @return bool
195
	 *
196
	 * Change the password of a user
197
	 */
198
	public function setPassword(string $uid, string $password): bool {
199
		$this->fixDI();
200
201
		if ($this->userExists($uid)) {
202
			$event = new GenericEvent($password);
203
			$this->eventDispatcher->dispatch('OCP\PasswordPolicy::validate', $event);
204
205
			$hasher = \OC::$server->getHasher();
206
			$hashedPassword = $hasher->hash($password);
207
208
			return $this->updatePassword($uid, $hashedPassword);
209
		}
210
211
		return false;
212
	}
213
214
	/**
215
	 * Set display name
216
	 *
217
	 * @param string $uid The username
218
	 * @param string $displayName The new display name
219
	 * @return bool
220
	 *
221
	 * Change the display name of a user
222
	 */
223
	public function setDisplayName(string $uid, string $displayName): bool {
224
		$this->fixDI();
225
226
		if ($this->userExists($uid)) {
227
			$query = $this->dbConn->getQueryBuilder();
228
			$query->update($this->table)
229
				->set('displayname', $query->createNamedParameter($displayName))
230
				->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
231
			$query->execute();
232
233
			$this->cache[$uid]['displayname'] = $displayName;
234
235
			return true;
236
		}
237
238
		return false;
239
	}
240
241
	/**
242
	 * get display name of the user
243
	 *
244
	 * @param string $uid user ID of the user
245
	 * @return string display name
246
	 */
247
	public function getDisplayName($uid): string {
248
		$uid = (string)$uid;
249
		$this->loadUser($uid);
250
		return empty($this->cache[$uid]['displayname']) ? $uid : $this->cache[$uid]['displayname'];
251
	}
252
253
	/**
254
	 * Get a list of all display names and user ids.
255
	 *
256
	 * @param string $search
257
	 * @param string|null $limit
258
	 * @param string|null $offset
259
	 * @return array an array of all displayNames (value) and the corresponding uids (key)
260
	 */
261
	public function getDisplayNames($search = '', $limit = null, $offset = null) {
262
		$this->fixDI();
263
264
		$query = $this->dbConn->getQueryBuilder();
265
266
		$query->select('uid', 'displayname')
267
			->from($this->table, 'u')
268
			->leftJoin('u', 'preferences', 'p', $query->expr()->andX(
0 ignored issues
show
Bug introduced by
$query->expr()->andX($qu...r()->literal('email'))) of type OCP\DB\QueryBuilder\ICompositeExpression is incompatible with the type string expected by parameter $condition of OCP\DB\QueryBuilder\IQueryBuilder::leftJoin(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

268
			->leftJoin('u', 'preferences', 'p', /** @scrutinizer ignore-type */ $query->expr()->andX(
Loading history...
269
				$query->expr()->eq('userid', 'uid'),
270
				$query->expr()->eq('appid', $query->expr()->literal('settings')),
271
				$query->expr()->eq('configkey', $query->expr()->literal('email')))
272
			)
273
			// sqlite doesn't like re-using a single named parameter here
274
			->where($query->expr()->iLike('uid', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
275
			->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
276
			->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
277
			->orderBy($query->func()->lower('displayname'), 'ASC')
278
			->orderBy('uid_lower', 'ASC')
279
			->setMaxResults($limit)
280
			->setFirstResult($offset);
281
282
		$result = $query->execute();
283
		$displayNames = [];
284
		while ($row = $result->fetch()) {
285
			$displayNames[(string)$row['uid']] = (string)$row['displayname'];
286
		}
287
288
		return $displayNames;
289
	}
290
291
	/**
292
	 * Check if the password is correct
293
	 *
294
	 * @param string $uid The username
295
	 * @param string $password The password
296
	 * @return string
297
	 *
298
	 * Check if the password is correct without logging in the user
299
	 * returns the user id or false
300
	 */
301
	public function checkPassword(string $uid, string $password) {
302
		$this->fixDI();
303
304
		$qb = $this->dbConn->getQueryBuilder();
305
		$qb->select('uid', 'password')
306
			->from($this->table)
307
			->where(
308
				$qb->expr()->eq(
309
					'uid_lower', $qb->createNamedParameter(mb_strtolower($uid))
310
				)
311
			);
312
		$result = $qb->execute();
313
		$row = $result->fetch();
314
		$result->closeCursor();
315
316
		if ($row) {
317
			$storedHash = $row['password'];
318
			$newHash = '';
319
			if (\OC::$server->getHasher()->verify($password, $storedHash, $newHash)) {
320
				if (!empty($newHash)) {
321
					$this->updatePassword($uid, $newHash);
322
				}
323
				return (string)$row['uid'];
324
			}
325
326
		}
327
328
		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...
329
	}
330
331
	/**
332
	 * Load an user in the cache
333
	 *
334
	 * @param string $uid the username
335
	 * @return boolean true if user was found, false otherwise
336
	 */
337
	private function loadUser($uid) {
338
		$this->fixDI();
339
340
		$uid = (string)$uid;
341
		if (!isset($this->cache[$uid])) {
342
			//guests $uid could be NULL or ''
343
			if ($uid === '') {
344
				$this->cache[$uid] = false;
345
				return true;
346
			}
347
348
			$qb = $this->dbConn->getQueryBuilder();
349
			$qb->select('uid', 'displayname')
350
				->from($this->table)
351
				->where(
352
					$qb->expr()->eq(
353
						'uid_lower', $qb->createNamedParameter(mb_strtolower($uid))
354
					)
355
				);
356
			$result = $qb->execute();
357
			$row = $result->fetch();
358
			$result->closeCursor();
359
360
			$this->cache[$uid] = false;
361
362
			// "uid" is primary key, so there can only be a single result
363
			if ($row !== false) {
364
				$this->cache[$uid]['uid'] = (string)$row['uid'];
365
				$this->cache[$uid]['displayname'] = (string)$row['displayname'];
366
			} else {
367
				return false;
368
			}
369
		}
370
371
		return true;
372
	}
373
374
	/**
375
	 * Get a list of all users
376
	 *
377
	 * @param string $search
378
	 * @param null|int $limit
379
	 * @param null|int $offset
380
	 * @return string[] an array of all uids
381
	 */
382
	public function getUsers($search = '', $limit = null, $offset = null) {
383
		$users = $this->getDisplayNames($search, $limit, $offset);
384
		$userIds = array_map(function ($uid) {
385
			return (string)$uid;
386
		}, array_keys($users));
387
		sort($userIds, SORT_STRING | SORT_FLAG_CASE);
388
		return $userIds;
389
	}
390
391
	/**
392
	 * check if a user exists
393
	 *
394
	 * @param string $uid the username
395
	 * @return boolean
396
	 */
397
	public function userExists($uid) {
398
		$this->loadUser($uid);
399
		return $this->cache[$uid] !== false;
400
	}
401
402
	/**
403
	 * get the user's home directory
404
	 *
405
	 * @param string $uid the username
406
	 * @return string|false
407
	 */
408
	public function getHome(string $uid) {
409
		if ($this->userExists($uid)) {
410
			return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $uid;
411
		}
412
413
		return false;
414
	}
415
416
	/**
417
	 * @return bool
418
	 */
419
	public function hasUserListings() {
420
		return true;
421
	}
422
423
	/**
424
	 * counts the users in the database
425
	 *
426
	 * @return int|bool
427
	 */
428
	public function countUsers() {
429
		$this->fixDI();
430
431
		$query = $this->dbConn->getQueryBuilder();
432
		$query->select($query->func()->count('uid'))
433
			->from($this->table);
434
		$result = $query->execute();
435
436
		return $result->fetchColumn();
437
	}
438
439
	/**
440
	 * returns the username for the given login name in the correct casing
441
	 *
442
	 * @param string $loginName
443
	 * @return string|false
444
	 */
445
	public function loginName2UserName($loginName) {
446
		if ($this->userExists($loginName)) {
447
			return $this->cache[$loginName]['uid'];
448
		}
449
450
		return false;
451
	}
452
453
	/**
454
	 * Backend name to be shown in user management
455
	 *
456
	 * @return string the name of the backend to be shown
457
	 */
458
	public function getBackendName() {
459
		return 'Database';
460
	}
461
462
	public static function preLoginNameUsedAsUserName($param) {
463
		if (!isset($param['uid'])) {
464
			throw new \Exception('key uid is expected to be set in $param');
465
		}
466
467
		$backends = \OC::$server->getUserManager()->getBackends();
468
		foreach ($backends as $backend) {
469
			if ($backend instanceof Database) {
470
				/** @var \OC\User\Database $backend */
471
				$uid = $backend->loginName2UserName($param['uid']);
472
				if ($uid !== false) {
473
					$param['uid'] = $uid;
474
					return;
475
				}
476
			}
477
		}
478
	}
479
480
	public function getRealUID(string $uid): string {
481
		if (!$this->userExists($uid)) {
482
			throw new \RuntimeException($uid . ' does not exist');
483
		}
484
485
		return $this->cache[$uid]['uid'];
486
	}
487
488
489
}
490