Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.88% covered (warning)
72.88%
86 / 118
52.17% covered (warning)
52.17%
12 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
Mysqli
72.88% covered (warning)
72.88%
86 / 118
52.17% covered (warning)
52.17%
12 / 23
91.06
0.00% covered (danger)
0.00%
0 / 1
 connection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connect
18.75% covered (danger)
18.75%
3 / 16
0.00% covered (danger)
0.00%
0 / 1
33.28
 error
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 fetchRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 fetchAll
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 fetchObject
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 numRows
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 log
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTableStatus
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTableNames
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
1
 getOne
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 nextId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 query
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 queryPrepared
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 affectedRows
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 clientVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 serverVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 close
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 lastInsertId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 now
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * The phpMyFAQ\Database\Mysqli class provides methods and functions for MySQL and
5 * MariaDB databases.
6 *
7 * This Source Code Form is subject to the terms of the Mozilla Public License,
8 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
9 * obtain one at https://mozilla.org/MPL/2.0/.
10 *
11 * @package   phpMyFAQ
12 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
13 * @author    David Soria Parra <dsoria@gmx.net>
14 * @copyright 2005-2026 phpMyFAQ Team
15 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
16 * @link      https://www.phpmyfaq.de
17 * @since     2005-12-13
18 */
19
20declare(strict_types=1);
21
22namespace phpMyFAQ\Database;
23
24use mysqli_result;
25use mysqli_sql_exception;
26use phpMyFAQ\Core\Exception;
27use phpMyFAQ\Database;
28use SensitiveParameter;
29
30/**
31 * Class Mysqli
32 *
33 * @package phpMyFAQ\Database
34 * @deprecated Use PDO instead. Will be removed in the v5.0 release.
35 */
36class Mysqli implements DatabaseDriver
37{
38    /**
39     * @var string[] Tables.
40     */
41    public array $tableNames = [];
42
43    /**
44     * The connection object.
45     */
46    private ?\mysqli $conn = null;
47
48    /**
49     * Returns the active connection or fails loudly when connect() has not
50     * been called yet or the connection was already closed.
51     *
52     * @throws Exception
53     */
54    private function connection(): \mysqli
55    {
56        return $this->conn ?? throw new Exception(message: 'There is no open database connection.');
57    }
58
59    /**
60     * The query log string.
61     */
62    private string $sqlLog = '';
63
64    /**
65     * Connects to the database.
66     *
67     * @param string $host Hostname or path to socket
68     * @param string $user Username
69     * @param string $password Password
70     * @param string $database Database name
71     * @return null|bool true, if connected, otherwise false
72     * @throws Exception
73     */
74    public function connect(
75        string $host,
76        #[SensitiveParameter]
77        string $user,
78        #[SensitiveParameter]
79        string $password,
80        string $database = '',
81        ?int $port = null,
82    ): ?bool {
83        try {
84            // Connect to MySQL via network by default.
85            $connection = new \mysqli($host, $user, $password, null, $port);
86            if (str_starts_with($host, '/')) {
87                // Connect to MySQL via socket
88                $connection = new \mysqli(null, $user, $password, null, $port, $host);
89            }
90        } catch (mysqli_sql_exception $mysqlisqlexception) {
91            throw new Exception($mysqlisqlexception->getMessage());
92        }
93
94        $this->conn = $connection;
95
96        if ($connection->connect_error) {
97            Database::errorPage($connection->connect_errno . ': ' . $connection->connect_error);
98            die();
99        }
100
101        // change character set to UTF-8
102        if (!$connection->set_charset('utf8mb4')) {
103            Database::errorPage($this->error());
104        }
105
106        if ('' !== $database) {
107            try {
108                $connection->select_db($database);
109            } catch (mysqli_sql_exception) {
110                throw new Exception('Cannot connect to database ' . $database);
111            }
112        }
113
114        return true;
115    }
116
117    /**
118     * Returns the error string.
119     */
120    public function error(): string
121    {
122        return $this->conn instanceof \mysqli ? $this->conn->error : '';
123    }
124
125    /**
126     * Escapes a string for use in a query.
127     */
128    public function escape(string $string): string
129    {
130        return $this->connection()->real_escape_string($string);
131    }
132
133    /**
134     * Fetch a result row as an associative array.
135     */
136    public function fetchArray(mixed $result): array|false|null
137    {
138        return $result instanceof mysqli_result ? $result->fetch_assoc() : null;
139    }
140
141    /**
142     * Fetch a result row.
143     */
144    public function fetchRow(mixed $result): mixed
145    {
146        return $result instanceof mysqli_result ? $result->fetch_row()[0] ?? false : false;
147    }
148
149    /**
150     * Fetches a complete result as an object.
151     *
152     * @param mixed $result Result set
153     * @throws Exception
154     * @return list<\stdClass>|null
155     */
156    public function fetchAll(mixed $result): ?array
157    {
158        $ret = [];
159        if (false === $result) {
160            throw new Exception('Error while fetching result: ' . $this->error());
161        }
162
163        while (true) {
164            $row = $this->fetchObject($result);
165            if (!$row instanceof \stdClass) {
166                break;
167            }
168
169            $ret[] = $row;
170        }
171
172        return $ret;
173    }
174
175    /**
176     * Fetch a result row as an object.
177     * This function fetches a result row as an object.
178     *
179     * @return \stdClass|false|null
180     *
181     * @throws Exception
182     */
183    public function fetchObject(mixed $result): mixed
184    {
185        /* @mago-expect lint:inline-variable-return - the variable carries the @var type for mago analyze */
186        /** @var \stdClass|false|null $row */
187        $row = $result instanceof mysqli_result ? $result->fetch_object() : null;
188
189        return $row;
190    }
191
192    /**
193     * Number of rows in a result.
194     */
195    public function numRows(mixed $result): int
196    {
197        if ($result instanceof mysqli_result) {
198            return (int) $result->num_rows;
199        }
200
201        return 0;
202    }
203
204    /**
205     * Logs the queries.
206     */
207    public function log(): string
208    {
209        return $this->sqlLog;
210    }
211
212    /**
213     * This function returns the table status.
214     *
215     * @param string $prefix Table prefix
216     * @return string[]
217     */
218    public function getTableStatus(string $prefix = ''): array
219    {
220        $status = [];
221        foreach ($this->getTableNames($prefix) as $table) {
222            $status[$table] = $this->getOne('SELECT count(*) FROM ' . $table);
223        }
224
225        return $status;
226    }
227
228    /**
229     * Returns an array with all table names.
230     *
231     * @todo Have to be refactored because of https://github.com/thorsten/phpMyFAQ/issues/965
232     *
233     * @param string $prefix Table prefix
234     *
235     * @return string[]
236     */
237    public function getTableNames(string $prefix = ''): array
238    {
239        return $this->tableNames = [
240            $prefix . 'faqadminlog',
241            $prefix . 'faqattachment',
242            $prefix . 'faqattachment_file',
243            $prefix . 'faqbackup',
244            $prefix . 'faqbookmarks',
245            $prefix . 'faqcaptcha',
246            $prefix . 'faqcategories',
247            $prefix . 'faqcategoryrelations',
248            $prefix . 'faqcategory_group',
249            $prefix . 'faqcategory_news',
250            $prefix . 'faqcategory_order',
251            $prefix . 'faqcategory_user',
252            $prefix . 'faqchanges',
253            $prefix . 'faqchat_messages',
254            $prefix . 'faqcomments',
255            $prefix . 'faqconfig',
256            $prefix . 'faqcustompages',
257            $prefix . 'faqdata',
258            $prefix . 'faqdata_group',
259            $prefix . 'faqdata_revisions',
260            $prefix . 'faqdata_tags',
261            $prefix . 'faqdata_user',
262            $prefix . 'faqforms',
263            $prefix . 'faqglossary',
264            $prefix . 'faqgroup',
265            $prefix . 'faqgroup_right',
266            $prefix . 'faqinstances',
267            $prefix . 'faqinstances_config',
268            $prefix . 'faqnews',
269            $prefix . 'faqquestions',
270            $prefix . 'faqright',
271            $prefix . 'faqsearches',
272            $prefix . 'faqseo',
273            $prefix . 'faqsessions',
274            $prefix . 'faqstopwords',
275            $prefix . 'faqtags',
276            $prefix . 'faquser',
277            $prefix . 'faquserdata',
278            $prefix . 'faquserlogin',
279            $prefix . 'faquser_group',
280            $prefix . 'faquser_right',
281            $prefix . 'faqvisits',
282            $prefix . 'faqvoting',
283        ];
284    }
285
286    /**
287     * Returns just one row.
288     */
289    private function getOne(string $query): string
290    {
291        $result = $this->connection()->query($query);
292        $row = $result instanceof mysqli_result ? $result->fetch_row() : null;
293
294        return (string) ($row[0] ?? '');
295    }
296
297    /**
298     * This function is a replacement for MySQL's auto-increment so that
299     * we don't need it anymore.
300     *
301     * @param string $table The name of the table
302     * @param string $column The name of the ID column
303     * @throws Exception
304     */
305    public function nextId(string $table, string $column): int
306    {
307        $select = sprintf('
308           SELECT
309               MAX(%s) AS current_id
310           FROM
311               %s', $column, $table);
312
313        $mysqliresult = $this->query($select);
314
315        $current = $mysqliresult instanceof mysqli_result ? $mysqliresult->fetch_row() : [0];
316
317        return (int) ($current[0] ?? 0) + 1;
318    }
319
320    /**
321     * This function sends a query to the database.
322     *
323     * @return mysqli_result|bool
324     * @throws Exception
325     */
326    public function query(string $query, int $offset = 0, int $rowcount = 0): mixed
327    {
328        $this->sqlLog .= $query;
329
330        if (0 < $rowcount) {
331            $query .= sprintf(' LIMIT %d,%d', $offset, $rowcount);
332        }
333
334        try {
335            $result = $this->connection()->query($query);
336        } catch (mysqli_sql_exception $mysqlisqlexception) {
337            throw new Exception($mysqlisqlexception->getMessage());
338        }
339
340        if (false === $result) {
341            $this->sqlLog .= $this->connection()->errno . ': ' . $this->error();
342        }
343
344        return $result;
345    }
346
347    /**
348     * Sends a parameterized query; `?` placeholders are bound by mysqli.
349     *
350     * @param array<int, string|int|float|null> $params
351     * @throws Exception
352     */
353    public function queryPrepared(string $query, array $params): mixed
354    {
355        $this->sqlLog .= $query;
356
357        if (!$this->conn instanceof \mysqli) {
358            throw new Exception('No database connection available for query: ' . $query);
359        }
360
361        try {
362            return $this->conn->execute_query($query, $params);
363        } catch (mysqli_sql_exception $mysqlisqlexception) {
364            throw new Exception($mysqlisqlexception->getMessage());
365        }
366    }
367
368    /**
369     * Returns the number of rows affected by the last INSERT, UPDATE, or DELETE query.
370     */
371    public function affectedRows(): int
372    {
373        $rows = $this->connection()->affected_rows;
374
375        return $rows < 0 ? 0 : (int) $rows;
376    }
377
378    /**
379     * Returns the client version string.
380     */
381    public function clientVersion(): string
382    {
383        return mysqli_get_client_info();
384    }
385
386    /**
387     * Returns the server version string.
388     */
389    public function serverVersion(): string
390    {
391        return $this->connection()->server_info;
392    }
393
394    /**
395     * Closes the connection to the database.
396     */
397    public function close(): void
398    {
399        if ($this->conn instanceof \mysqli) {
400            $this->conn->close();
401            $this->conn = null;
402        }
403    }
404
405    public function __destruct()
406    {
407        $this->close();
408    }
409
410    /**
411     * Returns the ID of the last inserted row.
412     */
413    public function lastInsertId(): int|string
414    {
415        return (int) $this->connection()->insert_id;
416    }
417
418    public function now(): string
419    {
420        return 'NOW()';
421    }
422}