Completed
Push — master ( ae71a5...213d43 )
by Morris
25:08 queued 10:22
created

Database::updatePassword()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 9
rs 9.9666
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\ILogger;
63
use OCP\User\Backend\ABackend;
64
use OCP\User\Backend\ICheckPasswordBackend;
65
use OCP\User\Backend\ICountUsersBackend;
66
use OCP\User\Backend\ICreateUserBackend;
67
use OCP\User\Backend\IGetDisplayNameBackend;
68
use OCP\User\Backend\IGetHomeBackend;
69
use OCP\User\Backend\ISetDisplayNameBackend;
70
use OCP\User\Backend\ISetPasswordBackend;
71
use OCP\Util;
72
use Symfony\Component\EventDispatcher\EventDispatcher;
73
use Symfony\Component\EventDispatcher\GenericEvent;
74
75
/**
76
 * Class for user management in a SQL Database (e.g. MySQL, SQLite)
77
 */
78
class Database extends ABackend
79
	implements ICreateUserBackend,
80
	           ISetPasswordBackend,
81
	           ISetDisplayNameBackend,
82
	           IGetDisplayNameBackend,
83
	           ICheckPasswordBackend,
84
	           IGetHomeBackend,
85
	           ICountUsersBackend {
86
	/** @var CappedMemoryCache */
87
	private $cache;
88
89
	/** @var EventDispatcher */
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 EventDispatcher $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();
0 ignored issues
show
Documentation Bug introduced by
It seems like $eventDispatcher ? $even...r->getEventDispatcher() can also be of type object<Symfony\Component...entDispatcherInterface>. However, the property $eventDispatcher is declared as type object<Symfony\Component...atcher\EventDispatcher>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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
Documentation introduced by
$query->expr()->andX($qu...r()->literal('email'))) is of type object<OCP\DB\QueryBuilder\ICompositeExpression>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
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;
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 View Code Duplication
		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
}
481