<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/*EXTRA FUNCTIONS: mysqli\_.+*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    core_database_drivers
 */

require_code('database/shared/mysql');

/**
 * Database Driver.
 *
 * @package    core_database_drivers
 */
class Database_Static_mysqli extends Database_super_mysql
{
    public $cache_db = array();
    public $last_select_db = null;
    public $reconnected_once = false;

    /**
     * Get a database connection. This function shouldn't be used by you, as a connection to the database is established automatically.
     *
     * @param  boolean $persistent Whether to create a persistent connection
     * @param  string $db_name The database name
     * @param  string $db_host The database host (the server)
     * @param  string $db_user The database connection username
     * @param  string $db_password The database connection password
     * @param  boolean $fail_ok Whether to on error echo an error and return with a null, rather than giving a critical error
     * @return ?array A database connection (note for MySQL, it's actually a pair, containing the database name too: because we need to select the name before each query on the connection) (null: error)
     */
    public function db_get_connection($persistent, $db_name, $db_host, $db_user, $db_password, $fail_ok = false)
    {
        if (!function_exists('mysqli_connect')) {
            $error = 'The \'mysqli\' PHP extension is not installed (anymore?). You need to contact the system administrator of this server, or use a different MySQL database driver (drivers can be chosen by editing _config.php).';
            if ($fail_ok) {
                echo ((running_script('install')) && (get_param_string('type', '') == 'ajax_db_details')) ? strip_html($error) : $error;
                return null;
            }
            critical_error('PASSON', $error);
        }

        // Potential caching
        $x = serialize(array($db_user, $db_host));
        if (array_key_exists($x, $this->cache_db)) {
            if ($this->last_select_db[1] !== $db_name) {
                mysqli_select_db($this->cache_db[$x], $db_name);
                $this->last_select_db = array($this->cache_db[$x], $db_name);
            }

            return array($this->cache_db[$x], $db_name);
        }

        mysqli_report(MYSQLI_REPORT_OFF);

        // Connect
        $db_port = 3306;
        if (strpos($db_host, ':') !== false) {
            list($db_host, $_db_port) = explode(':', $db_host);
            $db_port = intval($_db_port);
        }
        $db = @mysqli_connect(($persistent ? 'p:' : '') . $db_host, $db_user, $db_password, '', $db_port);

        if ($db === false) {
            $error = 'Could not connect to database-server (when authenticating) (' . mysqli_connect_error() . ')';
            if ($fail_ok) {
                echo ((running_script('install')) && (get_param_string('type', '') == 'ajax_db_details')) ? strip_html($error) : $error;
                return null;
            }
            critical_error('PASSON', $error); //warn_exit(do_lang_tempcode('CONNECT_DB_ERROR'));
        }
        if (!mysqli_select_db($db, $db_name)) {
            if ($db_user == 'root') {
                @mysqli_query($db, 'CREATE DATABASE IF NOT EXISTS ' . $db_name);
            }

            if (!mysqli_select_db($db, $db_name)) {
                $error = 'Could not connect to database (' . mysqli_error($db) . ')';
                if ($fail_ok) {
                    echo $error . "\n";
                    return null;
                }
                critical_error('PASSON', $error); //warn_exit(do_lang_tempcode('CONNECT_ERROR'));
            }
        }
        $this->last_select_db = array($db, $db_name);

        $this->cache_db[$x] = $db;

        global $SITE_INFO;
        if (empty($SITE_INFO['database_charset'])) {
            $SITE_INFO['database_charset'] = (get_charset() == 'utf-8') ? 'utf8mb4' : 'latin1';
        }
        if (function_exists('mysqli_set_charset')) {
            $test = @mysqli_set_charset($db, $SITE_INFO['database_charset']);
            if ((!$test) && ($SITE_INFO['database_charset'] == 'utf8mb4')) {
                // Conflict between compiled-in MySQL client library and what the server supports
                $test = @mysqli_set_charset($db, 'utf8');
                @mysqli_query($db, 'SET NAMES "' . addslashes('utf8mb4') . '"');
            }
        } else {
            @mysqli_query($db, 'SET NAMES "' . addslashes($SITE_INFO['database_charset']) . '"');
        }
        if (!empty($SITE_INFO['database_collation'])) {
            @mysqli_query($db, 'SET collation_connection=' . $SITE_INFO['database_collation']);
        }

        @mysqli_query($db, 'SET wait_timeout=28800');
        @mysqli_query($db, 'SET sql_big_selects=1');
        if ((get_forum_type() == 'cns') && (!$GLOBALS['IN_MINIKERNEL_VERSION'])) {
            @mysqli_query($db, 'SET sql_mode=\'STRICT_ALL_TABLES\'');
        } else {
            $test = @mysqli_query($db, 'SET sql_mode=\'MYSQL40\''); // We may be in some legacy context, such as backup restoration, upgrader, or another forum driver
            if ($test === false) { // Won't work on MySQL 8 for example
                @mysqli_query($db, 'SET sql_mode=\'STRICT_ALL_TABLES\'');
            }
        }
        // NB: Can add ,ONLY_FULL_GROUP_BY for testing on what other DBs will do, but can_arbitrary_groupby() would need to be made to return false

        return array($db, $db_name);
    }

