1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26
27/**
28 * Class DbMySQLiCore.
29 *
30 * @since 1.5.0,1
31 */
32class DbMySQLiCore extends Db
33{
34    /** @var mysqli */
35    protected $link;
36
37    /** @var mysqli_result */
38    protected $result;
39
40    /**
41     * Tries to connect to the database.
42     *
43     * @see DbCore::connect()
44     *
45     * @return mysqli
46     *
47     * @throws PrestaShopDatabaseException
48     */
49    public function connect()
50    {
51        $socket = false;
52        $port = false;
53        if (Tools::strpos($this->server, ':') !== false) {
54            list($server, $port) = explode(':', $this->server);
55            if (is_numeric($port) === false) {
56                $socket = $port;
57                $port = false;
58            }
59        } elseif (Tools::strpos($this->server, '/') !== false) {
60            $socket = $this->server;
61        }
62
63        if ($socket) {
64            $this->link = @new mysqli(null, $this->user, $this->password, $this->database, null, $socket);
65        } elseif ($port) {
66            $this->link = @new mysqli($server, $this->user, $this->password, $this->database, $port);
67        } else {
68            $this->link = @new mysqli($this->server, $this->user, $this->password, $this->database);
69        }
70
71        // Do not use object way for error because this work bad before PHP 5.2.9
72        if (mysqli_connect_error()) {
73            throw new PrestaShopDatabaseException(sprintf(Tools::displayError('Link to database cannot be established: %s'), mysqli_connect_error()));
74        }
75
76        // UTF-8 support
77        if (!$this->link->query('SET NAMES utf8mb4')) {
78            throw new PrestaShopDatabaseException(Tools::displayError('PrestaShop Fatal error: no utf-8 support. Please check your server configuration.'));
79        }
80
81        $this->link->query('SET SESSION sql_mode = \'\'');
82
83        return $this->link;
84    }
85
86    /**
87     * Tries to connect and create a new database.
88     *
89     * @param string $host
90     * @param string|null $user
91     * @param string|null $password
92     * @param string|null $database
93     * @param bool $dropit if true, drops the created database
94     *
95     * @return bool|mysqli_result
96     */
97    public static function createDatabase($host, $user = null, $password = null, $database = null, $dropit = false)
98    {
99        if (strpos($host, ':') !== false) {
100            list($host, $port) = explode(':', $host);
101            $link = @new mysqli($host, $user, $password, null, $port);
102        } else {
103            $link = @new mysqli($host, $user, $password);
104        }
105        $success = $link->query('CREATE DATABASE `' . str_replace('`', '\\`', $database) . '`');
106        if ($dropit && ($link->query('DROP DATABASE `' . str_replace('`', '\\`', $database) . '`') !== false)) {
107            return true;
108        }
109
110        return $success;
111    }
112
113    /**
114     * Destroys the database connection link.
115     *
116     * @see DbCore::disconnect()
117     */
118    public function disconnect()
119    {
120        @$this->link->close();
121    }
122
123    /**
124     * Executes an SQL statement, returning a result set as a mysqli_result object or true/false.
125     *
126     * @see DbCore::_query()
127     *
128     * @param string $sql
129     *
130     * @return bool|mysqli_result
131     */
132    protected function _query($sql)
133    {
134        return $this->link->query($sql);
135    }
136
137    /**
138     * Returns the next row from the result set.
139     *
140     * @see DbCore::nextRow()
141     *
142     * @param bool|mysqli_result $result
143     *
144     * @return array|bool
145     */
146    public function nextRow($result = false)
147    {
148        if (!$result) {
149            $result = $this->result;
150        }
151
152        if (!is_object($result)) {
153            return false;
154        }
155
156        return $result->fetch_assoc();
157    }
158
159    /**
160     * Returns all rows from the result set.
161     *
162     * @see DbCore::getAll()
163     *
164     * @param bool|mysqli_result $result
165     *
166     * @return array|false
167     */
168    protected function getAll($result = false)
169    {
170        if (!$result) {
171            $result = $this->result;
172        }
173
174        if (!is_object($result)) {
175            return false;
176        }
177
178        if (method_exists($result, 'fetch_all')) {
179            return $result->fetch_all(MYSQLI_ASSOC);
180        } else {
181            $ret = [];
182
183            while ($row = $this->nextRow($result)) {
184                $ret[] = $row;
185            }
186
187            return $ret;
188        }
189    }
190
191    /**
192     * Returns row count from the result set.
193     *
194     * @see DbCore::_numRows()
195     *
196     * @param bool|mysqli_result $result
197     *
198     * @return int
199     */
200    protected function _numRows($result)
201    {
202        return $result->num_rows;
203    }
204
205    /**
206     * Returns ID of the last inserted row.
207     *
208     * @see DbCore::Insert_ID()
209     *
210     * @return string|int
211     */
212    public function Insert_ID()
213    {
214        return $this->link->insert_id;
215    }
216
217    /**
218     * Return the number of rows affected by the last SQL query.
219     *
220     * @see DbCore::Affected_Rows()
221     *
222     * @return int
223     */
224    public function Affected_Rows()
225    {
226        return $this->link->affected_rows;
227    }
228
229    /**
230     * Returns error message.
231     *
232     * @see DbCore::getMsgError()
233     *
234     * @param bool $query
235     *
236     * @return string
237     */
238    public function getMsgError($query = false)
239    {
240        return $this->link->error;
241    }
242
243    /**
244     * Returns error code.
245     *
246     * @see DbCore::getNumberError()
247     *
248     * @return int
249     */
250    public function getNumberError()
251    {
252        return $this->link->errno;
253    }
254
255    /**
256     * Returns database server version.
257     *
258     * @see DbCore::getVersion()
259     *
260     * @return string
261     */
262    public function getVersion()
263    {
264        return $this->getValue('SELECT VERSION()');
265    }
266
267    /**
268     * Escapes illegal characters in a string.
269     *
270     * @see DbCore::_escape()
271     *
272     * @param string $str
273     *
274     * @return string
275     */
276    public function _escape($str)
277    {
278        return $this->link->real_escape_string($str);
279    }
280
281    /**
282     * Switches to a different database.
283     *
284     * @see DbCore::set_db()
285     *
286     * @param string $db_name
287     *
288     * @return bool
289     */
290    public function set_db($db_name)
291    {
292        return $this->link->query('USE `' . bqSQL($db_name) . '`');
293    }
294
295    /**
296     * Try a connection to the database and check if at least one table with same prefix exists.
297     *
298     * @see Db::hasTableWithSamePrefix()
299     *
300     * @param string $server Server address
301     * @param string $user Login for database connection
302     * @param string $pwd Password for database connection
303     * @param string $db Database name
304     * @param string $prefix Tables prefix
305     *
306     * @return bool
307     */
308    public static function hasTableWithSamePrefix($server, $user, $pwd, $db, $prefix)
309    {
310        $link = @new mysqli($server, $user, $pwd, $db);
311        if (mysqli_connect_error()) {
312            return false;
313        }
314
315        $sql = 'SHOW TABLES LIKE \'' . $prefix . '%\'';
316        $result = $link->query($sql);
317
318        return (bool) $result->fetch_assoc();
319    }
320
321    /**
322     * Try a connection to the database.
323     *
324     * @see Db::checkConnection()
325     *
326     * @param string $server Server address
327     * @param string $user Login for database connection
328     * @param string $pwd Password for database connection
329     * @param string $db Database name
330     * @param bool $newDbLink
331     * @param string|bool $engine
332     * @param int $timeout
333     *
334     * @return int Error code or 0 if connection was successful
335     */
336    public static function tryToConnect($server, $user, $pwd, $db, $new_db_link = true, $engine = null, $timeout = 5)
337    {
338        $link = mysqli_init();
339        if (!$link) {
340            return -1;
341        }
342
343        if (!$link->options(MYSQLI_OPT_CONNECT_TIMEOUT, $timeout)) {
344            return 1;
345        }
346
347        // There is an @ because mysqli throw a warning when the database does not exists
348        if (!@$link->real_connect($server, $user, $pwd, $db)) {
349            return (mysqli_connect_errno() == 1049) ? 2 : 1;
350        }
351
352        $link->close();
353
354        return 0;
355    }
356
357    /**
358     * Selects best table engine.
359     *
360     * @return string
361     */
362    public function getBestEngine()
363    {
364        $value = 'InnoDB';
365
366        $sql = 'SHOW VARIABLES WHERE Variable_name = \'have_innodb\'';
367        $result = $this->link->query($sql);
368        if (!$result) {
369            $value = 'MyISAM';
370        }
371        $row = $result->fetch_assoc();
372        if (!$row || strtolower($row['Value']) != 'yes') {
373            $value = 'MyISAM';
374        }
375
376        /* MySQL >= 5.6 */
377        $sql = 'SHOW ENGINES';
378        $result = $this->link->query($sql);
379        while ($row = $result->fetch_assoc()) {
380            if ($row['Engine'] == 'InnoDB') {
381                if (in_array($row['Support'], ['DEFAULT', 'YES'])) {
382                    $value = 'InnoDB';
383                }
384
385                break;
386            }
387        }
388
389        return $value;
390    }
391
392    /**
393     * Tries to connect to the database and create a table (checking creation privileges).
394     *
395     * @param string $server
396     * @param string $user
397     * @param string $pwd
398     * @param string $db
399     * @param string $prefix
400     * @param string|null $engine Table engine
401     *
402     * @return bool|string True, false or error
403     */
404    public static function checkCreatePrivilege($server, $user, $pwd, $db, $prefix, $engine = null)
405    {
406        $link = @new mysqli($server, $user, $pwd, $db);
407        if (mysqli_connect_error()) {
408            return false;
409        }
410
411        $enginesToTest = ['InnoDB', 'MyISAM'];
412        if ($engine !== null) {
413            $enginesToTest = [$engine];
414        }
415
416        foreach ($enginesToTest as $engineToTest) {
417            $result = $link->query('
418            CREATE TABLE `' . $prefix . 'test` (
419                `test` tinyint(1) unsigned NOT NULL
420            ) ENGINE=' . $engineToTest);
421
422            if ($result) {
423                $link->query('DROP TABLE `' . $prefix . 'test`');
424
425                return true;
426            }
427        }
428
429        return $link->error;
430    }
431
432    /**
433     * Tries to connect to the database and select content (checking select privileges).
434     *
435     * @param string $server
436     * @param string $user
437     * @param string $pwd
438     * @param string $db
439     * @param string $prefix
440     * @param string|null $engine Table engine
441     *
442     * @return bool|string True, false or error
443     */
444    public static function checkSelectPrivilege($server, $user, $pwd, $db, $prefix, $engine = null)
445    {
446        $link = @new mysqli($server, $user, $pwd, $db);
447        if (mysqli_connect_error()) {
448            return false;
449        }
450
451        if ($engine === null) {
452            $engine = 'MyISAM';
453        }
454
455        // Create a table
456        $link->query('
457		CREATE TABLE `' . $prefix . 'test` (
458			`test` tinyint(1) unsigned NOT NULL
459		) ENGINE=' . $engine);
460
461        // Select content
462        $result = $link->query('SELECT * FROM `' . $prefix . 'test`');
463
464        // Drop the table
465        $link->query('DROP TABLE `' . $prefix . 'test`');
466
467        if (!$result) {
468            return $link->error;
469        }
470
471        return true;
472    }
473
474    /**
475     * Try a connection to the database and set names to UTF-8.
476     *
477     * @see Db::checkEncoding()
478     *
479     * @param string $server Server address
480     * @param string $user Login for database connection
481     * @param string $pwd Password for database connection
482     *
483     * @return bool
484     */
485    public static function tryUTF8($server, $user, $pwd)
486    {
487        $link = @new mysqli($server, $user, $pwd);
488        $ret = $link->query('SET NAMES utf8mb4');
489        $link->close();
490
491        return $ret;
492    }
493}
494