Visman 8 年之前
父节点
当前提交
dc51f88896

+ 2 - 2
app/Core/Cache/FileCache.php

@@ -146,10 +146,10 @@ class FileCache implements ProviderCacheInterface
      */
     protected function file($key)
     {
-        if (is_string($key) && preg_match('%^[\w-]+$%D', $key)) {
+        if (is_string($key) && preg_match('%^[a-z0-9_-]+$%Di', $key)) {
             return $this->cacheDir . '/cache_' . $key . '.php';
         }
-        throw new InvalidArgumentException("`$key`: Key contains invalid characters");
+        throw new InvalidArgumentException("Key '$key' contains invalid characters.");
     }
 
     /**

+ 2 - 2
app/Core/Container.php

@@ -173,11 +173,11 @@ class Container
         if (is_string($value)) {
             if (strpos($value, '%') !== false) {
                 // whole string substitution can return any type of value
-                if (preg_match('~^%(\w+(?:\.\w+)*)%$~', $value, $matches)) {
+                if (preg_match('~^%([a-z0-9_]+(?:\.[a-z0-9_]+)*)%$~i', $value, $matches)) {
                     $value = $this->__get($matches[1]);
                 } else {
                     // partial string substitution casts value to string
-                    $value = preg_replace_callback('~%(\w+(?:\.\w+)*)%~',
+                    $value = preg_replace_callback('~%([a-z0-9_]+(?:\.[a-z0-9_]+)*)%~i',
                         function ($matches) {
                             return $this->__get($matches[1]);
                         }, $value);

+ 293 - 0
app/Core/DB.php

@@ -0,0 +1,293 @@
+<?php
+
+namespace ForkBB\Core;
+
+use PDO;
+use PDOStatement;
+use PDOException;
+
+class DB extends PDO
+{
+    /**
+     * Префикс для таблиц базы
+     * @var string
+     */
+    protected $dbPrefix;
+
+    /**
+     * Тип базы данных
+     * @var string
+     */
+    protected $dbType;
+
+    /**
+     * Драйвер текущей базы
+     * @var //????
+     */
+    protected $dbDrv;
+
+    /**
+     * Конструктор
+     *
+     * @param string $dsn
+     * @param string $username
+     * @param string $password
+     * @param array $options
+     * @param string $prefix
+     *
+     * @throws PDOException
+     */
+    public function __construct($dsn, $username = null, $password = null, array $options = [], $prefix = '')
+    {
+        $type = strstr($dsn, ':', true);
+        if (! $type || ! file_exists(__DIR__ . '/DB/' . ucfirst($type) . '.php')) {
+            throw new PDOException("For '$type' the driver isn't found.");
+        }
+        $this->dbType = $type;
+
+        $this->dbPrefix = $prefix;
+        $options += [
+            self::ATTR_DEFAULT_FETCH_MODE => self::FETCH_ASSOC,
+            self::ATTR_EMULATE_PREPARES   => false,
+            self::ATTR_ERRMODE            => self::ERRMODE_EXCEPTION,
+        ];
+
+        parent::__construct($dsn, $username, $password, $options);
+    }
+
+    /**
+     * Передает вызовы методов в драйвер текущей базы
+     *
+     * @param string $name
+     * @param array $args
+     *
+     * @return mixed
+     */
+    public function __call($name, array $args)
+    {
+        if (empty($this->dbDrv)) {
+            $drv = 'ForkBB\\Core\\DB\\' . ucfirst($this->dbType);
+            $this->dbDrv = new $drv($this, $this->dbPrefix);
+        }
+        return $this->dbDrv->$name(...$args);
+    }
+
+    /**
+     * Метод определяет массив ли опций подан на вход
+     *
+     * @param array $options
+     *
+     * @return bool
+     */
+    protected function isOptions(array $arr)
+    {
+        $verify = [self::ATTR_CURSOR => [self::CURSOR_FWDONLY, self::CURSOR_SCROLL]];
+
+        foreach ($arr as $key => $value) {
+           if (! isset($verify[$key]) || ! in_array($value, $verify[$key])) {
+               return false;
+           }
+        }
+        return true;
+    }
+
+    /**
+     * Метод приводит запрос с типизированными плейсхолдерами к понятному для PDO виду
+     *
+     * @param string $query
+     * @param array $params
+     *
+     * @throws PDOException
+     *
+     * @return array
+     */
+    protected function parse($query, array $params)
+    {
+        $parts = preg_split('%(?=[?:])(?<![a-z0-9_])(\?[a-z0-9_]+|\?(?!=:))?(::?[a-z0-9_]+)?%i', $query, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+        $idxIn = 0;
+        $idxOut = 1;
+        $query = '';
+        $total = count($parts);
+        $map = [];
+        $bind = [];
+
+        for ($i = 0; $i < $total; ++$i) {
+            switch ($parts[$i][0]) {
+                case '?':
+                    $type = isset($parts[$i][1]) ? substr($parts[$i], 1) : 's';
+                    $key = isset($parts[$i + 1]) && $parts[$i + 1][0] === ':'
+                           ? $parts[++$i]
+                           : $idxIn++;
+                    break;
+                case ':':
+                    if ($parts[$i][1] === ':') {
+                        $query .= $this->dbPrefix . substr($parts[$i], 2);
+                        continue 2;
+                    }
+                    $type = 's';
+                    $key = $parts[$i];
+                    break;
+                default:
+                    $query .= $parts[$i];
+                    continue 2;
+            }
+
+            if (! isset($params[$key])) {
+                throw new PDOException("'$key': No parameter for (?$type) placeholder");
+            }
+
+            switch ($type) {
+                case 'p':
+                    $query .= (string) $params[$key];
+                    continue 2;
+                case 'as':
+                case 'ai':
+                case 'a':
+                    $bindType = $type === 'ai' ? self::PARAM_INT : self::PARAM_STR;
+                    $comma = '';
+                    foreach ($params[$key] as $val) {
+                        $name = ':' . $idxOut++;
+                        $query .= $comma . $name;
+                        $bind[$name] = [$val, $bindType];
+                        $comma = ',';
+
+                        if (empty($map[$key])) {
+                            $map[$key] = [$type, $name];
+                        } else {
+                            $map[$key][] = $name;
+                        }
+                    }
+                    continue 2;
+                case '':
+                    break;
+                case 'i':
+                    $bindType = self::PARAM_INT;
+                    break;
+                case 'b':
+                    $bindType = self::PARAM_BOOL;
+                    break;
+                case 's':
+                default:
+                    $bindType = self::PARAM_STR;
+                    $type = 's';
+                    break;
+            }
+
+            $name = ':' . $idxOut++;
+            $query .= $name;
+            $bind[$name] = [$params[$key], $bindType];
+
+            if (empty($map[$key])) {
+                $map[$key] = [$type, $name];
+            } else {
+                $map[$key][] = $name;
+            }
+        }
+
+        return [$query, $bind, $map];
+    }
+
+    /**
+     * Метод связывает параметры запроса с соответвтующими значениями
+     *
+     * @param PDOStatement $stmt
+     * @param array $bind
+     */
+    protected function bind(PDOStatement $stmt, array $bind)
+    {
+        foreach ($bind as $key => $val) {
+            $stmt->bindValue($key, $val[0], $val[1]);
+        }
+    }
+
+    /**
+     * Метод расширяет PDO::exec()
+     *
+     * @param string $query
+     * @param array $params
+     *
+     * @return int|false
+     */
+    public function exec($query, array $params = [])
+    {
+        list($query, $bind, ) = $this->parse($query, $params);
+
+        if (empty($bind)) {
+            return parent::exec($query);
+        }
+
+        $stmt = parent::prepare($query);
+        $this->bind($stmt, $bind);
+
+        if ($stmt->execute()) {
+            return $stmt->rowCount(); //??? Для запроса SELECT... не ясно поведение!
+        }
+
+        return false;
+    }
+
+    /**
+     * Метод расширяет PDO::prepare()
+     *
+     * @param string $query
+     * @param array $arg1
+     * @param array $arg2
+     *
+     * @return PDOStatement
+     */
+    public function prepare($query, $arg1 = null, $arg2 = null)
+    {
+        if (empty($arg1) === empty($arg2) || ! empty($arg2)) {
+            $params = $arg1;
+            $options = $arg2;
+        } elseif ($this->isOptions($arg1)) {
+            $params = [];
+            $options = $arg1;
+        } else {
+            $params = $arg1;
+            $options = [];
+        }
+
+        list($query, $bind, $map) = $this->parse($query, $params);
+        $stmt = parent::prepare($query, $options);
+        $this->bind($stmt, $bind);
+
+        return $stmt;
+    }
+
+    /**
+     * Метод расширяет PDO::query()
+     *
+     * @param string $query
+     * @param mixed ...$args
+     *
+     * @return PDOStatement|false
+     */
+    public function query($query, ...$args)
+    {
+        if (isset($args[0]) && is_array($args[0])) {
+            $params = array_shift($args);
+        } else {
+            $params = [];
+        }
+
+        list($query, $bind, ) = $this->parse($query, $params);
+
+        if (empty($bind)) {
+            return parent::query($query, ...$args);
+        }
+
+        $stmt = parent::prepare($query);
+        $this->bind($stmt, $bind);
+
+        if ($stmt->execute()) {
+            if (! empty($args)) {
+                $stmt->setFetchMode(...$args);
+            }
+
+            return $stmt;
+        }
+
+        return false;
+    }
+}

+ 511 - 361
app/Core/DB/mysql.php

@@ -1,374 +1,524 @@
 <?php
 
-/**
- * Copyright (C) 2008-2012 FluxBB
- * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB
- * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
- */
-
 namespace ForkBB\Core\DB;
 
-// Make sure we have built in support for MySQL
-if (!function_exists('mysql_connect'))
-	exit('This PHP environment doesn\'t have MySQL support built in. MySQL support is required if you want to use a MySQL database to run this forum. Consult the PHP documentation for further assistance.');
-
+use ForkBB\Core\DB;
+use PDO;
+use PDOStatement;
+use PDOException;
 
-class DBLayer
+class Mysql
 {
-	var $prefix;
-	var $link_id;
-	var $query_result;
-
-	var $saved_queries = array();
-	var $num_queries = 0;
-
-	var $error_no = false;
-	var $error_msg = 'Unknown';
-
-	var $datatype_transformations = array(
-		'%^SERIAL$%'	=>	'INT(10) UNSIGNED AUTO_INCREMENT'
-	);
-
-
-	function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect)
-	{
-		$this->prefix = $db_prefix;
-
-		if ($p_connect)
-			$this->link_id = @mysql_pconnect($db_host, $db_username, $db_password);
-		else
-			$this->link_id = @mysql_connect($db_host, $db_username, $db_password);
-
-		if ($this->link_id)
-		{
-			if (!@mysql_select_db($db_name, $this->link_id))
-				error('Unable to select database. MySQL reported: '.mysql_error(), __FILE__, __LINE__);
-		}
-		else
-			error('Unable to connect to MySQL server. MySQL reported: '.mysql_error(), __FILE__, __LINE__);
-
-		// Setup the client-server character set (UTF-8)
-		if (!defined('FORUM_NO_SET_NAMES'))
-			$this->set_names('utf8');
-
-		return $this->link_id;
-	}
-	
-
-	function start_transaction()
-	{
-		return;
-	}
-
-
-	function end_transaction()
-	{
-		return;
-	}
-
-
-	function query($sql, $unbuffered = false)
-	{
-		if (defined('PUN_SHOW_QUERIES'))
-			$q_start = microtime(true);
-
-		if ($unbuffered)
-			$this->query_result = @mysql_unbuffered_query($sql, $this->link_id);
-		else
-			$this->query_result = @mysql_query($sql, $this->link_id);
-
-		if ($this->query_result)
-		{
-			if (defined('PUN_SHOW_QUERIES'))
-				$this->saved_queries[] = array($sql, sprintf('%.5f', microtime(true) - $q_start));
-
-			++$this->num_queries;
-
-			return $this->query_result;
-		}
-		else
-		{
-			if (defined('PUN_SHOW_QUERIES'))
-				$this->saved_queries[] = array($sql, 0);
-
-			$this->error_no = @mysql_errno($this->link_id);
-			$this->error_msg = @mysql_error($this->link_id);
-
-			return false;
-		}
-	}
-
-
-	function result($query_id = 0, $row = 0, $col = 0)
-	{
-		return ($query_id) ? @mysql_result($query_id, $row, $col) : false;
-	}
-
-
-	function fetch_assoc($query_id = 0)
-	{
-		return ($query_id) ? @mysql_fetch_assoc($query_id) : false;
-	}
-
-
-	function fetch_row($query_id = 0)
-	{
-		return ($query_id) ? @mysql_fetch_row($query_id) : false;
-	}
-
-
-	function num_rows($query_id = 0)
-	{
-		return ($query_id) ? @mysql_num_rows($query_id) : false;
-	}
-
-
-	function affected_rows()
-	{
-		return ($this->link_id) ? @mysql_affected_rows($this->link_id) : false;
-	}
-
-
-	function insert_id()
-	{
-		return ($this->link_id) ? @mysql_insert_id($this->link_id) : false;
-	}
-
-
-	function get_num_queries()
-	{
-		return $this->num_queries;
-	}
-
-
-	function get_saved_queries()
-	{
-		return $this->saved_queries;
-	}
-
-
-	function free_result($query_id = false)
-	{
-		return ($query_id) ? @mysql_free_result($query_id) : false;
-	}
-
-
-	function escape($str)
-	{
-		if (is_array($str))
-			return '';
-		else if (function_exists('mysql_real_escape_string'))
-			return mysql_real_escape_string($str, $this->link_id);
-		else
-			return mysql_escape_string($str);
-	}
-
-
-	function error()
-	{
-		$result['error_sql'] = @current(@end($this->saved_queries));
-		$result['error_no'] = $this->error_no;
-		$result['error_msg'] = $this->error_msg;
-
-		return $result;
-	}
-
-
-	function close()
-	{
-		if ($this->link_id)
-		{
-			if (is_resource($this->query_result))
-				@mysql_free_result($this->query_result);
-
-			return @mysql_close($this->link_id);
-		}
-		else
-			return false;
-	}
-
-	function get_names()
-	{
-		$result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\'');
-		return $this->result($result, 0, 1);
-	}
-
-
-	function set_names($names)
-	{
-		return $this->query('SET NAMES \''.$this->escape($names).'\'');
-	}
-
-
-	function get_version()
-	{
-		$result = $this->query('SELECT VERSION()');
-
-		return array(
-			'name'		=> 'MySQL Standard',
-			'version'	=> preg_replace('%^([^-]+).*$%', '\\1', $this->result($result))
-		);
-	}
-
-
-	function table_exists($table_name, $no_prefix = false)
-	{
-		$result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\'');
-		return $this->num_rows($result) > 0;
-	}
-
-
-	function field_exists($table_name, $field_name, $no_prefix = false)
-	{
-		$result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\'');
-		return $this->num_rows($result) > 0;
-	}
-
-
-	function index_exists($table_name, $index_name, $no_prefix = false)
+    /**
+     * @var DB
+     */
+    protected $db;
+
+    /**
+     * Префикс для таблиц базы
+     * @var string
+     */
+    protected $dbPrefix;
+
+    /**
+     * Набор символов БД
+     * @var string
+     */
+    protected $dbCharSet;
+
+    /**
+     * Массив замены типов полей таблицы
+     * @var array
+     */
+    protected $dbTypeRepl = [
+        '%^SERIAL$%i' => 'INT(10) UNSIGNED AUTO_INCREMENT',
+    ];
+
+    /**
+     * Конструктор
+     *
+     * @param DB $db
+     * @param string $prefix
+     */
+    public function __construct(DB $db, $prefix)
+    {
+        $this->db = $db;
+        $this->dbPrefix = $prefix;
+    }
+
+    /**
+     * Перехват неизвестных методов
+     *
+     * @param string $name
+     * @param array $args
+     *
+     * @throws PDOException
+     */
+    public function __call($name, array $args)
+    {
+        throw new PDOException("Method '{$name}' not found in DB driver.");
+    }
+
+    /**
+     * Проверяет строку на допустимые символы
+     *
+     * @param string $str
+     *
+     * @throws PDOException
+     */
+    protected function testStr($str)
+    {
+        if (! is_string($str) || preg_match('%[^a-zA-Z0-9_]%', $str)) {
+            throw new PDOException("Name '{$str}' have bad characters.");
+        }
+    }
+
+    /**
+     * Операции над полями индексов: проверка, замена
+     *
+     * @param array $arr
+     *
+     * @return string
+     */
+    protected function replIdxs(array $arr)
+    {
+        foreach ($arr as &$value) {
+            if (preg_match('%^(.*)\s*(\(\d+\))$%', $value, $matches)) {
+                $this->testStr($matches[1]);
+                $value = "`{$matches[1]}`{$matches[2]}";
+            } else {
+                $this->testStr($value);
+                $value = "`{$value}`";
+            }
+            unset($value);
+        }
+        return implode(',', $arr);
+    }
+
+    /**
+     * Замена типа поля в соответствии с dbTypeRepl
+     *
+     * @param string $type
+     *
+     * @return string
+     */
+    protected function replType($type)
+    {
+        return preg_replace(array_keys($this->dbTypeRepl), array_values($this->dbTypeRepl), $type);
+    }
+
+    /**
+     * Конвертирует данные в строку для DEFAULT
+     *
+     * @param mixed $data
+     *
+     * @throws PDOException
+     *
+     * @return string
+     */
+    protected function convToStr($data) {
+        if (is_string($data)) {
+            return $this->db->quote($data);
+        } elseif (is_numeric($data)) {
+            return (string) $data;
+        } elseif (is_bool($data)) {
+            return $data ? 'true' : 'false';
+        } else {
+            throw new PDOException('Invalid data type for DEFAULT.');
+        }
+    }
+
+    /**
+     * Вовзращает набор символов БД
+     *
+     * @return string
+     */
+    protected function getCharSet()
+    {
+        if (! $this->dbCharSet) {
+            $stmt = $this->db->query("SHOW VARIABLES LIKE 'character\_set\_database'");
+            $this->dbCharSet = $stmt->fetchColumn(1);
+            $stmt->closeCursor();
+        }
+        return $this->dbCharSet;
+    }
+
+    /**
+     * Проверяет наличие таблицы в базе
+     *
+     * @param string $table
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function tableExists($table, $noPrefix = false)
+    {
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        try {
+            $stmt = $this->db->query('SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:table', [':table' => $table]);
+            $result = $stmt->fetch();
+            $stmt->closeCursor();
+        } catch (PDOException $e) {
+            return false;
+        }
+        return ! empty($result);
+    }
+
+    /**
+     * Проверяет наличие поля в таблице
+     *
+     * @param string $table
+     * @param string $field
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+	public function fieldExists($table, $field, $noPrefix = false)
 	{
-		$exists = false;
-
-		$result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name);
-		while ($cur_index = $this->fetch_assoc($result))
-		{
-			if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name))
-			{
-				$exists = true;
-				break;
-			}
-		}
-
-		return $exists;
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        try {
+            $stmt = $this->db->query('SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:table AND COLUMN_NAME = ?s:field', [':table' => $table, ':field' => $field]);
+            $result = $stmt->fetch();
+            $stmt->closeCursor();
+        } catch (PDOException $e) {
+            return false;
+        }
+        return ! empty($result);
 	}
 
-
-	function create_table($table_name, $schema, $no_prefix = false)
+    /**
+     * Проверяет наличие индекса в таблице
+     *
+     * @param string $table
+     * @param string $index
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function indexExists($table, $index, $noPrefix = false)
+    {
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $index = $index == 'PRIMARY' ? $index : $table . '_' . $index;
+        try {
+            $stmt = $this->db->query('SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s:table AND INDEX_NAME = ?s:index', [':table' => $table, ':index' => $index]);
+            $result = $stmt->fetch();
+            $stmt->closeCursor();
+        } catch (PDOException $e) {
+            return false;
+        }
+        return ! empty($result);
+    }
+
+    /**
+     * Создает таблицу
+     *
+     * @param string $table
+     * @param array $schema
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function createTable($table, array $schema, $noPrefix = false)
+    {
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+        $charSet = $this->getCharSet();
+        $query = "CREATE TABLE IF NOT EXISTS `{$table}` (";
+        foreach ($schema['FIELDS'] as $field => $data) {
+            $this->testStr($field);
+            // имя и тип
+            $query .= "`{$field}` " . $this->replType($data[0]);
+            // не NULL
+            if (empty($data[1])) {
+                $query .= ' NOT NULL';
+            }
+            // значение по умолчанию
+            if (isset($data[2])) {
+                $query .= ' DEFAULT ' . $this->convToStr($data[2]);
+            }
+            // сравнение
+            if (isset($data[3]) && is_string($data[3])) {
+                $this->testStr($data[3]);
+                $query .= " CHARACTER SET {$charSet} COLLATE {$charSet}_{$data[3]}";
+            }
+            $query .= ', ';
+        }
+        if (isset($schema['PRIMARY KEY'])) {
+            $query .= 'PRIMARY KEY (' . $this->replIdxs($schema['PRIMARY KEY']) . '), ';
+        }
+        if (isset($schema['UNIQUE KEYS'])) {
+            foreach ($schema['UNIQUE KEYS'] as $key => $fields) {
+                $this->testStr($key);
+                $query .= "UNIQUE `{$table}_{$key}` (" . $this->replIdxs($fields) . '), ';
+            }
+        }
+        if (isset($schema['INDEXES'])) {
+            foreach ($schema['INDEXES'] as $index => $fields) {
+                $this->testStr($index);
+                $query .= "INDEX `{$table}_{$index}` (" . $this->replIdxs($fields) . '), ';
+            }
+        }
+        if (isset($schema['ENGINE'])) {
+            $engine = $schema['ENGINE'];
+        } else {
+            // при отсутствии типа таблицы он определяется на основании типов других таблиц в базе
+            $stmt = $this->db->query("SHOW TABLE STATUS LIKE '{$this->dbPrefix}%'");
+            $engine = [];
+            while ($row = $stmt->fetch()) {
+                if (isset($engine[$row['Engine']])) {
+                    ++$engine[$row['Engine']];
+                } else {
+                    $engine[$row['Engine']] = 1;
+                }
+            }
+            // в базе нет таблиц
+            if (empty($engine)) {
+                $engine = 'MyISAM';
+            } else {
+                arsort($engine);
+                // берем тип наиболее часто встречаемый у имеющихся таблиц
+                $engine = array_shift(array_keys($engine));
+            }
+        }
+        $this->testStr($engine);
+		$query = rtrim($query, ', ') . ") ENGINE = {$engine} CHARACTER SET {$charSet}";
+        return $this->db->exec($query) !== false;
+    }
+
+    /**
+     * Удаляет таблицу
+     *
+     * @param string $table
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function dropTable($table, $noPrefix = false)
+    {
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+		return $this->db->exec("DROP TABLE IF EXISTS `{$table}`") !== false;
+    }
+
+    /**
+     * Переименовывает таблицу
+     *
+     * @param string $old
+     * @param string $new
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function renameTable($old, $new, $noPrefix = false)
+    {
+        if ($this->tableExists($new, $noPrefix) && ! $this->tableExists($old, $noPrefix)) {
+            return true;
+        }
+        $old = ($noPrefix ? '' : $this->dbPrefix) . $old;
+        $this->testStr($old);
+        $new = ($noPrefix ? '' : $this->dbPrefix) . $new;
+        $this->testStr($new);
+        return $this->db->exec("ALTER TABLE `{$old}` RENAME TO `{$new}`") !== false;
+    }
+
+    /**
+     * Добавляет поле в таблицу
+     *
+     * @param string $table
+     * @param string $field
+     * @param bool $allowNull
+     * @param mixed $default
+     * @param string $after
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function addField($table, $field, $type, $allowNull, $default = null, $after = null, $noPrefix = false)
+    {
+        if ($this->fieldExists($table, $field, $noPrefix)) {
+            return true;
+        }
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+        $this->testStr($field);
+        $query = "ALTER TABLE `{$table}` ADD `{$field}` " . $this->replType($type);
+        if ($allowNull) {
+            $query .= ' NOT NULL';
+        }
+        if (null !== $default) {
+            $query .= ' DEFAULT ' . $this->convToStr($default);
+        }
+        if (null !== $after) {
+            $this->testStr($after);
+            $query .= " AFTER `{$after}`";
+        }
+        return $this->db->exec($query) !== false;
+    }
+
+    /**
+     * Модифицирует поле в таблице
+     *
+     * @param string $table
+     * @param string $field
+     * @param bool $allowNull
+     * @param mixed $default
+     * @param string $after
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+	public function alterField($table, $field, $type, $allowNull, $default = null, $after = null, $noPrefix = false)
 	{
-		if ($this->table_exists($table_name, $no_prefix))
-			return true;
-
-		$query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n";
-
-		// Go through every schema element and add it to the query
-		foreach ($schema['FIELDS'] as $field_name => $field_data)
-		{
-			$field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']);
-
-			$query .= $field_name.' '.$field_data['datatype'];
-
-			if (isset($field_data['collation']))
-				$query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation'];
-
-			if (!$field_data['allow_null'])
-				$query .= ' NOT NULL';
-
-			if (isset($field_data['default']))
-				$query .= ' DEFAULT '.$field_data['default'];
-
-			$query .= ",\n";
-		}
-
-		// If we have a primary key, add it
-		if (isset($schema['PRIMARY KEY']))
-			$query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n";
-
-		// Add unique keys
-		if (isset($schema['UNIQUE KEYS']))
-		{
-			foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields)
-				$query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n";
-		}
-
-		// Add indexes
-		if (isset($schema['INDEXES']))
-		{
-			foreach ($schema['INDEXES'] as $index_name => $index_fields)
-				$query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n";
-		}
-
-		// We remove the last two characters (a newline and a comma) and add on the ending
-		$query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'MyISAM').' CHARACTER SET utf8';
-
-		return $this->query($query) ? true : false;
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+        $this->testStr($field);
+        $query = "ALTER TABLE `{$table}` MODIFY `{$field}` " . $this->replType($type);
+        if ($allowNull) {
+            $query .= ' NOT NULL';
+        }
+        if (null !== $default) {
+            $query .= ' DEFAULT ' . $this->convToStr($default);
+        }
+        if (null !== $after) {
+            $this->testStr($after);
+            $query .= " AFTER `{$after}`";
+        }
+        return $this->db->exec($query) !== false;
 	}
 
-
-	function drop_table($table_name, $no_prefix = false)
-	{
-		if (!$this->table_exists($table_name, $no_prefix))
-			return true;
-
-		return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
-	}
-
-
-	function rename_table($old_table, $new_table, $no_prefix = false)
-	{
-		// If the new table exists and the old one doesn't, then we're happy
-		if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix))
-			return true;
-
-		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false;
-	}
-
-
-	function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false)
-	{
-		if ($this->field_exists($table_name, $field_name, $no_prefix))
-			return true;
-
-		$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
-
-		if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
-			$default_value = '\''.$this->escape($default_value).'\'';
-
-		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
-	}
-
-
-	function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false)
-	{
-		if (!$this->field_exists($table_name, $field_name, $no_prefix))
-			return true;
-
-		$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
-
-		if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
-			$default_value = '\''.$this->escape($default_value).'\'';
-
-		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
-	}
-
-
-	function drop_field($table_name, $field_name, $no_prefix = false)
-	{
-		if (!$this->field_exists($table_name, $field_name, $no_prefix))
-			return true;
-
-		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false;
-	}
-
-
-	function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false)
-	{
-		if ($this->index_exists($table_name, $index_name, $no_prefix))
-			return true;
-
-		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false;
-	}
-
-
-	function drop_index($table_name, $index_name, $no_prefix = false)
-	{
-		if (!$this->index_exists($table_name, $index_name, $no_prefix))
-			return true;
-
-		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false;
-	}
-
-	function truncate_table($table_name, $no_prefix = false)
-	{
-		return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
-	}
+    /**
+     * Удаляет поле из таблицы
+     *
+     * @param string $table
+     * @param string $field
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function dropField($table, $field, $noPrefix = false)
+    {
+        if (! $this->fieldExists($table, $field, $noPrefix)) {
+            return true;
+        }
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+        $this->testStr($field);
+        return $this->db->exec("ALTER TABLE `{$table}` DROP COLUMN `{$field}`") !== false;
+    }
+
+    /**
+     * Добавляет индекс в таблицу
+     *
+     * @param string $table
+     * @param string $index
+     * @param array $fields
+     * @param bool $unique
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function addIndex($table, $index, array $fields, $unique = false, $noPrefix = false)
+    {
+        if ($this->indexExists($table, $index, $noPrefix)) {
+            return true;
+        }
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+        $query = "ALTER TABLE `{$table}` ADD ";
+        if ($index == 'PRIMARY') {
+            $query .= 'PRIMARY KEY';
+        } else {
+            $index = $table . '_' . $index;
+            $this->testStr($index);
+            if ($unique) {
+                $query .= "UNIQUE `{$index}`";
+            } else {
+                $query .= "INDEX `{$index}`";
+            }
+        }
+        $query .= ' (' . $this->replIdxs($fields) . ')';
+        return $this->db->exec($query) !== false;
+    }
+
+    /**
+     * Удаляет индекс из таблицы
+     *
+     * @param string $table
+     * @param string $index
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function dropIndex($table, $index, $noPrefix = false)
+    {
+        if (! $this->indexExists($table, $index, $noPrefix)) {
+            return true;
+        }
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+        $query = "ALTER TABLE `{$table}` ";
+        if ($index == 'PRIMARY') {
+            $query .= "DROP PRIMARY KEY";
+        } else {
+            $index = $table . '_' . $index;
+            $this->testStr($index);
+            $query .= "DROP INDEX `{$index}`";
+        }
+        return $this->db->exec($query) !== false;
+    }
+
+    /**
+     * Очищает таблицу
+     *
+     * @param string $table
+     * @param bool $noPrefix
+     *
+     * @return bool
+     */
+    public function truncateTable($table, $noPrefix = false)
+    {
+        $table = ($noPrefix ? '' : $this->dbPrefix) . $table;
+        $this->testStr($table);
+        return $this->db->exec("TRUNCATE TABLE `{$table}`") !== false;
+    }
+
+    /**
+     * Статистика
+     *
+     * @return array|string
+     */
+    public function statistics()
+    {
+        $this->testStr($this->dbPrefix);
+        $stmt = $this->db->query("SHOW TABLE STATUS LIKE '{$this->dbPrefix}%'");
+        $records = $size = 0;
+        $engine = [];
+        while ($row = $stmt->fetch()) {
+            $records += $row['Rows'];
+            $size += $row['Data_length'] + $row['Index_length'];
+            if (isset($engine[$row['Engine']])) {
+                ++$engine[$row['Engine']];
+            } else {
+                $engine[$row['Engine']] = 1;
+            }
+        }
+        arsort($engine);
+        $tmp = [];
+        foreach ($engine as $key => $val) {
+            $tmp[] = "{$key}({$val})";
+        }
+
+        $other = [];
+        $stmt = $this->db->query("SHOW VARIABLES LIKE 'character\_set\_%'");
+        while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
+            $other[$row[0]] = $row[1];
+        }
+
+        return [
+            'db'      => 'MySQL (PDO) ' . $this->db->getAttribute(\PDO::ATTR_SERVER_VERSION) . ' : ' . implode(', ', $tmp),
+            'records' => $records,
+            'size'    => $size,
+            'server info' => $this->db->getAttribute(\PDO::ATTR_SERVER_INFO),
+        ] + $other;
+    }
 }