    /**
     * Find whether full-text-search is present
     *
     * @param  array $db A DB connection
     * @return boolean Whether it is
     */
    public function db_has_full_text($db)
    {
        return true;
    }

    /**
     * Find whether subquery support is present
     *
     * @param  array $db A DB connection
     * @return boolean Whether it is
     */
    public function db_has_subqueries($db)
    {
        return true;
    }

    /**
     * Find whether collate support is present
     *
     * @param  array $db A DB connection
     * @return boolean Whether it is
     */
    public function db_has_collate_settings($db)
    {
        return true;
    }

    /**
     * Find whether full-text-boolean-search is present
     *
     * @return boolean Whether it is
     */
    public function db_has_full_text_boolean()
    {
        return true;
    }

    /**
     * Escape a string so it may be inserted into a query. If SQL statements are being built up and passed using db_query then it is essential that this is used for security reasons. Otherwise, the abstraction layer deals with the situation.
     *
     * @param  string $string The string
     * @return string The escaped string
     */
    public function db_escape_string($string)
    {
        if (function_exists('ctype_alnum')) {
            if (ctype_alnum($string)) {
                return $string; // No non-trivial characters
            }
        }
        if (preg_match('#[^a-zA-Z0-9\.]#', $string) === 0) {
            return $string; // No non-trivial characters
        }

        $string = fix_bad_unicode($string);

        if ($this->last_select_db === null) {
            return addslashes($string);
        }
        return mysqli_real_escape_string($this->last_select_db[0], $string);
    }

