Lines 88.07% 96 / 109
Methods 46.66% 7 / 15
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 3 / 3 100.00% 1 / 1 1
 create 87.50% 14 / 16 0.00% 0 / 1 4.03
 update 86.66% 13 / 15 0.00% 0 / 1 4.04
 delete 85.71% 12 / 14 0.00% 0 / 1 4.05
 checkCredentials 91.66% 22 / 24 0.00% 0 / 1 10.06
 rehash 100.00% 4 / 4 100.00% 1 / 1 1
 isValidLogin 87.50% 7 / 8 0.00% 0 / 1 2.01
 [phpMyFAQ\Auth] getEncryptionContainer 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Auth] getErrors 100.00% 2 / 2 100.00% 1 / 1 3
 [phpMyFAQ\Auth] addError 0.00% 0 / 1 0.00% 0 / 1 2
 [phpMyFAQ\Auth] selectAuth 80.00% 8 / 10 0.00% 0 / 1 4.13
 [phpMyFAQ\Auth] enableReadOnly 100.00% 3 / 3 100.00% 1 / 1 1
 [phpMyFAQ\Auth] disableReadOnly 100.00% 3 / 3 100.00% 1 / 1 1
 [phpMyFAQ\Auth] isReadOnly 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Auth] encrypt 66.66% 2 / 3 0.00% 0 / 1 2.15
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}

Inherited from phpMyFAQ\Auth

73    public function getEncryptionContainer(string $encType): Encryption
74    {
75        $this->encContainer = Encryption::getInstance($encType, $this->configuration);
76        return $this->encContainer;
77    }
82    public function getErrors(): string
83    {
84        $message = $this->errors !== [] ? implode(separator: PHP_EOL, array: $this->errors) . PHP_EOL : '';
85        return $message . ($this->encContainer instanceof \phpMyFAQ\Encryption ? $this->encContainer->error() : '');
86    }
91    public function addError(string $message): void
92    {
93        $this->errors[] = $message;
94    }
101    public function selectAuth(string $method): Auth&AuthDriverInterface
102    {
103        $method = ucfirst(strtolower($method));
104        $authClass = '\\phpMyFAQ\\Auth\\Auth' . $method;
105
106        if (!class_exists($authClass) || !is_subclass_of($authClass, self::class)) {
107            $this->errors[] = self::PMF_ERROR_USER_NO_AUTH_TYPE;
108            throw new Exception(message: self::PMF_ERROR_USER_NO_AUTH_TYPE);
109        }
110
111        $auth = new $authClass($this->configuration);
112        if (!$auth instanceof AuthDriverInterface) {
113            $this->errors[] = self::PMF_ERROR_USER_NO_AUTH_TYPE;
114            throw new Exception(message: self::PMF_ERROR_USER_NO_AUTH_TYPE);
115        }
116
117        return $auth;
118    }
123    public function enableReadOnly(): bool
124    {
125        $oldReadOnly = $this->readOnly;
126        $this->readOnly = true;
127
128        return $oldReadOnly;
129    }
134    public function disableReadOnly(): bool
135    {
136        $oldReadOnly = $this->readOnly;
137        $this->readOnly = false;
138
139        return $oldReadOnly;
140    }
145    public function isReadOnly(): bool
146    {
147        return $this->readOnly;
148    }
155    public function encrypt(#[SensitiveParameter] string $string): string
156    {
157        if (!$this->encContainer instanceof \phpMyFAQ\Encryption) {
158            throw new Exception(message: 'No encryption container configured. Call getEncryptionContainer() first.');
159        }
160
161        return $this->encContainer->encrypt($string);
162    }