+ 374 - 0
app/Core/DBold/mysql.php

@@ -0,0 +1,374 @@
+<?php
+
+/**
+ * Copyright (C) 2008-2012 FluxBB
+ * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB
+ * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
+ */
+
+namespace ForkBB\Core\DB;
+
+// Make sure we have built in support for MySQL
+if (!function_exists('mysql_connect'))
+	exit('This PHP environment doesn\'t have MySQL support built in. MySQL support is required if you want to use a MySQL database to run this forum. Consult the PHP documentation for further assistance.');
+
+
+class DBLayer
+{
+	var $prefix;
+	var $link_id;
+	var $query_result;
+
+	var $saved_queries = array();
+	var $num_queries = 0;
+
+	var $error_no = false;
+	var $error_msg = 'Unknown';
+
+	var $datatype_transformations = array(
+		'%^SERIAL$%'	=>	'INT(10) UNSIGNED AUTO_INCREMENT'
+	);
+
+
+	function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix, $p_connect)
+	{
+		$this->prefix = $db_prefix;
+
+		if ($p_connect)
+			$this->link_id = @mysql_pconnect($db_host, $db_username, $db_password);
+		else
+			$this->link_id = @mysql_connect($db_host, $db_username, $db_password);
+
+		if ($this->link_id)
+		{
+			if (!@mysql_select_db($db_name, $this->link_id))
+				error('Unable to select database. MySQL reported: '.mysql_error(), __FILE__, __LINE__);
+		}
+		else
+			error('Unable to connect to MySQL server. MySQL reported: '.mysql_error(), __FILE__, __LINE__);
+
+		// Setup the client-server character set (UTF-8)
+		if (!defined('FORUM_NO_SET_NAMES'))
+			$this->set_names('utf8');
+
+		return $this->link_id;
+	}
+	
+
+	function start_transaction()
+	{
+		return;
+	}
+
+
+	function end_transaction()
+	{
+		return;
+	}
+
+
+	function query($sql, $unbuffered = false)
+	{
+		if (defined('PUN_SHOW_QUERIES'))
+			$q_start = microtime(true);
+
+		if ($unbuffered)
+			$this->query_result = @mysql_unbuffered_query($sql, $this->link_id);
+		else
+			$this->query_result = @mysql_query($sql, $this->link_id);
+
+		if ($this->query_result)
+		{
+			if (defined('PUN_SHOW_QUERIES'))
+				$this->saved_queries[] = array($sql, sprintf('%.5f', microtime(true) - $q_start));
+
+			++$this->num_queries;
+
+			return $this->query_result;
+		}
+		else
+		{
+			if (defined('PUN_SHOW_QUERIES'))
+				$this->saved_queries[] = array($sql, 0);
+
+			$this->error_no = @mysql_errno($this->link_id);
+			$this->error_msg = @mysql_error($this->link_id);
+
+			return false;
+		}
+	}
+
+
+	function result($query_id = 0, $row = 0, $col = 0)
+	{
+		return ($query_id) ? @mysql_result($query_id, $row, $col) : false;
+	}
+
+
+	function fetch_assoc($query_id = 0)
+	{
+		return ($query_id) ? @mysql_fetch_assoc($query_id) : false;
+	}
+
+
+	function fetch_row($query_id = 0)
+	{
+		return ($query_id) ? @mysql_fetch_row($query_id) : false;
+	}
+
+
+	function num_rows($query_id = 0)
+	{
+		return ($query_id) ? @mysql_num_rows($query_id) : false;
+	}
+
+
+	function affected_rows()
+	{
+		return ($this->link_id) ? @mysql_affected_rows($this->link_id) : false;
+	}
+
+
+	function insert_id()
+	{
+		return ($this->link_id) ? @mysql_insert_id($this->link_id) : false;
+	}
+
+
+	function get_num_queries()
+	{
+		return $this->num_queries;
+	}
+
+
+	function get_saved_queries()
+	{
+		return $this->saved_queries;
+	}
+
+
+	function free_result($query_id = false)
+	{
+		return ($query_id) ? @mysql_free_result($query_id) : false;
+	}
+
+
+	function escape($str)
+	{
+		if (is_array($str))
+			return '';
+		else if (function_exists('mysql_real_escape_string'))
+			return mysql_real_escape_string($str, $this->link_id);
+		else
+			return mysql_escape_string($str);
+	}
+
+
+	function error()
+	{
+		$result['error_sql'] = @current(@end($this->saved_queries));
+		$result['error_no'] = $this->error_no;
+		$result['error_msg'] = $this->error_msg;
+
+		return $result;
+	}
+
+
+	function close()
+	{
+		if ($this->link_id)
+		{
+			if (is_resource($this->query_result))
+				@mysql_free_result($this->query_result);
+
+			return @mysql_close($this->link_id);
+		}
+		else
+			return false;
+	}
+
+	function get_names()
+	{
+		$result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\'');
+		return $this->result($result, 0, 1);
+	}
+
+
+	function set_names($names)
+	{
+		return $this->query('SET NAMES \''.$this->escape($names).'\'');
+	}
+
+
+	function get_version()
+	{
+		$result = $this->query('SELECT VERSION()');
+
+		return array(
+			'name'		=> 'MySQL Standard',
+			'version'	=> preg_replace('%^([^-]+).*$%', '\\1', $this->result($result))
+		);
+	}
+
+
+	function table_exists($table_name, $no_prefix = false)
+	{
+		$result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\'');
+		return $this->num_rows($result) > 0;
+	}
+
+
+	function field_exists($table_name, $field_name, $no_prefix = false)
+	{
+		$result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\'');
+		return $this->num_rows($result) > 0;
+	}
+
+
+	function index_exists($table_name, $index_name, $no_prefix = false)
+	{
+		$exists = false;
+
+		$result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name);
+		while ($cur_index = $this->fetch_assoc($result))
+		{
+			if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name))
+			{
+				$exists = true;
+				break;
+			}
+		}
+
+		return $exists;
+	}
+
+
+	function create_table($table_name, $schema, $no_prefix = false)
+	{
+		if ($this->table_exists($table_name, $no_prefix))
+			return true;
+
+		$query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n";
+
+		// Go through every schema element and add it to the query
+		foreach ($schema['FIELDS'] as $field_name => $field_data)
+		{
+			$field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']);
+
+			$query .= $field_name.' '.$field_data['datatype'];
+
+			if (isset($field_data['collation']))
+				$query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation'];
+
+			if (!$field_data['allow_null'])
+				$query .= ' NOT NULL';
+
+			if (isset($field_data['default']))
+				$query .= ' DEFAULT '.$field_data['default'];
+
+			$query .= ",\n";
+		}
+
+		// If we have a primary key, add it
+		if (isset($schema['PRIMARY KEY']))
+			$query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n";
+
+		// Add unique keys
+		if (isset($schema['UNIQUE KEYS']))
+		{
+			foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields)
+				$query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n";
+		}
+
+		// Add indexes
+		if (isset($schema['INDEXES']))
+		{
+			foreach ($schema['INDEXES'] as $index_name => $index_fields)
+				$query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n";
+		}
+
+		// We remove the last two characters (a newline and a comma) and add on the ending
+		$query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'MyISAM').' CHARACTER SET utf8';
+
+		return $this->query($query) ? true : false;
+	}
+
+
+	function drop_table($table_name, $no_prefix = false)
+	{
+		if (!$this->table_exists($table_name, $no_prefix))
+			return true;
+
+		return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
+	}
+
+
+	function rename_table($old_table, $new_table, $no_prefix = false)
+	{
+		// If the new table exists and the old one doesn't, then we're happy
+		if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix))
+			return true;
+
+		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false;
+	}
+
+
+	function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false)
+	{
+		if ($this->field_exists($table_name, $field_name, $no_prefix))
+			return true;
+
+		$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
+
+		if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
+			$default_value = '\''.$this->escape($default_value).'\'';
+
+		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
+	}
+
+
+	function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false)
+	{
+		if (!$this->field_exists($table_name, $field_name, $no_prefix))
+			return true;
+
+		$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
+
+		if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
+			$default_value = '\''.$this->escape($default_value).'\'';
+
+		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
+	}
+
+
+	function drop_field($table_name, $field_name, $no_prefix = false)
+	{
+		if (!$this->field_exists($table_name, $field_name, $no_prefix))
+			return true;
+
+		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false;
+	}
+
+
+	function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false)
+	{
+		if ($this->index_exists($table_name, $index_name, $no_prefix))
+			return true;
+
+		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false;
+	}
+
+
+	function drop_index($table_name, $index_name, $no_prefix = false)
+	{
+		if (!$this->index_exists($table_name, $index_name, $no_prefix))
+			return true;
+
+		return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false;
+	}
+
+	function truncate_table($table_name, $no_prefix = false)
+	{
+		return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
+	}
+}

