Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.43% covered (success)
86.43%
121 / 140
59.09% covered (warning)
59.09%
13 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
Sqlite3
86.43% covered (success)
86.43%
121 / 140
59.09% covered (warning)
59.09%
13 / 22
56.25
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
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchObject
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 fetchArray
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 fetchRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 fetchAll
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 error
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 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%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 query
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 queryPrepared
57.89% covered (warning)
57.89%
11 / 19
0.00% covered (danger)
0.00%
0 / 1
10.66
 fetchAssoc
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 numRows
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
 getTableNames
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
1
 nextId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 affectedRows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serverVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clientVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 close
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 lastInsertId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 now
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * The phpMyFAQ\Database\Sqlite3 class provides methods and functions for a SQLite v3 database.
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    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2012-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2012-03-02
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Database;
21
22use phpMyFAQ\Core\Exception;
23use SensitiveParameter;
24
25/**
26 * Class Sqlite3
27 *
28 * @package phpMyFAQ\Database
29 * @deprecated Use PDO instead. Will be removed in the v5.0 release.
30 */
31class Sqlite3 implements DatabaseDriver
32{
33    /**
34     * @var string[] Tables.
35     */
36    public array $tableNames = [];
37
38    /**
39     * The connection object.
40     */
41    private ?\SQLite3 $conn = null;
42
43    /**
44     * Returns the active connection or fails loudly when connect() has not
45     * been called yet or the connection was already closed.
46     */
47    private function connection(): \SQLite3
48    {
49        return $this->conn ?? throw new \RuntimeException('There is no open database connection.');
50    }
51
52    /**
53     * The query log string.
54     * @see query()
55     */
56    private string $sqlLog = '';
57
58    private const string ERROR_MESSAGE =
59        "Do not call numRows() after you've fetched one or more result records, because " . (
60            Sqlite3::class . '::numRows() has to reset the results at its end.'
61        );
62
63    /**
64     * Connects to the database.
65     */
66    public function connect(
67        string $host,
68        string $user,
69        #[SensitiveParameter]
70        string $password,
71        string $database = '',
72        ?int $port = null,
73    ): ?bool {
74        $connection = new \SQLite3($host);
75        $connection->enableExceptions(true);
76        $this->conn = $connection;
77
78        return true;
79    }
80
81    /**
82     * Escapes a string for use in a query.
83     */
84    public function escape(string $string): string
85    {
86        return \SQLite3::escapeString($string);
87    }
88
89    /**
90     * Fetch a result row as an object.
91     *
92     * @return \stdClass|null NULL if there are no more results
93     */
94    public function fetchObject(mixed $result): ?object
95    {
96        if (!$result instanceof \SQLite3Result) {
97            return null;
98        }
99
100        $return = $result->fetchArray(SQLITE3_ASSOC);
101
102        return is_array($return) ? (object) $return : null;
103    }
104
105    /**
106     * Fetch a result row as an array.
107     */
108    public function fetchArray(mixed $result): ?array
109    {
110        if (!$result instanceof \SQLite3Result) {
111            return [];
112        }
113
114        $fetchedData = $result->fetchArray(SQLITE3_ASSOC);
115
116        return is_array($fetchedData) ? $fetchedData : [];
117    }
118
119    /**
120     * Fetch a result row.
121     */
122    public function fetchRow(mixed $result): mixed
123    {
124        return $result instanceof \SQLite3Result ? $result->fetchArray(SQLITE3_ASSOC) : false;
125    }
126
127    /**
128     * Fetches a complete result as an object.
129     *
130     * @param mixed $result Resultset
131     * @return list<\stdClass>|null
132     * @throws Exception
133     */
134    public function fetchAll(mixed $result): ?array
135    {
136        $ret = [];
137        if (!$result instanceof \SQLite3Result) {
138            throw new Exception('Error while fetching result: ' . $this->error());
139        }
140
141        while (true) {
142            $row = $result->fetchArray(SQLITE3_ASSOC);
143            if (!is_array($row)) {
144                break;
145            }
146
147            $ret[] = (object) $row;
148        }
149
150        return $ret;
151    }
152
153    /**
154     * Returns the error string.
155     */
156    public function error(): string
157    {
158        if (!$this->conn instanceof \SQLite3 || 0 === $this->conn->lastErrorCode()) {
159            return '';
160        }
161
162        return $this->conn->lastErrorMsg();
163    }
164
165    /**
166     * Logs the queries.
167     */
168    public function log(): string
169    {
170        return $this->sqlLog;
171    }
172
173    /**
174     * This function returns the table status.
175     *
176     * @param string $prefix Table prefix
177     * @throws Exception
178     */
179    public function getTableStatus(string $prefix = ''): array
180    {
181        $arr = [];
182
183        // Use sqlite_schema (preferred) instead of sqlite_master to avoid linter complaints and for newer SQLite
184        $result = $this->query("SELECT name FROM sqlite_schema WHERE type='table' ORDER BY name");
185        while (true) {
186            $row = $this->fetchAssoc($result);
187            if ($row === []) {
188                break;
189            }
190
191            $tableName = (string) $row['name'];
192            $numResult = $this->query(sprintf('SELECT * FROM %s', $tableName));
193            $arr[$tableName] = $this->numRows($numResult);
194        }
195
196        return $arr;
197    }
198
199    /**
200     * This function sends a query to the database.
201     *
202     * @return \SQLite3Result|bool $result
203     */
204    public function query(string $query, int $offset = 0, int $rowcount = 0): \SQLite3Result|bool
205    {
206        $this->sqlLog .= $query;
207
208        if (0 < $rowcount) {
209            $query .= sprintf(' LIMIT %d,%d', $offset, $rowcount);
210        }
211
212        try {
213            $result = $this->connection()->query($query);
214        } catch (\SQLite3Exception) {
215            $result = false;
216        }
217
218        if (!$result) {
219            $this->sqlLog .= $this->error();
220        }
221
222        return $result;
223    }
224
225    /**
226     * Sends a parameterized query; `?` placeholders are bound by SQLite3.
227     *
228     * @param array<int, string|int|float|null> $params
229     */
230    public function queryPrepared(string $query, array $params): \SQLite3Result|bool
231    {
232        $this->sqlLog .= $query;
233
234        if (!$this->conn instanceof \SQLite3) {
235            return false;
236        }
237
238        try {
239            $statement = $this->conn->prepare($query);
240        } catch (\SQLite3Exception) {
241            $statement = false;
242        }
243
244        if (!$statement instanceof \SQLite3Stmt) {
245            $this->sqlLog .= $this->error();
246
247            return false;
248        }
249
250        $position = 1;
251        foreach ($params as $param) {
252            $statement->bindValue($position, $param);
253            ++$position;
254        }
255
256        try {
257            $result = $statement->execute();
258        } catch (\SQLite3Exception) {
259            $result = false;
260        }
261
262        if ($result === false) {
263            $this->sqlLog .= $this->error();
264        }
265
266        return $result;
267    }
268
269    /**
270     * Fetch a result row as an associate array.
271     */
272    public function fetchAssoc(mixed $result): array
273    {
274        if (!$result instanceof \SQLite3Result) {
275            return [];
276        }
277
278        $fetchedData = $result->fetchArray(SQLITE3_ASSOC);
279
280        return is_array($fetchedData) ? $fetchedData : [];
281    }
282
283    /**
284     * Number of rows in a result.
285     * @throws Exception
286     */
287    public function numRows(mixed $result): int
288    {
289        if (!$result instanceof \SQLite3Result) {
290            return 0;
291        }
292
293        if (property_exists($result, 'fetchedByPMF') && (bool) $result->fetchedByPMF) {
294            throw new Exception(self::ERROR_MESSAGE);
295        }
296
297        $numberOfRows = 0;
298        while ($result->fetchArray(SQLITE3_NUM)) {
299            ++$numberOfRows;
300        }
301
302        $result->reset();
303
304        return $numberOfRows;
305    }
306
307    /**
308     * Returns an array with all table names.
309     *
310     * @todo Have to be refactored because of https://github.com/thorsten/phpMyFAQ/issues/965
311     *
312     * @param string $prefix Table prefix
313     *
314     * @return string[]
315     */
316    public function getTableNames(string $prefix = ''): array
317    {
318        return $this->tableNames = [
319            $prefix . 'faqadminlog',
320            $prefix . 'faqattachment',
321            $prefix . 'faqattachment_file',
322            $prefix . 'faqbackup',
323            $prefix . 'faqbookmarks',
324            $prefix . 'faqcaptcha',
325            $prefix . 'faqcategories',
326            $prefix . 'faqcategoryrelations',
327            $prefix . 'faqcategory_group',
328            $prefix . 'faqcategory_news',
329            $prefix . 'faqcategory_order',
330            $prefix . 'faqcategory_user',
331            $prefix . 'faqchanges',
332            $prefix . 'faqchat_messages',
333            $prefix . 'faqcomments',
334            $prefix . 'faqconfig',
335            $prefix . 'faqcustompages',
336            $prefix . 'faqdata',
337            $prefix . 'faqdata_group',
338            $prefix . 'faqdata_revisions',
339            $prefix . 'faqdata_tags',
340            $prefix . 'faqdata_user',
341            $prefix . 'faqforms',
342            $prefix . 'faqglossary',
343            $prefix . 'faqgroup',
344            $prefix . 'faqgroup_right',
345            $prefix . 'faqinstances',
346            $prefix . 'faqinstances_config',
347            $prefix . 'faqnews',
348            $prefix . 'faqquestions',
349            $prefix . 'faqright',
350            $prefix . 'faqsearches',
351            $prefix . 'faqseo',
352            $prefix . 'faqsessions',
353            $prefix . 'faqstopwords',
354            $prefix . 'faqtags',
355            $prefix . 'faquser',
356            $prefix . 'faquserdata',
357            $prefix . 'faquserlogin',
358            $prefix . 'faquser_group',
359            $prefix . 'faquser_right',
360            $prefix . 'faqvisits',
361            $prefix . 'faqvoting',
362        ];
363    }
364
365    /**
366     * Returns the next ID of a table.
367     *
368     * @param string $table the name of the table
369     * @param string $column the name of the ID column
370     */
371    public function nextId(string $table, string $column): int
372    {
373        $result = (int) $this->connection()->querySingle(sprintf(
374            'SELECT max(%s) AS current_id FROM %s',
375            $column,
376            $table,
377        ));
378
379        return $result + 1;
380    }
381
382    /**
383     * Returns the number of rows affected by the last INSERT, UPDATE, or DELETE query.
384     */
385    public function affectedRows(): int
386    {
387        return $this->connection()->changes();
388    }
389
390    /**
391     * Returns the library version string.
392     */
393    public function serverVersion(): string
394    {
395        return $this->clientVersion();
396    }
397
398    /**
399     * Returns the library version string.
400     */
401    public function clientVersion(): string
402    {
403        return (string) \SQLite3::version()['versionString'];
404    }
405
406    /**
407     * Closes the connection to the database.
408     */
409    public function close(): bool
410    {
411        if (!$this->conn instanceof \SQLite3) {
412            return true;
413        }
414
415        $closed = $this->conn->close();
416        $this->conn = null;
417
418        return $closed;
419    }
420
421    /**
422     * Returns the ID of the last inserted row.
423     */
424    public function lastInsertId(): int|string
425    {
426        return $this->connection()->lastInsertRowID();
427    }
428
429    public function now(): string
430    {
431        return "DATETIME('now', 'localtime')";
432    }
433}