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 http://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-2020 phpMyFAQ Team
15 * @license   http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
16 * @link      https://www.phpmyfaq.de
17 */
18
19namespace phpMyFAQ\Database;
20
21use mysqli_result;
22use phpMyFAQ\Database;
23use phpMyFAQ\Exception;
24use phpMyFAQ\Utils;
25
26/**
27 * Class Mysqli
28 *
29 * @package phpMyFAQ\Database
30 */
31class Mysqli implements DatabaseDriver
32{
33    /**
34     * Tables.
35     *
36     * @var array
37     */
38    public $tableNames = [];
39
40    /**
41     * The connection object.
42     *
43     * @var \mysqli
44     */
45    private $conn = false;
46
47    /**
48     * The query log string.
49     *
50     * @var string
51     */
52    private $sqllog = '';
53
54    /**
55     * Connects to the database.
56     *
57     * @param string $host Hostname or path to socket
58     * @param string $user Username
59     * @param string $password Password
60     * @param string $database Database name
61     * @param int|null $port
62     * @return null|bool true, if connected, otherwise false
63     * @throws Exception
64     */
65    public function connect(string $host, string $user, string $password, $database = '', $port = 3306): ?bool
66    {
67        if (substr($host, 0, 1) === '/') {
68            // Connect to MySQL via socket
69            $this->conn = new \mysqli(null, $user, $password, null, $port, $host);
70        } else {
71            // Connect to MySQL via network
72            $this->conn = new \mysqli($host, $user, $password, null, $port);
73        }
74
75        if ($this->conn->connect_error) {
76            Database::errorPage($this->conn->connect_errno . ': ' . $this->conn->connect_error);
77            die();
78        }
79
80        // change character set to UTF-8
81        if (!$this->conn->set_charset('utf8')) {
82            Database::errorPage($this->error());
83        }
84
85        if ('' !== $database) {
86            if (!$this->conn->select_db($database)) {
87                throw new Exception('Cannot connect to database ' . $database);
88            }
89        }
90
91        return true;
92    }
93
94    /**
95     * Returns the error string.
96     *
97     * @return string
98     */
99    public function error(): string
100    {
101        return $this->conn->error;
102    }
103
104    /**
105     * Escapes a string for use in a query.
106     *
107     * @param string
108     *
109     * @return string
110     */
111    public function escape($string): string
112    {
113        return $this->conn->real_escape_string($string);
114    }
115
116    /**
117     * Fetch a result row as an object.
118     *
119     * This function fetches a result as an associative array.
120     *
121     * @param mixed $result
122     *
123     * @return array
124     */
125    public function fetchArray($result): ?array
126    {
127        return $result->fetch_assoc();
128    }
129
130    /**
131     * Fetches a complete result as an object.
132     *
133     * @param mysqli_result $result Result set
134     *
135     * @return array
136     * @throws Exception
137     */
138    public function fetchAll($result): array
139    {
140        $ret = [];
141        if (false === $result) {
142            throw new Exception('Error while fetching result: ' . $this->error());
143        }
144
145        while ($row = $this->fetchObject($result)) {
146            $ret[] = $row;
147        }
148
149        return $ret;
150    }
151
152    /**
153     * Fetch a result row as an object.
154     *
155     * This function fetches a result row as an object.
156     *
157     * @param mysqli_result $result
158     *
159     * @return mixed
160     * @throws Exception
161     */
162    public function fetchObject($result)
163    {
164        if ($result instanceof mysqli_result) {
165            return $result->fetch_object();
166        }
167
168        throw new Exception($this->error());
169    }
170
171    /**
172     * Number of rows in a result.
173     *
174     * @param mysqli_result $result
175     *
176     * @return int
177     */
178    public function numRows($result): int
179    {
180        if ($result instanceof mysqli_result) {
181            return $result->num_rows;
182        } else {
183            return 0;
184        }
185    }
186
187    /**
188     * Logs the queries.
189     *
190     * @return string
191     */
192    public function log(): string
193    {
194        return $this->sqllog;
195    }
196
197    /**
198     * This function returns the table status.
199     *
200     * @param string $prefix Table prefix
201     *
202     * @return array
203     */
204    public function getTableStatus($prefix = ''): array
205    {
206        $status = [];
207        foreach ($this->getTableNames($prefix) as $table) {
208            $status[$table] = $this->getOne('SELECT count(*) FROM ' . $table);
209        }
210
211        return $status;
212    }
213
214    /**
215     * Returns an array with all table names.
216     *
217     * @todo Have to be refactored because of https://github.com/thorsten/phpMyFAQ/issues/965
218     *
219     * @param string $prefix Table prefix
220     *
221     * @return string[]
222     */
223    public function getTableNames($prefix = ''): array
224    {
225        return $this->tableNames = [
226            $prefix . 'faqadminlog',
227            $prefix . 'faqattachment',
228            $prefix . 'faqattachment_file',
229            $prefix . 'faqcaptcha',
230            $prefix . 'faqcategories',
231            $prefix . 'faqcategory_group',
232            $prefix . 'faqcategory_news',
233            $prefix . 'faqcategory_user',
234            $prefix . 'faqcategoryrelations',
235            $prefix . 'faqchanges',
236            $prefix . 'faqcomments',
237            $prefix . 'faqconfig',
238            $prefix . 'faqdata',
239            $prefix . 'faqdata_group',
240            $prefix . 'faqdata_revisions',
241            $prefix . 'faqdata_tags',
242            $prefix . 'faqdata_user',
243            $prefix . 'faqglossary',
244            $prefix . 'faqgroup',
245            $prefix . 'faqgroup_right',
246            $prefix . 'faqinstances',
247            $prefix . 'faqinstances_config',
248            $prefix . 'faqmeta',
249            $prefix . 'faqnews',
250            $prefix . 'faqquestions',
251            $prefix . 'faqright',
252            $prefix . 'faqsearches',
253            $prefix . 'faqsections',
254            $prefix . 'faqsection_group',
255            $prefix . 'faqsection_news',
256            $prefix . 'faqsessions',
257            $prefix . 'faqstopwords',
258            $prefix . 'faqtags',
259            $prefix . 'faquser',
260            $prefix . 'faquser_group',
261            $prefix . 'faquser_right',
262            $prefix . 'faquserdata',
263            $prefix . 'faquserlogin',
264            $prefix . 'faqvisits',
265            $prefix . 'faqvoting',
266        ];
267    }
268
269    /**
270     * Returns just one row.
271     *
272     * @param string $query
273     *
274     * @return string
275     */
276    private function getOne($query): string
277    {
278        $row = $this->conn->query($query)->fetch_row();
279
280        return $row[0];
281    }
282
283    /**
284     * This function is a replacement for MySQL's auto-increment so that
285     * we don't need it anymore.
286     *
287     * @param string $table The name of the table
288     * @param string $id    The name of the ID column
289     *
290     * @return int
291     */
292    public function nextId($table, $id): int
293    {
294        $select = sprintf(
295            '
296           SELECT
297               MAX(%s) AS current_id
298           FROM
299               %s',
300            $id,
301            $table
302        );
303
304        $result = $this->query($select);
305
306        if ($result instanceof mysqli_result) {
307            $current = $result->fetch_row();
308        } else {
309            $current = [0];
310        }
311
312        return $current[0] + 1;
313    }
314
315    /**
316     * This function sends a query to the database.
317     *
318     * @param string $query
319     * @param int $offset
320     * @param int $rowcount
321     *
322     * @return mysqli_result $result
323     */
324    public function query(string $query, $offset = 0, $rowcount = 0)
325    {
326        if (DEBUG) {
327            $this->sqllog .= Utils::debug($query);
328        }
329
330        if (0 < $rowcount) {
331            $query .= sprintf(' LIMIT %d,%d', $offset, $rowcount);
332        }
333
334        $result = $this->conn->query($query);
335
336        if (false === $result) {
337            $this->sqllog .= $this->conn->errno . ': ' . $this->error();
338        }
339
340        return $result;
341    }
342
343    /**
344     * Returns the client version string.
345     *
346     * @return string
347     */
348    public function clientVersion(): string
349    {
350        return $this->conn->get_client_info();
351    }
352
353    /**
354     * Returns the server version string.
355     *
356     * @return string
357     */
358    public function serverVersion(): string
359    {
360        return $this->conn->server_info;
361    }
362
363    /**
364     * Closes the connection to the database.
365     */
366    public function close()
367    {
368        if (is_resource($this->conn)) {
369            $this->conn->close();
370        }
371    }
372
373    /**
374     * Destructor.
375     */
376    public function __destruct()
377    {
378        if (is_resource($this->conn)) {
379            $this->conn->close();
380        }
381    }
382
383    /**
384     * @return string
385     */
386    public function now(): string
387    {
388        return 'NOW()';
389    }
390}
391