Passed
Push — master ( 70500e...f76321 )
by Roeland
20:36 queued 10:46
created

Database::fixLimit()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 6
rs 10
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);
0 ignored issues
show
Bug introduced by
'OCP\PasswordPolicy::validate' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

134
			$this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\PasswordPolicy::validate', $event);
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

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

134
			$this->eventDispatcher->/** @scrutinizer ignore-call */ 
135
                           dispatch('OCP\PasswordPolicy::validate', $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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);
0 ignored issues
show
Bug introduced by
'OCP\PasswordPolicy::validate' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

203
			$this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\PasswordPolicy::validate', $event);
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

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

203
			$this->eventDispatcher->/** @scrutinizer ignore-call */ 
204
                           dispatch('OCP\PasswordPolicy::validate', $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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
		$limit = $this->fixLimit($limit);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $limit is correct as $this->fixLimit($limit) targeting OC\User\Database::fixLimit() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
263
264
		$this->fixDI();
265
266
		$query = $this->dbConn->getQueryBuilder();
267
268
		$query->select('uid', 'displayname')
269
			->from($this->table, 'u')
270
			->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

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