+ 0 - 0
app/Core/DB/mysql_innodb.php → app/Core/DBold/mysql_innodb.php


+ 0 - 0
app/Core/DB/mysqli.php → app/Core/DBold/mysqli.php


+ 0 - 0
app/Core/DB/mysqli_innodb.php → app/Core/DBold/mysqli_innodb.php


+ 0 - 0
app/Core/DB/pgsql.php → app/Core/DBold/pgsql.php


+ 0 - 0
app/Core/DB/sqlite.php → app/Core/DBold/sqlite.php


+ 1 - 1
app/Core/Install.php

@@ -476,7 +476,7 @@ foreach ($styles as $temp)
         else
         {
             // Validate prefix
-            if (strlen($db_prefix) > 0 && (! preg_match('%^[a-zA-Z]\w*$%', $db_prefix) || strlen($db_prefix) > 40))
+            if (strlen($db_prefix) > 0 && (! preg_match('%^[a-z][a-z0-9_]*$%i', $db_prefix) || strlen($db_prefix) > 40))
                 error(sprintf($lang_install['Table prefix error'], $db->prefix));
 
             $this->c->DB_TYPE = $db_type;

+ 1 - 1
app/Core/Mail.php

@@ -90,7 +90,7 @@ class Mail
     {
         if (! is_string($email)
             || mb_strlen($email, 'UTF-8') > 80
-            || ! preg_match('%^([\w!#$\%&\'*+-/=?^`{|}~]+(?:\.[\w!#$\%&\'*+-/=?^`{|}~]+)*)@([^\x00-\x20]+)$%D', $email, $matches)
+            || ! preg_match('%^([a-z0-9_!#$\%&\'*+-/=?^`{|}~]+(?:\.[a-z0-9_!#$\%&\'*+-/=?^`{|}~]+)*)@([^\x00-\x20]+)$%Di', $email, $matches)
         ) {
             return false;
         }

+ 24 - 63
app/Models/Actions/CacheGenerator.php

@@ -2,23 +2,24 @@
 
 namespace ForkBB\Models\Actions;
 
-//use ForkBB\Core\DB;
+use ForkBB\Core\Container;
 use ForkBB\Models\User;
 
 class CacheGenerator
 {
     /**
-     * @var ForkBB\Core\DB
+     * Контейнер
+     * @var Container
      */
-    protected $db;
+    protected $c;
 
     /**
      * Конструктор
-     * @param ForkBB\Core\DB $db
+     * @param Container $container
      */
-    public function __construct($db)
+    public function __construct(Container $container)
     {
-        $this->db = $db;
+        $this->c = $container;
     }
 
     /**
@@ -27,14 +28,7 @@ class CacheGenerator
      */
     public function config()
     {
-        // Get the forum config from the DB
-        $result = $this->db->query('SELECT * FROM '.$this->db->prefix.'config', true) or error('Unable to fetch forum config', __FILE__, __LINE__, $this->db->error());
-        $arr = [];
-        while ($cur = $this->db->fetch_row($result)) {
-            $arr[$cur[0]] = $cur[1];
-        }
-        $this->db->free_result($result);
-        return $arr;
+        return $this->c->DB->query('SELECT conf_name, conf_value FROM ::config')->fetchAll(\PDO::FETCH_KEY_PAIR);
     }
 
     /**
@@ -43,14 +37,7 @@ class CacheGenerator
      */
     public function bans()
     {
-        // Get the ban list from the DB
-        $result = $this->db->query('SELECT * FROM '.$this->db->prefix.'bans', true) or error('Unable to fetch ban list', __FILE__, __LINE__, $this->db->error());
-        $arr = [];
-        while ($cur = $this->db->fetch_assoc($result)) {
-            $arr[] = $cur;
-        }
-        $this->db->free_result($result);
-        return $arr;
+        return $this->c->DB->query('SELECT id, username, ip, email, message, expire FROM ::bans')->fetchAll();
     }
 
     /**
@@ -59,17 +46,13 @@ class CacheGenerator
      */
     public function censoring()
     {
-        $result = $this->db->query('SELECT search_for, replace_with FROM '.$this->db->prefix.'censoring') or error('Unable to fetch censoring list', __FILE__, __LINE__, $this->db->error());
-        $num_words = $this->db->num_rows($result);
-
-        $search_for = $replace_with = [];
-        for ($i = 0; $i < $num_words; $i++) {
-            list($search_for[$i], $replace_with[$i]) = $this->db->fetch_row($result);
-            $search_for[$i] = '%(?<![\p{L}\p{N}])('.str_replace('\*', '[\p{L}\p{N}]*?', preg_quote($search_for[$i], '%')).')(?![\p{L}\p{N}])%iu';
+        $stmt = $this->c->DB->query('SELECT search_for, replace_with FROM ::censoring');
+        $search = $replace = [];
+        while ($row = $stmt->fetch()) {
+            $replace[] = $row['replace_with'];
+            $search[] = '%(?<![\p{L}\p{N}])('.str_replace('\*', '[\p{L}\p{N}]*?', preg_quote($row['search_for'], '%')).')(?![\p{L}\p{N}])%iu';
         }
-        $this->db->free_result($result);
-
-        return [$search_for, $replace_with];
+        return [$search, $replace];
     }
 
     /**
@@ -80,13 +63,8 @@ class CacheGenerator
     public function usersInfo()
     {
         $stats = [];
-
-        $result = $this->db->query('SELECT COUNT(id)-1 FROM '.$this->db->prefix.'users WHERE group_id!='.PUN_UNVERIFIED) or error('Unable to fetch total user count', __FILE__, __LINE__, $this->db->error());
-        $stats['total_users'] = $this->db->result($result);
-
-        $result = $this->db->query('SELECT id, username FROM '.$this->db->prefix.'users WHERE group_id!='.PUN_UNVERIFIED.' ORDER BY registered DESC LIMIT 1') or error('Unable to fetch newest registered user', __FILE__, __LINE__, $this->db->error());
-        $stats['last_user'] = $this->db->fetch_assoc($result);
-
+        $stats['total_users'] = $this->c->DB->query('SELECT COUNT(id)-1 FROM ::users WHERE group_id!='.PUN_UNVERIFIED)->fetchColumn();
+        $stats['last_user'] = $this->c->DB->query('SELECT id, username FROM ::users WHERE group_id!='.PUN_UNVERIFIED.' ORDER BY registered DESC LIMIT 1')->fetch();
         return $stats;
     }
 
@@ -96,15 +74,7 @@ class CacheGenerator
      */
     public function admins()
     {
-        // Get admins from the DB
-        $result = $this->db->query('SELECT id FROM '.$this->db->prefix.'users WHERE group_id='.PUN_ADMIN) or error('Unable to fetch users info', __FILE__, __LINE__, $this->db->error());
-        $arr = [];
-        while ($row = $this->db->fetch_row($result)) {
-            $arr[] = $row[0];
-        }
-        $this->db->free_result($result);
-
-        return $arr;
+        return $this->c->DB->query('SELECT id FROM ::users WHERE group_id='.PUN_ADMIN)->fetchAll(\PDO::FETCH_COLUMN);
     }
 
     /**
@@ -113,14 +83,7 @@ class CacheGenerator
      */
     public function smilies()
     {
-        $arr = [];
-        $result = $this->db->query('SELECT text, image FROM '.$this->db->prefix.'smilies ORDER BY disp_position') or error('Unable to retrieve smilies', __FILE__, __LINE__, $this->db->error());
-        while ($cur = $this->db->fetch_assoc($result)) {
-            $arr[$cur['text']] = $cur['image'];
-        }
-        $this->db->free_result($result);
-
-        return $arr;
+        return $this->c->DB->query('SELECT text, image FROM ::smilies ORDER BY disp_position')->fetchAll(\PDO::FETCH_KEY_PAIR); //???? text уникальное?
     }
 
     /**
@@ -129,22 +92,20 @@ class CacheGenerator
      */
     public function forums(User $user)
     {
-        $groupId = $user->gId;
-		$result = $this->db->query('SELECT g_read_board FROM '.$this->db->prefix.'groups WHERE g_id='.$groupId) or error('Unable to fetch user group read permission', __FILE__, __LINE__, $this->db->error());
-		$read = $this->db->result($result);
+        $stmt = $this->c->DB->query('SELECT g_read_board FROM ::groups WHERE g_id=?i:id', [':id' => $user->gId]);
+        $read = $stmt->fetchColumn();
+        $stmt->closeCursor();
 
         $tree = $desc = $asc = [];
 
         if ($read) {
-            $result = $this->db->query('SELECT c.id AS cid, c.cat_name, f.id AS fid, f.forum_name, f.redirect_url, f.parent_forum_id, f.disp_position FROM '.$this->db->prefix.'categories AS c INNER JOIN '.$this->db->prefix.'forums AS f ON c.id=f.cat_id LEFT JOIN '.$this->db->prefix.'forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id='.$groupId.') WHERE fp.read_forum IS NULL OR fp.read_forum=1 ORDER BY c.disp_position, c.id, f.disp_position', true) or error('Unable to fetch category/forum list', __FILE__, __LINE__, $this->db->error());
-
-            while ($f = $this->db->fetch_assoc($result)) {
+            $stmt = $this->c->DB->query('SELECT c.id AS cid, c.cat_name, f.id AS fid, f.forum_name, f.redirect_url, f.parent_forum_id, f.disp_position FROM ::categories AS c INNER JOIN ::forums AS f ON c.id=f.cat_id LEFT JOIN ::forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id=?i:id) WHERE fp.read_forum IS NULL OR fp.read_forum=1 ORDER BY c.disp_position, c.id, f.disp_position', [':id' => $user->gId]);
+            while ($f = $stmt->fetch()) {
                 $tree[$f['parent_forum_id']][$f['fid']] = $f;
             }
             $this->forumsDesc($desc, $tree);
             $this->forumsAsc($asc, $tree);
         }
-
         return [$tree, $desc, $asc];
     }
 

+ 3 - 7
app/Models/Actions/CheckBans.php

@@ -34,7 +34,6 @@ class CheckBans
     {
         $user = $this->c->user;
 
-        // Для админов и при отсутствии банов прекращаем проверку
         if ($user->isAdmin) {
             return null;
         } elseif ($user->isGuest) {
@@ -46,9 +45,9 @@ class CheckBans
         if ($banned) {
             $this->c->Online->delete($user); //???? а зачем это надо?
             return $this->ban;
+        } else {
+            return null;
         }
-
-        return null;
     }
 
     /**
@@ -102,12 +101,9 @@ class CheckBans
                 }
             }
         }
-
-        // If we removed any expired bans during our run-through, we need to regenerate the bans cache
         if (! empty($remove))
         {
-            $db = $this->c->DB;
-            $db->query('DELETE FROM '.$db->prefix.'bans WHERE id IN (' . implode(',', $remove) . ')') or error('Unable to delete expired ban', __FILE__, __LINE__, $db->error());
+            $this->c->DB->exec('DELETE FROM ::bans WHERE id IN (?ai:remove)', [':remove' => $remove]);
             $this->c->{'bans update'};
         }
         return $banned;

+ 2 - 2
app/Models/Actions/LoadUserFromCookie.php

@@ -62,7 +62,7 @@ class LoadUserFromCookie
             // быстрое переключение языка - Visman
             $language = $this->cookie->get('glang');
             if (null !== $language) {
-                $language = preg_replace('%[^\w]%', '', $language);
+                $language = preg_replace('%[^a-zA-Z0-9_]%', '', $language);
                 $languages = forum_list_langs();
                 if (in_array($language, $languages)) {
                     $user->language = $language;
@@ -138,7 +138,7 @@ class LoadUserFromCookie
             $agent = preg_replace('%(?:https?://|www\.)[^\)]*(\)[^/]+$)?%i', ' ', $agent);
         }
         if (strpos($agent, '@') !== false) {
-            $agent = preg_replace('%\b[\w\.-]+@[^\)]+%', ' ', $agent);
+            $agent = preg_replace('%\b[a-z0-9_\.-]+@[^\)]+%i', ' ', $agent);
         }
 
         $agentL = strtolower($agent);

+ 33 - 78
app/Models/Online.php

@@ -5,7 +5,6 @@ namespace ForkBB\Models;
 use ForkBB\Core\Container;
 use ForkBB\Models\User;
 use ForkBB\Models\Pages\Page;
-use RuntimeException;
 
 class Online
 {
@@ -26,29 +25,15 @@ class Online
      */
     protected $config;
 
-    /**
-     * @var DB
-     */
-    protected $db;
-
-    /**
-     * @var User
-     */
-    protected $user;
-
     /**
      * Конструктор
      * @param array $config
-     * @param DB $db
-     * @param User $user
      * @param Container $container
      */
-    public function __construct(array $config, $db, User $user, Container $container)
+    public function __construct(Container $container)
     {
-        $this->config = $config;
-        $this->db = $db;
-        $this->user = $user;
         $this->c = $container;
+        $this->config = $container->config;
     }
 
     /**
@@ -83,13 +68,13 @@ class Online
         $setIdle = false;
 
         if ($this->config['o_users_online'] == '1' && $type) {
-            $result = $this->db->query('SELECT user_id, ident, logged, idle, o_position, o_name FROM '.$this->db->prefix.'online ORDER BY logged') or error('Unable to fetch users from online list', __FILE__, __LINE__, $this->db->error());
+            $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle, o_position, o_name FROM ::online ORDER BY logged');
         } elseif ($type) {
-            $result = $this->db->query('SELECT user_id, ident, logged, idle FROM '.$this->db->prefix.'online ORDER BY logged') or error('Unable to fetch users from online list', __FILE__, __LINE__, $this->db->error());
+            $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle FROM ::online ORDER BY logged');
         } else {
-            $result = $this->db->query('SELECT user_id, ident, logged, idle FROM '.$this->db->prefix.'online WHERE logged<'.$tOnline) or error('Unable to fetch users from online list', __FILE__, __LINE__, $this->db->error());
+            $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle FROM ::online WHERE logged<?i:online', [':online' => $tOnline]);
         }
-        while ($cur = $this->db->fetch_assoc($result)) {
+        while ($cur = $stmt->fetch()) {
 
             // посетитель уже не онлайн (или почти не онлайн)
             if ($cur['logged'] < $tOnline) {
@@ -97,7 +82,7 @@ class Online
                 if ($cur['user_id'] > 1) {
                     if ($cur['logged'] < $tVisit) {
                         $deleteU = true;
-                        $this->db->query('UPDATE '.$this->db->prefix.'users SET last_visit='.$cur['logged'].' WHERE id='.$cur['user_id']) or error('Unable to update user visit data', __FILE__, __LINE__, $this->db->error());
+                        $this->c->DB->exec('UPDATE ::users SET last_visit=?i:last WHERE id=?i:id', [':last' => $cur['logged'], ':id' => $cur['user_id']]);
                     } elseif ($cur['idle'] == '0') {
                         $setIdle = true;
                     }
@@ -140,36 +125,28 @@ class Online
                 ++$all;
             }
         }
-        $this->db->free_result($result);
 
         // удаление просроченных пользователей
         if ($deleteU) {
-            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE logged<'.$tVisit) or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+            $this->c->DB->exec('DELETE FROM ::online WHERE logged<?i:visit', [':visit' => $tVisit]);
         }
 
         // удаление просроченных гостей
         if ($deleteG) {
-            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE user_id=1 AND logged<'.$tOnline) or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+            $this->c->DB->exec('DELETE FROM ::online WHERE user_id=1 AND logged<?i:online', [':online' => $tOnline]);
         }
 
         // обновление idle
         if ($setIdle) {
-            $this->db->query('UPDATE '.$this->db->prefix.'online SET idle=1 WHERE logged<'.$tOnline) or error('Unable to update into online list', __FILE__, __LINE__, $this->db->error());
+            $this->c->DB->exec('UPDATE ::online SET idle=1 WHERE logged<?i:online', [':online' => $tOnline]);
         }
 
         // обновление максимального значение пользоватеелй онлайн
         if ($this->config['st_max_users'] < $all) {
-            $this->db->query('UPDATE '.$this->db->prefix.'config SET conf_value=\''.$all.'\' WHERE conf_name=\'st_max_users\'') or error('Unable to update config value \'st_max_users\'', __FILE__, __LINE__, $this->db->error());
-            $this->db->query('UPDATE '.$this->db->prefix.'config SET conf_value=\''.$now.'\' WHERE conf_name=\'st_max_users_time\'') or error('Unable to update config value \'st_max_users_time\'', __FILE__, __LINE__, $this->db->error());
-
+            $this->c->DB->exec('UPDATE ::config SET conf_value=?s:value WHERE conf_name=?s:name', [':value' => $all, ':name' => 'st_max_users']);
+            $this->c->DB->exec('UPDATE ::config SET conf_value=?s:value WHERE conf_name=?s:name', [':value' => $now, ':name' => 'st_max_users_time']);
             $this->c->{'config update'};
         }
-/*
-@set_time_limit(0);
-for ($i=0;$i<100;++$i) {
-    $this->db->query('REPLACE INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) VALUES(1, \''.$this->db->escape($i).'\', '.time().', \''.$this->db->escape($position).'\', \'Super Puper '.$this->db->escape($i).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
-}
-*/
         return [$users, $guests, $bots];
     }
 
@@ -181,52 +158,30 @@ for ($i=0;$i<100;++$i) {
     {
         $now = time();
         // гость
-        if ($this->user->isGuest) {
-            $oname = (string) $this->user->isBot;
-
-            if ($this->user->isLogged) {
-                $this->db->query('UPDATE '.$this->db->prefix.'online SET logged='.$now.', o_position=\''.$this->db->escape($position).'\', o_name=\''.$this->db->escape($oname).'\' WHERE user_id=1 AND ident=\''.$this->db->escape($this->user->ip).'\'') or error('Unable to update online list', __FILE__, __LINE__, $this->db->error());
+        if ($this->c->user->isGuest) {
+            $vars = [
+                ':logged' => time(),
+                ':pos' => $position,
+                ':name' => (string) $this->c->user->isBot,
+                ':ip' => $this->c->user->ip
+            ];
+            if ($this->c->user->isLogged) {
+                $this->c->DB->exec('UPDATE ::online SET logged=?i:logged, o_position=?s:pos, o_name=?s:name WHERE user_id=1 AND ident=?s:ip', $vars);
             } else {
-                $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) SELECT 1, \''.$this->db->escape($this->user->ip).'\', '.$now.', \''.$this->db->escape($position).'\', \''.$this->db->escape($oname).'\' FROM '.$this->db->prefix.'groups WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id=1 AND ident=\''.$this->db->escape($this->user->ip).'\') LIMIT 1') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
-
-                // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table
-/*                switch ($this->c->DB_TYPE) {
-                    case 'mysql':
-                    case 'mysqli':
-                    case 'mysql_innodb':
-                    case 'mysqli_innodb':
-                    case 'sqlite':
-                        $this->db->query('REPLACE INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) VALUES(1, \''.$this->db->escape($this->user->ip).'\', '.$now.', \''.$this->db->escape($position).'\', \''.$this->db->escape($oname).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
-                        break;
-
-                    default:
-                        $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) SELECT 1, \''.$this->db->escape($this->user->ip).'\', '.$now.', \''.$this->db->escape($position).'\', \''.$this->db->escape($oname).'\' WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id=1 AND ident=\''.$this->db->escape($this->user->ip).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
-                        break;
-                }
-*/
+                $this->c->DB->exec('INSERT INTO ::online (user_id, ident, logged, o_position, o_name) SELECT 1, ?s:ip, ?i:logged, ?s:pos, ?s:name FROM ::groups WHERE NOT EXISTS (SELECT 1 FROM ::online WHERE user_id=1 AND ident=?s:ip) LIMIT 1', $vars);
             }
         } else {
         // пользователь
-            if ($this->user->isLogged) {
-                $idle_sql = ($this->user->idle == '1') ? ', idle=0' : '';
-                $this->db->query('UPDATE '.$this->db->prefix.'online SET logged='.$now.$idle_sql.', o_position=\''.$this->db->escape($position).'\' WHERE user_id='.$this->user->id) or error('Unable to update online list', __FILE__, __LINE__, $this->db->error());
+            $vars = [
+                ':logged' => time(),
+                ':pos' => $position,
+                ':id' => $this->c->user->id,
+                ':name' => $this->c->user->username,
+            ];
+            if ($this->c->user->isLogged) {
+                $this->c->DB->exec('UPDATE ::online SET logged=?i:logged, idle=0, o_position=?s:pos WHERE user_id=?i:id', $vars);
             } else {
-                $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position) SELECT '.$this->user->id.', \''.$this->db->escape($this->user->username).'\', '.$now.', \''.$this->db->escape($position).'\' FROM '.$this->db->prefix.'groups WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id='.$this->user->id.') LIMIT 1') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
-                // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table
-/*                switch ($this->c->DB_TYPE) {
-                    case 'mysql':
-                    case 'mysqli':
-                    case 'mysql_innodb':
-                    case 'mysqli_innodb':
-                    case 'sqlite':
-                        $this->db->query('REPLACE INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position) VALUES('.$this->user->id.', \''.$this->db->escape($this->user->username).'\', '.$now.', \''.$this->db->escape($position).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
-                        break;
-
-                    default:
-                        $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position) SELECT '.$this->user->id.', \''.$this->db->escape($this->user->username).'\', '.$now.', \''.$this->db->escape($position).'\' WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id='.$this->user->id.')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
-                        break;
-                }
-*/
+                $this->c->DB->exec('INSERT INTO ::online (user_id, ident, logged, o_position) SELECT ?i:id, ?s:name, ?i:logged, ?s:pos FROM ::groups WHERE NOT EXISTS (SELECT 1 FROM ::online WHERE user_id=?i:id) LIMIT 1', $vars);
             }
         }
     }
@@ -237,9 +192,9 @@ for ($i=0;$i<100;++$i) {
     public function delete(User $user)
     {
         if ($user->isGuest) {
-            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE user_id=1 AND ident=\''.$this->db->escape($user->ip).'\'') or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+            $this->c->DB->exec('DELETE FROM ::online WHERE user_id=1 AND ident=?s:ip', [':ip' => $user->ip]);
         } else {
-            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE user_id='.$user->id) or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+            $this->c->DB->exec('DELETE FROM ::online WHERE user_id=?i:id', [':id' => $user->id]);
         }
     }
 }

+ 7 - 22
app/Models/Pages/Admin/Statistics.php

@@ -66,27 +66,14 @@ class Statistics extends Admin
         }
 
         // Get number of current visitors
-        $db = $this->c->DB;
-        $result = $db->query('SELECT COUNT(user_id) FROM '.$db->prefix.'online WHERE idle=0') or error('Unable to fetch online count', __FILE__, __LINE__, $db->error());
-        $this->data['numOnline'] = $db->result($result);
+        $this->data['numOnline'] = $this->c->DB->query('SELECT COUNT(user_id) FROM ::online WHERE idle=0')->fetchColumn();
 
-        // Collect some additional info about MySQL
-        if (in_array($this->c->DB_TYPE, ['mysql', 'mysqli', 'mysql_innodb', 'mysqli_innodb'])) {
-            // Calculate total db size/row count
-            $result = $db->query('SHOW TABLE STATUS LIKE \''.$db->prefix.'%\'') or error('Unable to fetch table status', __FILE__, __LINE__, $db->error());
-
-            $tRecords = $tSize = 0;
-            while ($status = $db->fetch_assoc($result)) {
-                $tRecords += $status['Rows'];
-                $tSize += $status['Data_length'] + $status['Index_length'];
-            }
-
-            $this->data['tSize'] = $this->size($tSize);
-            $this->data['tRecords'] = $this->number($tRecords);
-        } else {
-            $this->data['tSize'] = 0;
-            $this->data['tRecords'] = 0;
-        }
+        $stat = $this->c->DB->statistics();
+        $this->data['dbVersion'] = $stat['db'];
+        $this->data['tSize'] = $this->size($stat['size']);
+        $this->data['tRecords'] = $this->number($stat['records']);
+        unset($stat['db'], $stat['size'], $stat['records']);
+        $this->data['tOther'] = $stat;
 
         // Check for the existence of various PHP opcode caches/optimizers
         if (function_exists('mmcache')) {
@@ -105,8 +92,6 @@ class Statistics extends Admin
             $this->data['accelerator'] = __('NA');
         }
 
-        $this->data['dbVersion'] = implode(' ', $db->get_version());
-
         return $this;
     }
 }

+ 2 - 2
app/Models/Pages/Debug.php

@@ -24,12 +24,12 @@ class Debug extends Page
     {
         $this->data = [
             'time' => $this->number(microtime(true) - (empty($_SERVER['REQUEST_TIME_FLOAT']) ? $this->c->START : $_SERVER['REQUEST_TIME_FLOAT']), 3),
-            'numQueries' => $this->c->DB->get_num_queries(),
+            'numQueries' => 0, //$this->c->DB->get_num_queries(),
             'memory' => $this->size(memory_get_usage()),
             'peak' => $this->size(memory_get_peak_usage()),
         ];
 
-        if (defined('PUN_SHOW_QUERIES')) {
+        if (defined('PUN_SHOW_QUERIES') && 0) {
             $this->data['queries'] = $this->c->DB->get_saved_queries();
         } else {
             $this->data['queries'] = null;

+ 16 - 13
app/Models/Pages/Index.php

@@ -39,14 +39,13 @@ class Index extends Page
         $this->c->Lang->load('index');
         $this->c->Lang->load('subforums');
 
-        $db = $this->c->DB;
         $user = $this->c->user;
         $r = $this->c->Router;
 
         $stats = $this->c->users_info;
 
-        $result = $db->query('SELECT SUM(num_topics), SUM(num_posts) FROM '.$db->prefix.'forums') or error('Unable to fetch topic/post count', __FILE__, __LINE__, $db->error());
-        list($stats['total_topics'], $stats['total_posts']) = array_map([$this, 'number'], array_map('intval', $db->fetch_row($result)));
+        $stmt = $this->c->DB->query('SELECT SUM(num_topics), SUM(num_posts) FROM ::forums');
+        list($stats['total_topics'], $stats['total_posts']) = array_map([$this, 'number'], array_map('intval', $stmt->fetch(\PDO::FETCH_NUM)));
 
         $stats['total_users'] = $this->number($stats['total_users']);
 
@@ -126,22 +125,22 @@ class Index extends Page
             return [];
         }
 
-        $db = $this->c->DB;
         $user = $this->c->user;
 
         // текущие данные по подразделам
-        $forums = array_slice($fAsc[$root], 1);
+        $vars = [
+            ':id' => $user->id,
+            ':forums' => array_slice($fAsc[$root], 1),
+        ];
         if ($user->isGuest) {
-            $result = $db->query('SELECT id, forum_desc, moderators, num_topics, num_posts, last_post, last_post_id, last_poster, last_topic FROM '.$db->prefix.'forums WHERE id IN ('.implode(',', $forums).')', true) or error('Unable to fetch forum list', __FILE__, __LINE__, $db->error());
+            $stmt = $this->c->DB->query('SELECT id, forum_desc, moderators, num_topics, num_posts, last_post, last_post_id, last_poster, last_topic FROM ::forums WHERE id IN (?ai:forums)', $vars);
         } else {
-            $result = $db->query('SELECT f.id, f.forum_desc, f.moderators, f.num_topics, f.num_posts, f.last_post, f.last_post_id, f.last_poster, f.last_topic, mof.mf_upper FROM '.$db->prefix.'forums AS f LEFT JOIN '.$db->prefix.'mark_of_forum AS mof ON (mof.uid='.$user->id.' AND f.id=mof.fid) WHERE f.id IN ('.implode(',', $forums).')', true) or error('Unable to fetch forum list', __FILE__, __LINE__, $db->error());
+            $stmt = $this->c->DB->query('SELECT f.id, f.forum_desc, f.moderators, f.num_topics, f.num_posts, f.last_post, f.last_post_id, f.last_poster, f.last_topic, mof.mf_upper FROM ::forums AS f LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:id AND f.id=mof.fid) WHERE f.id IN (?ai:forums)', $vars);
         }
-
         $forums = [];
-        while ($cur = $db->fetch_assoc($result)) {
+        while ($cur = $stmt->fetch()) {
             $forums[$cur['id']] = $cur;
         }
-        $db->free_result($result);
 
         // поиск новых
         $new = [];
@@ -156,15 +155,19 @@ class Index extends Page
             }
             // проверка по темам
             if (! empty($new)) {
-                $result = $db->query('SELECT t.forum_id, t.id, t.last_post FROM '.$db->prefix.'topics AS t LEFT JOIN '.$db->prefix.'mark_of_topic AS mot ON (mot.uid='.$user->id.' AND mot.tid=t.id) WHERE t.forum_id IN('.implode(',', array_keys($new)).') AND t.last_post>'.$max.' AND t.moved_to IS NULL AND (mot.mt_upper IS NULL OR t.last_post>mot.mt_upper)') or error('Unable to fetch new topics', __FILE__, __LINE__, $db->error());
+                $vars = [
+                    ':id' => $user->id,
+                    ':forums' => $new,
+                    ':max' => $max,
+                ];
+                $stmt = $this->c->DB->query('SELECT t.forum_id, t.id, t.last_post FROM ::topics AS t LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:id AND mot.tid=t.id) WHERE t.forum_id IN(?ai:forums) AND t.last_post>?i:max AND t.moved_to IS NULL AND (mot.mt_upper IS NULL OR t.last_post>mot.mt_upper)', $vars);
                 $tmp = [];
-                while ($cur = $db->fetch_assoc($result)) {
+                while ($cur = $stmt->fetch()) {
                     if ($cur['last_post']>$new[$cur['forum_id']]) {
                         $tmp[$cur['forum_id']] = true;
                     }
                 }
                 $new = $tmp;
-                $db->free_result($result);
             }
         }
 

+ 0 - 127
app/Models/User - копия.php

@@ -1,127 +0,0 @@
-<?php
-
-namespace ForkBB\Models;
-
-use ForkBB\Core\Model; //????
-use R2\DependencyInjection\ContainerInterface;
-use RuntimeException;
-
-class User extends Model
-{
-    /**
-     * Контейнер
-     * @var ContainerInterface
-     */
-    protected $c;
-
-    /**
-     * @var array
-     */
-    protected $config;
-
-    /**
-     * @var UserCookie
-     */
-    protected $userCookie;
-
-    /**
-     * @var DB
-     */
-    protected $db;
-
-    /**
-     * Конструктор
-     */
-    public function __construct(array $config, $cookie, $db, ContainerInterface $container)
-    {
-        $this->config = $config;
-        $this->userCookie = $cookie;
-        $this->db = $db;
-        $this->c = $container;
-    }
-
-    /**
-     * @return User
-     */
-    public function init()
-    {
-        $this->current = $this->c->get('LoadCurrentUser')->load();
-
-        return $this;
-    }
-
-
-    /**
-     * Выход
-     */
-    public function logout()
-    {
-        if ($this->current['is_guest']) {
-            return;
-        }
-
-        $this->userCookie->deleteUserCookie();
-        $this->c->get('Online')->delete($this);
-        // Update last_visit (make sure there's something to update it with)
-        if (isset($this->current['logged'])) {
-            $this->db->query('UPDATE '.$this->db->prefix.'users SET last_visit='.$this->current['logged'].' WHERE id='.$this->current['id']) or error('Unable to update user visit data', __FILE__, __LINE__, $this->db->error());
-        }
-    }
-
-    /**
-     * Вход
-     * @param string $name
-     * @param string $password
-     * @param bool $save
-     * @return mixed
-     */
-    public function login($name, $password, $save)
-    {
-        $result = $this->db->query('SELECT u.id, u.group_id, u.username, u.password, u.registration_ip, g.g_moderator FROM '.$this->db->prefix.'users AS u LEFT JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id WHERE u.username=\''.$this->db->escape($name).'\'') or error('Unable to fetch user info', __FILE__, __LINE__, $this->db->error());
-        $user = $this->db->fetch_assoc($result);
-        $this->db->free_result($result);
-
-        if (empty($user['id'])) {
-            return false;
-        }
-
-        $authorized = false;
-        // For FluxBB by Visman 1.5.10.74 and above
-        if (strlen($user['password']) == 40) {
-            if (hash_equals($user['password'], sha1($password . $this->c->getParameter('SALT1')))) {
-                $authorized = true;
-
-                $user['password'] = password_hash($password, PASSWORD_DEFAULT);
-                $this->db->query('UPDATE '.$this->db->prefix.'users SET password=\''.$this->db->escape($user['password']).'\' WHERE id='.$user['id']) or error('Unable to update user password', __FILE__, __LINE__, $this->db->error());
-            }
-        } else {
-            $authorized = password_verify($password, $user['password']);
-        }
-
-        if (! $authorized) {
-            return false;
-        }
-
-        // Update the status if this is the first time the user logged in
-        if ($user['group_id'] == PUN_UNVERIFIED)
-        {
-            $this->db->query('UPDATE '.$this->db->prefix.'users SET group_id='.$this->config['o_default_user_group'].' WHERE id='.$user['id']) or error('Unable to update user status', __FILE__, __LINE__, $this->db->error());
-
-            $this->c->get('users_info update');
-        }
-
-        // перезаписываем ip админа и модератора - Visman
-        if ($this->config['o_check_ip'] == '1' && $user['registration_ip'] != $this->current['ip'])
-        {
-            if ($user['g_id'] == PUN_ADMIN || $user['g_moderator'] == '1')
-                $this->db->query('UPDATE '.$this->db->prefix.'users SET registration_ip=\''.$this->db->escape($this->current['ip']).'\' WHERE id='.$user['id']) or error('Unable to update user IP', __FILE__, __LINE__, $this->db->error());
-        }
-
-        $this->c->get('Online')->delete($this);
-
-        $this->c->get('UserCookie')->setUserCookie($user['id'], $user['password'], $save);
-
-        return $user['id'];
-    }
-
-}

+ 43 - 45
app/Models/UserMapper.php

@@ -20,11 +20,6 @@ class UserMapper
      */
     protected $config;
 
-    /**
-     * @var DB
-     */
-    protected $db;
-
     /**
      * Конструктор
      * @param Container $container
@@ -33,7 +28,6 @@ class UserMapper
     {
         $this->c = $container;
         $this->config = $container->config;
-        $this->db = $container->DB;
     }
 
     /**
@@ -54,23 +48,16 @@ class UserMapper
     public function getCurrent($id = 1)
     {
         $ip = $this->getIpAddress();
-        $id = (int) $id;
-
+        $user = null;
         if ($id > 1) {
-            $result = $this->db->query('SELECT u.*, g.*, o.logged, o.idle FROM '.$this->db->prefix.'users AS u INNER JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$this->db->prefix.'online AS o ON o.user_id=u.id WHERE u.id='.$id) or error('Unable to fetch user information', __FILE__, __LINE__, $this->db->error());
-            $user = $this->db->fetch_assoc($result);
-            $this->db->free_result($result);
+            $user = $this->c->DB->query('SELECT u.*, g.*, o.logged, o.idle FROM ::users AS u INNER JOIN ::groups AS g ON u.group_id=g.g_id LEFT JOIN ::online AS o ON o.user_id=u.id WHERE u.id=?i:id', [':id' => $id])->fetch();
         }
         if (empty($user['id'])) {
-            $result = $this->db->query('SELECT u.*, g.*, o.logged, o.last_post, o.last_search FROM '.$this->db->prefix.'users AS u INNER JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$this->db->prefix.'online AS o ON (o.user_id=1 AND o.ident=\''.$this->db->escape($ip).'\') WHERE u.id=1') or error('Unable to fetch guest information', __FILE__, __LINE__, $this->db->error());
-            $user = $this->db->fetch_assoc($result);
-            $this->db->free_result($result);
+            $user = $this->c->DB->query('SELECT u.*, g.*, o.logged, o.last_post, o.last_search FROM ::users AS u INNER JOIN ::groups AS g ON u.group_id=g.g_id LEFT JOIN ::online AS o ON (o.user_id=1 AND o.ident=?s:ip) WHERE u.id=1', [':ip' => $ip])->fetch();
         }
-
         if (empty($user['id'])) {
             throw new RuntimeException('Unable to fetch guest information. Your database must contain both a guest user and a guest user group.');
         }
-
         $user['ip'] = $ip;
         return new User($user, $this->c);
     }
@@ -82,7 +69,7 @@ class UserMapper
     public function updateLastVisit(User $user)
     {
         if ($user->isLogged) {
-            $this->db->query('UPDATE '.$this->db->prefix.'users SET last_visit='.$user->logged.' WHERE id='.$user->id) or error('Unable to update user visit data', __FILE__, __LINE__, $this->db->error());
+            $this->c->DB->exec('UPDATE ::users SET last_visit=?i:loggid WHERE id=?i:id', [':loggid' => $user->logged, ':id' => $user->id]);
         }
     }
 
@@ -97,33 +84,27 @@ class UserMapper
     {
         switch ($field) {
             case 'id':
-                $where = 'u.id=' . (int) $value;
+                $where = 'u.id= ?i';
                 break;
             case 'username':
-                $where = 'u.username=\'' . $this->db->escape($value) . '\'';
+                $where = 'u.username= ?s';
                 break;
             case 'email':
-                $where = 'u.email=\'' . $this->db->escape($value) . '\'';
+                $where = 'u.email= ?s';
                 break;
             default:
                 throw new InvalidArgumentException('Field not supported');
         }
-        $result = $this->db->query('SELECT u.*, g.* FROM '.$this->db->prefix.'users AS u LEFT JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id WHERE '.$where) or error('Unable to fetch user information', __FILE__, __LINE__, $this->db->error());
-
+        $result = $this->c->DB->query('SELECT u.*, g.* FROM ::users AS u LEFT JOIN ::groups AS g ON u.group_id=g.g_id WHERE ' . $where, [$value])->fetchAll();
         // найдено несколько пользователей
-        if ($this->db->num_rows($result) !== 1) {
-            return $this->db->num_rows($result);
+        if (count($result) !== 1) {
+            return count($result);
         }
-
-        $user = $this->db->fetch_assoc($result);
-        $this->db->free_result($result);
-
         // найден гость
-        if ($user['id'] == 1) {
+        if ($result[0]['id'] == 1) {
             return 1;
         }
-
-        return new User($user, $this->c);
+        return new User($result[0], $this->c);
     }
 
     /**
@@ -133,8 +114,12 @@ class UserMapper
      */
     public function isUnique($username)
     {
-        $result = $this->db->query('SELECT username FROM '.$this->db->prefix.'users WHERE (UPPER(username)=UPPER(\''.$this->db->escape($username).'\') OR UPPER(username)=UPPER(\''.$this->db->escape(preg_replace('%[^\p{L}\p{N}]%u', '', $username)).'\'))') or error('Unable to fetch user info', __FILE__, __LINE__, $this->db->error());
-        return ! $this->db->num_rows($result);
+        $vars = [
+            ':name' => $username,
+            ':other' => preg_replace('%[^\p{L}\p{N}]%u', '', $username),
+        ];
+        $result = $this->c->DB->query('SELECT username FROM ::users WHERE UPPER(username)=UPPER(?s:name) OR UPPER(username)=UPPER(?s:other)', $vars)->fetchAll();
+        return ! count($result);
     }
 
     /**
@@ -149,19 +134,17 @@ class UserMapper
             return;
         }
 
-        $set = [];
+        $set = $vars = [];
         foreach ($update as $field => $value) {
-            if (! is_string($field) || (null !== $value && ! is_int($value) && ! is_string($value))) {
-                return;
-            }
-            if (null === $value) {
-                $set[] = $field . '= NULL';
+            $vars[] = $value;
+            if (is_int($value)) {
+                $set[] = $field . ' = ?i';
             } else {
-                $set[] = $field . '=' . (is_int($value) ? $value : '\'' . $this->db->escape($value) . '\'');
+                $set[] = $field . ' = ?s';
             }
         }
-
-        $this->db->query('UPDATE '.$this->db->prefix.'users SET '.implode(', ', $set).' WHERE id='.$id) or error('Unable to update user data', __FILE__, __LINE__, $this->db->error());
+        $vars[] = $id;
+        $this->c->DB->query('UPDATE ::users SET ' . implode(', ', $set) . ' WHERE id=?i', $vars); //????
     }
 
     /**
@@ -172,8 +155,23 @@ class UserMapper
      */
     public function newUser(User $user)
     {
-        $this->db->query('INSERT INTO '.$this->db->prefix.'users (username, group_id, password, email, email_confirmed, email_setting, timezone, dst, language, style, registered, registration_ip, activate_string, u_mark_all_read) VALUES(\''.$this->db->escape($user->username).'\', '.$user->groupId.', \''.$this->db->escape($user->password).'\', \''.$this->db->escape($user->email).'\', '.$user->emailConfirmed.', '.$this->config['o_default_email_setting'].', '.$this->config['o_default_timezone'].' , '.$this->config['o_default_dst'].', \''.$this->db->escape($user->language).'\', \''.$user->style.'\', '.time().', \''.$this->db->escape($this->getIpAddress()).'\', \''.$this->db->escape($user->activateString).'\', '.$user->uMarkAllRead.')') or error('Unable to create user', __FILE__, __LINE__, $this->db->error());
-        $new_uid = $this->db->insert_id(); //????
-        return $new_uid;
+        $vars = [
+            ':name' => $user->username,
+            ':group' => $user->groupId,
+            ':password' => $user->password,
+            ':email' => $user->email,
+            ':confirmed' => $user->emailConfirmed,
+            ':setting' => $this->config['o_default_email_setting'],
+            ':timezone' => $this->config['o_default_timezone'],
+            ':dst' => $this->config['o_default_dst'],
+            ':language' => $user->language,
+            ':style' => $user->style,
+            ':registered' => time(),
+            ':ip' => $this->getIpAddress(),
+            ':activate' => $user->activateString,
+            ':mark' => $user->uMarkAllRead,
+        ];
+        $this->c->DB->query('INSERT INTO ::users (username, group_id, password, email, email_confirmed, email_setting, timezone, dst, language, style, registered, registration_ip, activate_string, u_mark_all_read) VALUES(?s:name, ?i:group, ?s:password, ?s:email, ?i:confirmed, ?i:setting, ?s:timezone, ?i:dst, ?s:language, ?s:style, ?i:registered, ?s:ip, ?s:activate, ?i:mark)', $vars);
+        return $this->c->DB->lastInsertId();
     }
 }

+ 1 - 1
app/Models/Validator.php

@@ -345,7 +345,7 @@ class Validator
         if (false === $error) {
             return [null, $type, 'The :alias is not required'];
         } else {
-            return [$value, $type, false];
+            return [$value, $type, true];
         }
     }
 

+ 3 - 0
app/lang/English/admin_index.po

@@ -140,3 +140,6 @@ msgstr "Rows: %s"
 
 msgid "Database data size"
 msgstr "Size: %s"
+
+msgid "Other"
+msgstr "Other:"

+ 3 - 0
app/lang/Russian/admin_index.po

@@ -140,3 +140,6 @@ msgstr "Строк: %s"
 
 msgid "Database data size"
 msgstr "Размер: %s"
+
+msgid "Other"
+msgstr "Дополнительные данные:"

+ 7 - 1
app/templates/admin/statistics.tpl

@@ -14,10 +14,16 @@
             </dd>
             <dt>{!! __('Database label') !!}</dt>
             <dd>
-              {!! $dbVersion !!}
+              {{ $dbVersion }}
 @if($tRecords && $tSize)
               <br>{!! __('Database data rows', $tRecords) !!}
               <br>{!! __('Database data size', $tSize) !!}
+@endif
+@if($tOther)
+              <br><br>{!! __('Other')!!}
+@foreach($tOther as $key => $value)
+              <br>{{ $key }} = {{ $value }}
+@endforeach
 @endif
             </dd>
 @endif