Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.29% covered (success)
89.29%
75 / 84
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthDatabase
89.29% covered (success)
89.29%
75 / 84
28.57% covered (danger)
28.57%
2 / 7
26.83
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 create
87.50% covered (success)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
4.03
 update
86.67% covered (success)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
4.04
 delete
85.71% covered (success)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
4.05
 checkCredentials
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
10.06
 rehash
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isValidLogin
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
1<?php
2
3/**
4 * Manages user authentication with databases.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Lars Tiedemann <php@larstiedemann.de>
12 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
13 * @copyright 2005-2026 phpMyFAQ Team
14 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
15 * @link      https://www.phpmyfaq.de
16 * @since     2005-09-30
17 */
18
19declare(strict_types=1);
20
21namespace phpMyFAQ\Auth;
22
23use phpMyFAQ\Auth;
24use phpMyFAQ\Configuration;
25use phpMyFAQ\Core\Exception;
26use phpMyFAQ\Database;
27use phpMyFAQ\Database\DatabaseDriver;
28use phpMyFAQ\User;
29
30/**
31 * Class AuthDatabase
32 *
33 * @package phpMyFAQ\Auth
34 */
35class AuthDatabase extends Auth implements AuthDriverInterface
36{
37    private readonly DatabaseDriver $databaseDriver;
38
39    private readonly PasswordHasher $passwordHasher;
40
41    /**
42     * @inheritDoc
43     */
44    public function __construct(Configuration $configuration)
45    {
46        parent::__construct($configuration);
47
48        $this->databaseDriver = $this->configuration->getDb();
49        $this->passwordHasher = new PasswordHasher($this->configuration);
50    }
51
52    /**
53     * @inheritDoc
54     * @throws AuthException
55     */
56    public function create(string $login, #[\SensitiveParameter] string $password, string $domain = ''): bool
57    {
58        if ($this->isValidLogin($login) > 0) {
59            throw new AuthException(User::ERROR_USER_ADD . ': ' . User::ERROR_USER_LOGIN_NOT_UNIQUE);
60        }
61
62        $add = sprintf(
63            "INSERT INTO %sfaquserlogin (login, pass, domain) VALUES ('%s', '%s', '%s')",
64            Database::getTablePrefix(),
65            $this->databaseDriver->escape($login),
66            $this->databaseDriver->escape($this->passwordHasher->hash($password)),
67            $this->databaseDriver->escape($domain),
68        );
69
70        $add = $this->databaseDriver->query($add);
71
72        $error = $this->databaseDriver->error();
73
74        if ($error !== '') {
75            throw new AuthException(User::ERROR_USER_ADD . ': ' . $error);
76        }
77
78        if (!$add) {
79            throw new AuthException(User::ERROR_USER_ADD);
80        }
81
82        return true;
83    }
84
85    /**
86     * @inheritDoc
87     * @throws AuthException
88     */
89    public function update(string $login, #[\SensitiveParameter] string $password): bool
90    {
91        if ($this->isValidLogin($login) < 1) {
92            throw new AuthException(User::ERROR_USER_CHANGE . ': ' . User::ERROR_USER_NOT_FOUND);
93        }
94
95        $change = sprintf(
96            "UPDATE %sfaquserlogin SET pass = '%s' WHERE login = '%s'",
97            Database::getTablePrefix(),
98            $this->databaseDriver->escape($this->passwordHasher->hash($password)),
99            $this->databaseDriver->escape($login),
100        );
101
102        $change = $this->databaseDriver->query($change);
103
104        $error = $this->databaseDriver->error();
105
106        if ($error !== '') {
107            throw new AuthException(User::ERROR_USER_CHANGE . ': ' . $error);
108        }
109
110        if (!$change) {
111            throw new AuthException(User::ERROR_USER_CHANGE);
112        }
113
114        return true;
115    }
116
117    /**
118     * @inheritDoc
119     * @throws AuthException|Exception
120     */
121    public function delete(string $login): bool
122    {
123        if ($this->isValidLogin($login) < 1) {
124            throw new Exception(User::ERROR_USER_DELETE . User::ERROR_USER_NOT_FOUND);
125        }
126
127        $delete = sprintf(
128            "DELETE FROM %sfaquserlogin WHERE login = '%s'",
129            Database::getTablePrefix(),
130            $this->databaseDriver->escape($login),
131        );
132
133        $delete = $this->databaseDriver->query($delete);
134
135        $error = $this->databaseDriver->error();
136
137        if ($error !== '') {
138            throw new AuthException(User::ERROR_USER_DELETE . ': ' . $error);
139        }
140
141        if (!$delete) {
142            throw new AuthException(User::ERROR_USER_DELETE . ': ' . $error);
143        }
144
145        return true;
146    }
147
148    /**
149     * @inheritDoc
150     * @throws AuthException
151     */
152    public function checkCredentials(
153        string $login,
154        #[\SensitiveParameter]
155        string $password,
156        ?array $optionalData = null,
157    ): bool {
158        $check = $this->databaseDriver->queryPrepared(
159            sprintf('SELECT login, pass FROM %sfaquserlogin WHERE login = ?', Database::getTablePrefix()),
160            [$login],
161        );
162
163        $error = $this->databaseDriver->error();
164
165        if ($error !== '') {
166            throw new AuthException(User::ERROR_USER_NOT_FOUND . ': ' . $error);
167        }
168
169        $numRows = $this->databaseDriver->numRows($check);
170        if ($numRows < 1) {
171            throw new AuthException(User::ERROR_USER_NOT_FOUND);
172        }
173
174        // if login not unique, raise an error but continue
175        if ($numRows > 1) {
176            throw new AuthException(User::ERROR_USER_LOGIN_NOT_UNIQUE);
177        }
178
179        // if multiple accounts are ok, just 1 valid required
180        while (true) {
181            $user = $this->databaseDriver->fetchArray($check);
182            if ($user === false || $user === null || $user === []) {
183                break;
184            }
185
186            $login = (string) $user['login'];
187            $passwordHash = (string) $user['pass'];
188
189            if (!$this->passwordHasher->verify($login, $password, $passwordHash)) {
190                continue;
191            }
192
193            if ($this->passwordHasher->needsRehash($passwordHash)) {
194                $this->rehash($login, $password);
195            }
196
197            return true;
198        }
199
200        throw new AuthException(User::ERROR_USER_INCORRECT_PASSWORD);
201    }
202
203    /**
204     * Transparently upgrades a stored password hash to current bcrypt
205     * parameters after a successful login. Best-effort: a failed write must
206     * never block an otherwise valid login.
207     */
208    private function rehash(string $login, #[\SensitiveParameter] string $password): void
209    {
210        $this->databaseDriver->queryPrepared(
211            sprintf('UPDATE %sfaquserlogin SET pass = ? WHERE login = ?', Database::getTablePrefix()),
212            [$this->passwordHasher->hash($password), $login],
213        );
214    }
215
216    /**
217     * @inheritDoc
218     * @throws AuthException
219     */
220    public function isValidLogin(string $login, ?array $optionalData = null): int
221    {
222        $check = $this->databaseDriver->queryPrepared(
223            sprintf('SELECT login FROM %sfaquserlogin WHERE login = ?', Database::getTablePrefix()),
224            [$login],
225        );
226
227        $error = $this->databaseDriver->error();
228
229        if ($error !== '') {
230            throw new AuthException($error);
231        }
232
233        return $this->databaseDriver->numRows($check);
234    }
235}