    /**
     * This function is a very basic query executor. It shouldn't usually be used by you, as there are abstracted versions available.
     *
     * @param  string $query The complete SQL query
     * @param  array $db_parts A DB connection
     * @param  ?integer $max The maximum number of rows to affect (null: no limit)
     * @param  ?integer $start The start row to affect (null: no specification)
     * @param  boolean $fail_ok Whether to output an error on failure
     * @param  boolean $get_insert_id Whether to get the autoincrement ID created for an insert query
     * @return ?mixed The results (null: no results), or the insert ID
     */
    public function db_query($query, $db_parts, $max = null, $start = null, $fail_ok = false, $get_insert_id = false)
    {
        list($db, $db_name) = $db_parts;

        if (isset($query[500000])) { // Let's hope we can fail on this, because it's a huge query. We can only allow it if MySQL can.
            $test_result = $this->db_query('SHOW VARIABLES LIKE \'max_allowed_packet\'', $db_parts, null, null, true);

            if (!is_array($test_result)) {
                return null;
            }
            if (intval($test_result[0]['Value']) < intval(strlen($query) * 1.2)) {
                if ($get_insert_id) {
                    fatal_exit(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html($query), escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))));
                } else {
                    attach_message(do_lang_tempcode('QUERY_FAILED_TOO_BIG', escape_html(substr($query, 0, 300)) . '...', escape_html(integer_format(strlen($query))), escape_html(integer_format(intval($test_result[0]['Value'])))), 'warn');
                }
                return null;
            }
        }

        if ($this->last_select_db[1] !== $db_name) {
            mysqli_select_db($db, $db_name);
            $this->last_select_db = array($db, $db_name);
        }

        static $version = null;
        if ($version === null) {
            $version = mysqli_get_server_version($db);
        }
        if ($version >= 80000) {
            $query = $this->fix_mysql8_query($query);
        }

        $this->apply_sql_limit_clause($query, $max, $start);

        $results = @mysqli_query($db, $query);
        if (($results === false) && ((!$fail_ok) || (strpos(mysqli_error($db), 'is marked as crashed and should be repaired') !== false))) {
            $err = mysqli_error($db);

            if ((function_exists('mysqli_ping')) && ($err == 'MySQL server has gone away') && (!$this->reconnected_once)) {
                safe_ini_set('mysqli.reconnect', '1');
                $this->reconnected_once = true;
                mysqli_ping($db);
                $ret = $this->db_query($query, $db_parts, null/*already encoded*/, null/*already encoded*/, $fail_ok, $get_insert_id);
                $this->reconnected_once = false;
                return $ret;
            }

            if (function_exists('ocp_mark_as_escaped')) {
                ocp_mark_as_escaped($err);
            }
            if ((!running_script('upgrader')) && ((!get_mass_import_mode()) || (get_param_integer('keep_fatalistic', 0) == 1)) && (strpos($err, 'Duplicate entry') === false)) {
                $matches = array();
                if (preg_match('#/(\w+)\' is marked as crashed and should be repaired#U', $err, $matches) !== 0) {
                    $this->db_query('REPAIR TABLE ' . $matches[1], $db_parts);
                }

                if (!function_exists('do_lang') || is_null(do_lang('QUERY_FAILED', null, null, null, null, false))) {
                    fatal_exit(htmlentities('Query failed: ' . $query . ' : ' . $err));
                }
                fatal_exit(do_lang_tempcode('QUERY_FAILED', escape_html($query), ($err)));
            } else {
                echo htmlentities('Database query failed: ' . $query . ' [') . ($err) . htmlentities(']') . "<br />\n";
                return null;
            }
        }

        $sub = substr(ltrim($query), 0, 4);
        if (($results !== true) && (($sub === '(SEL') || ($sub === 'SELE') || ($sub === 'sele') || ($sub === 'CHEC') || ($sub === 'EXPL') || ($sub === 'REPA') || ($sub === 'DESC') || ($sub === 'SHOW')) && ($results !== false)) {
            return $this->db_get_query_rows($results, $query, $start);
        }

        if ($get_insert_id) {
            if (strtoupper(substr($query, 0, 7)) === 'UPDATE ') {
                return mysqli_affected_rows($db);
            }
            $ins = mysqli_insert_id($db);
            if ($ins === 0) {
                $table = substr($query, 12, strpos($query, ' ', 12) - 12);
                $rows = $this->db_query('SELECT MAX(id) AS x FROM ' . $table, $db_parts, 1, 0, false, false);
                return $rows[0]['x'];
            }
            return $ins;
        }

        return null;
    }

    /**
     * Get the rows returned from a SELECT query.
     *
     * @param  resource $results The query result pointer
     * @param  string $query The complete SQL query (useful for debugging)
     * @param  ?integer $start Whether to start reading from (null: irrelevant)
     * @return array A list of row maps
     */
    public function db_get_query_rows($results, $query, $start = null)
    {
        $names = array();
        $types = array();
        $fields = mysqli_fetch_fields($results);
        foreach ($fields as $x => $field) {
            $names[$x] = $field->name;
            $types[$x] = $field->type;
        }

        $out = array();
        $newrow = array();
        while (($row = mysqli_fetch_row($results)) !== null) {
            $j = 0;
            foreach ($row as $v) {
                $name = $names[$j];
                $type = $types[$j];

                if (($type === 1) || ($type === 2) || ($type === 3) || ($type === 8) || ($type === 9)) { // Integer field of some kind
                    if ($v === null) {
                        $newrow[$name] = null;
                    } else {
                        $newrow[$name] = intval($v);
                    }
                } elseif (($type === 4) || ($type === 5) || ($type === 246)) { // Decimal field of some kind
                    if ($v === null) {
                        $newrow[$name] = null;
                    } else {
                        $newrow[$name] = floatval($v);
                    }
                } elseif ($type === 16) { // Bit field
                    if ((strlen($v) === 1) && (ord($v[0]) <= 1)) {
                        $newrow[$name] = ord($v); // 0/1 char format
                    } else {
                        $newrow[$name] = intval($v); // Int-as-string format
                    }
                } else {
                    $newrow[$name] = $v;
                }

                $j++;
            }

            $out[] = $newrow;
        }
        mysqli_free_result($results);

        return $out;
    }
}
