浏览代码

2017-11-03

Visman 7 年之前
父节点
当前提交
37f80f6fb8
共有 100 个文件被更改,包括 3303 次插入2603 次删除
  1. 4 2
      app/Controllers/Install.php
  2. 11 11
      app/Controllers/Primary.php
  3. 10 8
      app/Controllers/Routing.php
  4. 0 120
      app/Core/AbstractModel.php
  5. 8 5
      app/Core/Cache/FileCache.php
  6. 18 8
      app/Core/Container.php
  7. 0 118
      app/Core/Cookie.php
  8. 5 0
      app/Core/Csrf.php
  9. 12 12
      app/Core/DB.php
  10. 41 0
      app/Core/DB/mysql.php
  11. 8 3
      app/Core/Func.php
  12. 10 2
      app/Core/Lang.php
  13. 42 0
      app/Core/Mail.php
  14. 0 244
      app/Core/Request.php
  15. 10 0
      app/Core/Router.php
  16. 20 4
      app/Core/Secury.php
  17. 34 6
      app/Core/Validator.php
  18. 29 12
      app/Core/View.php
  19. 10 2
      app/Models/Actions/CacheGenerator.php
  20. 1 1
      app/Models/Actions/CacheLoader.php
  21. 0 111
      app/Models/Actions/CheckBans.php
  22. 23 0
      app/Models/AdminList.php
  23. 22 0
      app/Models/AdminList/Load.php
  24. 46 0
      app/Models/BanList.php
  25. 67 0
      app/Models/BanList/Check.php
  26. 25 0
      app/Models/BanList/Delete.php
  27. 35 0
      app/Models/BanList/IsBanned.php
  28. 60 0
      app/Models/BanList/Load.php
  29. 41 0
      app/Models/CensorshipList.php
  30. 33 0
      app/Models/CensorshipList/Load.php
  31. 23 0
      app/Models/Config.php
  32. 22 0
      app/Models/Config/Load.php
  33. 37 0
      app/Models/Config/Save.php
  34. 212 0
      app/Models/Cookie.php
  35. 25 0
      app/Models/DBMap.php
  36. 91 0
      app/Models/DataModel.php
  37. 54 0
      app/Models/Forum.php
  38. 62 0
      app/Models/ForumList.php
  39. 93 0
      app/Models/ForumList/Load.php
  40. 132 0
      app/Models/ForumList/LoadTree.php
  41. 33 0
      app/Models/MethodModel.php
  42. 115 0
      app/Models/Model.php
  43. 31 39
      app/Models/Online.php
  44. 144 267
      app/Models/Page.php
  45. 90 0
      app/Models/Pages/Admin.php
  46. 0 105
      app/Models/Pages/Admin/Admin.php
  47. 35 39
      app/Models/Pages/Admin/Groups.php
  48. 9 17
      app/Models/Pages/Admin/Index.php
  49. 24 30
      app/Models/Pages/Admin/Statistics.php
  50. 102 99
      app/Models/Pages/Auth.php
  51. 26 27
      app/Models/Pages/Ban.php
  52. 27 4
      app/Models/Pages/CrumbTrait.php
  53. 20 29
      app/Models/Pages/Debug.php
  54. 34 68
      app/Models/Pages/Forum.php
  55. 48 94
      app/Models/Pages/ForumsTrait.php
  56. 21 42
      app/Models/Pages/Index.php
  57. 99 90
      app/Models/Pages/Install.php
  58. 14 45
      app/Models/Pages/Maintenance.php
  59. 12 9
      app/Models/Pages/OnlineTrait.php
  60. 68 0
      app/Models/Pages/Post.php
  61. 26 52
      app/Models/Pages/Redirect.php
  62. 85 92
      app/Models/Pages/Register.php
  63. 23 38
      app/Models/Pages/Rules.php
  64. 243 162
      app/Models/Pages/Topic.php
  65. 9 7
      app/Models/Pages/UsersTrait.php
  66. 23 0
      app/Models/SmileyList.php
  67. 22 0
      app/Models/SmileyList/Load.php
  68. 30 0
      app/Models/Stats.php
  69. 27 0
      app/Models/Stats/Load.php
  70. 82 0
      app/Models/Stopwords.php
  71. 21 39
      app/Models/User.php
  72. 26 0
      app/Models/User/IsUnique.php
  73. 46 0
      app/Models/User/Load.php
  74. 70 53
      app/Models/User/LoadUserFromCookie.php
  75. 71 0
      app/Models/User/Save.php
  76. 19 0
      app/Models/User/UpdateLastVisit.php
  77. 0 157
      app/Models/UserCookie.php
  78. 0 177
      app/Models/UserMapper.php
  79. 6 7
      app/bootstrap.php
  80. 2 2
      app/lang/English/common.po
  81. 3 3
      app/lang/Russian/common.po
  82. 11 11
      app/templates/admin/group.tpl
  83. 15 15
      app/templates/admin/groups.tpl
  84. 2 2
      app/templates/admin/index.tpl
  85. 10 10
      app/templates/admin/statistics.tpl
  86. 5 5
      app/templates/ban.tpl
  87. 2 2
      app/templates/change_passphrase.tpl
  88. 11 11
      app/templates/forum.tpl
  89. 2 2
      app/templates/index.tpl
  90. 3 3
      app/templates/layouts/admin.tpl
  91. 4 4
      app/templates/layouts/debug.tpl
  92. 3 3
      app/templates/layouts/form.tpl
  93. 23 23
      app/templates/layouts/install.tpl
  94. 10 10
      app/templates/layouts/iswev.tpl
  95. 10 10
      app/templates/layouts/main.tpl
  96. 5 5
      app/templates/layouts/redirect.tpl
  97. 14 14
      app/templates/layouts/stats.tpl
  98. 8 8
      app/templates/login.tpl
  99. 1 1
      app/templates/maintenance.tpl
  100. 2 2
      app/templates/message.tpl

+ 4 - 2
app/Controllers/Install.php

@@ -15,6 +15,7 @@ class Install
 
     /**
      * Конструктор
+     *
      * @param Container $container
      */
     public function __construct(Container $container)
@@ -24,6 +25,7 @@ class Install
 
     /**
      * Маршрутиризация
+     *
      * @return Page
      */
     public function routing()
@@ -38,7 +40,7 @@ class Install
             . preg_replace('%:(80|443)$%', '', $_SERVER['HTTP_HOST'])
             . substr($uri, 0, (int) strrpos($uri, '/'));
 
-        $this->c->Lang->load('common', $this->c->config['o_default_lang']);
+        $this->c->Lang->load('common', $this->c->config->o_default_lang);
         $this->c->user = new User(['id' => 2, 'group_id' => $this->c->GROUP_ADMIN], $this->c);
 
         $r = $this->c->Router;
@@ -54,7 +56,7 @@ class Install
                 $page = $this->c->$page->$action($route[2]);
                 break;
             default:
-                $page = $this->c->Redirect->setPage('Install')->setMessage('Redirect to install');
+                $page = $this->c->Redirect->page('Install')->message('Redirect to install');
                 break;
         }
         return $page;

+ 11 - 11
app/Controllers/Primary.php

@@ -14,6 +14,7 @@ class Primary
 
     /**
      * Конструктор
+     *
      * @param Container $container
      */
     public function __construct(Container $container)
@@ -25,28 +26,27 @@ class Primary
      * Проверка на обслуживание
      * Проверка на обновление
      * Проверка на бан
+     *
      * @return Page|null
      */
     public function check()
     {
-        if ($this->c->config['o_maintenance'] && ! $this->c->MAINTENANCE_OFF) {
-           if (! in_array($this->c->UserCookie->id(), $this->c->admins)
-               || ! in_array($this->c->user->id, $this->c->admins)
-           ) {
-               return $this->c->Maintenance;
-           }
+        if ($this->c->config->o_maintenance && ! $this->c->MAINTENANCE_OFF) {
+            if (! in_array($this->c->Cookie->uId, $this->c->admins->list) //????
+                || ! in_array($this->c->user->id, $this->c->admins->list) //????
+            ) {
+                return $this->c->Maintenance;
+            }
         }
 
-        if (empty($this->c->config['i_fork_revision'])
-            || $this->c->config['i_fork_revision'] < $this->c->FORK_REVISION
+        if ($this->c->config->i_fork_revision < $this->c->FORK_REVISION
         ) {
             header('Location: db_update.php'); //????
             exit;
         }
 
-        $ban = $this->c->CheckBans->check();
-        if (is_array($ban)) {
-            return $this->c->Ban->ban($ban);
+        if (! $this->c->user->isAdmin && $this->c->bans->check($this->c->user)) {
+            return $this->c->Ban->ban($this->c->user);
         }
     }
 }

+ 10 - 8
app/Controllers/Routing.php

@@ -14,6 +14,7 @@ class Routing
 
     /**
      * Конструктор
+     *
      * @param Container $container
      */
     public function __construct(Container $container)
@@ -23,6 +24,7 @@ class Routing
 
     /**
      * Маршрутиризация
+     *
      * @return Page
      */
     public function routing()
@@ -44,7 +46,7 @@ class Routing
             $r->add('POST', '/login/{email}/{key}/{hash}', 'Auth:changePassPost');
 
             // регистрация
-            if ($config['o_regs_allow'] == '1') {
+            if ($config->o_regs_allow == '1') {
                 $r->add('GET',  '/registration', 'Rules:confirmation', 'Register');
                 $r->add('POST', '/registration/agree', 'Register:reg', 'RegisterForm');
                 $r->add('GET',  '/registration/activate/{id:\d+}/{key}/{hash}', 'Register:activate', 'RegActivate');
@@ -58,19 +60,19 @@ class Routing
             $r->add('GET', '/registration[/{tail:.*}]', 'Redirect:toIndex');
         }
         // просмотр разрешен
-        if ($user->gReadBoard == '1') {
+        if ($user->g_read_board == '1') {
             // главная
             $r->add('GET', '/', 'Index:view', 'Index');
             // правила
-            if ($config['o_rules'] == '1' && (! $user->isGuest || $config['o_regs_allow'] == '1')) {
+            if ($config->o_rules == '1' && (! $user->isGuest || $config->o_regs_allow == '1')) {
                 $r->add('GET', '/rules', 'Rules:view', 'Rules');
             }
             // поиск
-            if ($user->gSearch == '1') {
+            if ($user->g_search == '1') {
                 $r->add('GET', '/search', 'Search:view', 'Search');
             }
             // юзеры
-            if ($user->gViewUsers == '1') {
+            if ($user->g_view_users == '1') {
                 // список пользователей
                 $r->add('GET', '/userlist[/{page:[1-9]\d*}]', 'Userlist:view', 'Userlist');
                 // юзеры
@@ -93,7 +95,7 @@ class Routing
             $r->add('GET', '/post/{id:[1-9]\d*}/delete', 'Delete:delete', 'DeletePost'); //????
             $r->add('GET', '/post/{id:[1-9]\d*}/edit', 'Edit:edit', 'EditPost'); //????
             $r->add('GET', '/post/{id:[1-9]\d*}/report', 'Report:report', 'ReportPost'); //????
-            
+
         }
         // админ и модератор
         if ($user->isAdmMod) {
@@ -127,8 +129,8 @@ class Routing
                 break;
             case $r::NOT_FOUND:
                 // ... 404 Not Found
-                if ($user->gReadBoard != '1' && $user->isGuest) {
-                    $page = $this->c->Redirect->setPage('Login');
+                if ($user->g_read_board != '1' && $user->isGuest) {
+                    $page = $this->c->Redirect->page('Login');
                 } else {
                     $page = $this->c->Message->message('Bad request');
                 }

+ 0 - 120
app/Core/AbstractModel.php

@@ -1,120 +0,0 @@
-<?php
-
-namespace ForkBB\Core;
-
-abstract class AbstractModel
-{
-    /**
-     * Данные модели
-     * @var array
-     */
-    protected $data;
-
-    abstract protected function beforeConstruct(array $data);
-
-    /**
-     * Конструктор
-     * @param array $data
-     */
-    public function __construct(array $data = [])
-    {
-        $data = $this->beforeConstruct($data);
-        foreach ($data as $key => $val) {
-            if (is_string($key)) {
-                $this->data[$this->camelCase($key)] = $val;
-            }
-        }
-    }
-
-    /**
-     * Проверяет наличие свойства
-     * @param mixed $key
-     * @return bool
-     */
-    public function __isset($key)
-    {
-        return is_string($key) && isset($this->data[$key]);
-    }
-
-    /**
-     * Удаляет свойство
-     * @param mixed $key
-     */
-    public function __unset($key)
-    {
-        if (is_string($key)) {
-            unset($this->data[$key]);
-        }
-    }
-
-    /**
-     * Устанавливает значение для свойства
-     * При $key = __attrs ожидаем массив в $val
-     * @param mixed $key
-     * @param mixed $vak
-     */
-    public function __set($key, $val)
-    {
-        if ('__attrs' === $key) {
-            if (is_array($val)) {
-                foreach ($val as $x => $y) {
-                    $x = $this->camelCase($x);
-                    $this->$x = $y;
-                }
-            }
-        } elseif (is_string($key)) {
-            $method = 'set' . ucfirst($key);
-            if (method_exists($this, $method)) {
-                $this->$method($val);
-            } else {
-                $this->data[$key] = $val;
-            }
-        }
-    }
-
-    /**
-     * Возвращает значение свойства
-     * При $key = __attrs возвращает все свойства
-     * @param mixed $key
-     * @return mixed
-     */
-    public function __get($key)
-    {
-        if ('__attrs' === $key) {
-            $data = [];
-            foreach ($this->data as $x => $y) {
-                $data[$this->underScore($x)] = $y; //????
-            }
-            return $data;
-        } elseif (is_string($key)) {
-            $method = 'get' . ucfirst($key);
-            if (method_exists($this, $method)) {
-                return $this->$method();
-            } else {
-                return isset($this->data[$key]) ? $this->data[$key] : null;
-            }
-        }
-    }
-
-    /**
-     * Преобразует строку в camelCase
-     * @param string $str
-     * @return string
-     */
-    protected function camelCase($str)
-    {
-       return lcfirst(str_replace('_', '', ucwords($str, '_')));
-    }
-
-    /**
-     * Преобразует строку в under_score
-     * @param string $str
-     * @return string
-     */
-    protected function underScore($str)
-    {
-       return preg_replace('%([A-Z])%', function($match) {
-           return '_' . strtolower($match[1]);
-       }, $str);
-    }
-}

+ 8 - 5
app/Core/Cache/FileCache.php

@@ -18,8 +18,8 @@ class FileCache implements ProviderCacheInterface
      *
      * @param string $dir
      *
-     * @throws \InvalidArgumentException
-     * @throws \RuntimeException
+     * @throws InvalidArgumentException
+     * @throws RuntimeException
      */
     public function __construct($dir)
     {
@@ -63,7 +63,8 @@ class FileCache implements ProviderCacheInterface
      * @param mixed $value
      * @param int $ttl
      *
-     * @throws \RuntimeException
+     * @throws RuntimeException
+     *
      * @return bool
      */
     public function set($key, $value, $ttl = null)
@@ -84,7 +85,8 @@ class FileCache implements ProviderCacheInterface
      *
      * @param string $key
      *
-     * @throws \RuntimeException
+     * @throws RuntimeException
+     *
      * @return bool
      */
     public function delete($key)
@@ -141,7 +143,8 @@ class FileCache implements ProviderCacheInterface
      *
      * @param string $key
      *
-     * @throws \InvalidArgumentException
+     * @throws InvalidArgumentException
+     *
      * @return string
      */
     protected function file($key)

+ 18 - 8
app/Core/Container.php

@@ -18,6 +18,7 @@ class Container
 
     /**
      * Конструктор
+     *
      * @param array config
      */
     public function __construct(array $config = null)
@@ -37,6 +38,7 @@ class Container
 
     /**
      * Adding config
+     *
      * @param array config
      */
     public function config(array $config)
@@ -53,9 +55,12 @@ class Container
 
     /**
      * Gets a service or parameter.
+     *
      * @param string $id
+     *
+     * @throws InvalidArgumentException
+     *
      * @return mixed
-     * @throws \InvalidArgumentException
      */
     public function __get($id)
     {
@@ -79,7 +84,7 @@ class Container
             $toShare = false;
             $config = (array) $this->multiple[$id];
         } else {
-            throw new InvalidArgumentException('Wrong property name '.$id);
+            throw new InvalidArgumentException('Wrong property name: ' . $id);
         }
         // N.B. "class" is just the first element, regardless of its key
         $class = array_shift($config);
@@ -107,6 +112,7 @@ class Container
     /**
      * Sets a service or parameter.
      * Provides a fluent interface.
+     *
      * @param string $id
      * @param mixed $service
      */
@@ -121,8 +127,10 @@ class Container
 
     /**
      * Gets data from array.
+     *
      * @param array $array
      * @param array $tree
+     *
      * @return mixed
      */
     public function fromArray(array $array, array $tree)
@@ -145,21 +153,23 @@ class Container
      * @param string $name  The parameter name
      * @param mixed  $value The parameter value
      *
+     * @throws InvalidArgumentException
+     *
      * @return ContainerInterface Self reference
      */
     public function setParameter($name, $value)
     {
         $segments = explode('.', $name);
         $n = count($segments);
-        $ptr =& $this->config;
+        $ptr = &$this->config;
         foreach ($segments as $s) {
             if (--$n) {
-                if (!array_key_exists($s, $ptr)) {
+                if (! array_key_exists($s, $ptr)) {
                     $ptr[$s] = [];
-                } elseif (!is_array($ptr[$s])) {
-                    throw new \InvalidArgumentException("Scalar \"{$s}\" in the path \"{$name}\"");
+                } elseif (! is_array($ptr[$s])) {
+                    throw new InvalidArgumentException("Scalar '{$s}' in the path '{$name}'");
                 }
-                $ptr =& $ptr[$s];
+                $ptr = &$ptr[$s];
             } else {
                 $ptr[$s] = $value;
             }
@@ -181,7 +191,7 @@ class Container
                         '~%([a-z0-9_]+(?:\.[a-z0-9_]+)*)%~i',
                         function ($matches) {
                             return $this->__get($matches[1]);
-                        }, 
+                        },
                         $value
                     );
                 }

+ 0 - 118
app/Core/Cookie.php

@@ -1,118 +0,0 @@
-<?php
-
-namespace ForkBB\Core;
-
-use ForkBB\Core\Secury;
-
-class Cookie
-{
-    /**
-     * @var Secury
-     */
-    protected $secury;
-
-    /**
-     * Префикс для наименований
-     * @var string
-     */
-    protected $prefix;
-
-    /**
-     * Домен
-     * @var string
-     */
-    protected $domain;
-
-    /**
-     * Путь
-     * @var string
-     */
-    protected $path;
-
-    /**
-     * Флаг передачи кук по защищенному соединению
-     * @var bool
-     */
-    protected $secure;
-
-    /**
-     * Конструктор
-     *
-     * @param Secury $secury
-     * @param array $options
-     */
-    public function __construct(Secury $secury, array $options)
-    {
-        $this->secury = $secury;
-
-        $options += [
-            'prefix' => '',
-            'domain' => '',
-            'path'   => '',
-            'secure' => false,
-        ];
-
-        $this->prefix = (string) $options['prefix'];
-        $this->domain = (string) $options['domain'];
-        $this->path = (string) $options['path'];
-        $this->secure = (bool) $options['secure'];
-    }
-
-    /**
-     * Устанавливает куку
-     *
-     * @param string $name
-     * @param string $value
-     * @param int $expire
-     * @param string $path
-     * @param string $domain
-     * @param bool $secure
-     * @param bool $httponly
-     *
-     * @return bool
-     */
-    public function set($name, $value, $expire = 0, $path = null, $domain = null, $secure = false, $httponly = true)
-    {
-        $name = $this->prefix . $name;
-        $path = isset($path) ? $path : $this->path;
-        $domain = isset($domain) ? $domain : $this->domain;
-        $secure = $this->secure || (bool) $secure;
-        $result = setcookie($name, $value, $expire, $path, $domain, $secure, (bool) $httponly);
-        if ($result) {
-            $_COOKIE[$name] = $value;
-        }
-        return $result;
-    }
-
-    /**
-     * Получает значение куки
-     *
-     * @param string $name
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function get($name, $default = null)
-    {
-        $name = $this->prefix . $name;
-        return isset($_COOKIE[$name]) ? $this->secury->replInvalidChars($_COOKIE[$name]) : $default;
-    }
-
-    /**
-     * Удаляет куку
-     *
-     * @param string $name
-     * @param string $path
-     * @param string $domain
-     *
-     * @return bool
-     */
-    public function delete($name, $path = null, $domain = null)
-    {
-        $result = $this->set($name, '', 1, $path, $domain);
-        if ($result) {
-            unset($_COOKIE[$this->prefix . $name]);
-        }
-        return $result;
-    }
-}

+ 5 - 0
app/Core/Csrf.php

@@ -18,6 +18,7 @@ class Csrf
 
     /**
      * Конструктор
+     *
      * @param Secury $secury
      * @param User $user
      */
@@ -29,9 +30,11 @@ class Csrf
 
     /**
      * Возвращает csrf токен
+     *
      * @param string $marker
      * @param array $args
      * @param string|int $time
+     *
      * @return string
      */
     public function create($marker, array $args = [], $time = null)
@@ -48,9 +51,11 @@ class Csrf
 
     /**
      * Проверка токена
+     *
      * @param mixed $token
      * @param string $marker
      * @param array $args
+     *
      * @return bool
      */
     public function verify($token, $marker, array $args = [])

+ 12 - 12
app/Core/DB.php

@@ -2,10 +2,10 @@
 
 namespace ForkBB\Core;
 
+use ForkBB\Core\DBStatement;
 use PDO;
 use PDOStatement;
 use PDOException;
-use ForkBB\Core\DBStatement;
 
 class DB extends PDO
 {
@@ -127,7 +127,7 @@ class DB extends PDO
         $idxOut = 1;
         $map = [];
         $query = preg_replace_callback(
-            '%(?=[?:])(?<![\w?:])(?:::(\w+)|\?(?![?:])(?:(\w+)(?::(\w+))?)?|:(\w+))%', 
+            '%(?=[?:])(?<![\w?:])(?:::(\w+)|\?(?![?:])(?:(\w+)(?::(\w+))?)?|:(\w+))%',
             function($matches) use ($params, &$idxIn, &$idxOut, &$map) {
                 if (! empty($matches[1])) {
                     return $this->dbPrefix . $matches[1];
@@ -170,7 +170,7 @@ class DB extends PDO
                     $map[$key][] = $name;
                 }
                 return implode(',', $res);
-            }, 
+            },
             $query
         );
         return $map;
@@ -178,10 +178,10 @@ class DB extends PDO
 
     /**
      * Метод возвращает значение из массива параметров по ключу или исключение
-     * 
+     *
      * @param mixed $key
      * @param array $params
-     * 
+     *
      * @throws PDOException
      *
      * @return mixed
@@ -189,7 +189,7 @@ class DB extends PDO
     public function getValue($key, array $params)
     {
         if (
-            is_string($key) 
+            is_string($key)
             && (isset($params[':' . $key]) || array_key_exists(':' . $key, $params))
         ) {
             return $params[':' . $key];
@@ -205,7 +205,7 @@ class DB extends PDO
 
     /**
      * Метод для получения количества выполненных запросов
-     * 
+     *
      * @return int
      */
     public function getCount()
@@ -215,7 +215,7 @@ class DB extends PDO
 
     /**
      * Метод для получения статистики выполненных запросов
-     * 
+     *
      * @return array
      */
     public function getQueries()
@@ -225,7 +225,7 @@ class DB extends PDO
 
     /**
      * Метод для сохранения статистики по выполненному запросу
-     * 
+     *
      * @param string $query
      * @param float $time
      */
@@ -291,11 +291,11 @@ class DB extends PDO
         }
 
         $map = $this->parse($query, $params);
-        
+
         $start = microtime(true);
         $stmt  = parent::prepare($query, $options);
         $this->delta = microtime(true) - $start;
-        
+
         $stmt->setMap($map);
 
         $stmt->bindValueList($params);
@@ -331,7 +331,7 @@ class DB extends PDO
         $start = microtime(true);
         $stmt  = parent::prepare($query);
         $this->delta = microtime(true) - $start;
-        
+
         $stmt->setMap($map);
 
         if ($stmt->execute($params)) {

+ 41 - 0
app/Core/DB/mysql.php

@@ -34,6 +34,25 @@ class Mysql
         '%^SERIAL$%i' => 'INT(10) UNSIGNED AUTO_INCREMENT',
     ];
 
+    /**
+     * Подстановка типов полей для карты БД
+     * @var array
+     */
+    protected $types = [
+        'bool'      => 'b',
+        'boolean'   => 'b',
+        'tinyint'   => 'i',
+        'smallint'  => 'i',
+        'mediumint' => 'i',
+        'int'       => 'i',
+        'integer'   => 'i',
+        'bigint'    => 'i',
+        'decimal'   => 'i',
+        'dec'       => 'i',
+        'float'     => 'i',
+        'double'    => 'i',
+    ];
+
     /**
      * Конструктор
      *
@@ -524,4 +543,26 @@ class Mysql
             'server info' => $this->db->getAttribute(\PDO::ATTR_SERVER_INFO),
         ] + $other;
     }
+
+    /**
+     * Формирует карту базы данных
+     *
+     * @return array
+     */
+    public function getMap()
+    {
+        $stmt = $this->db->query('SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE ?s', ["{$this->dbPrefix}%"]);
+        $result = [];
+        $table = null;
+        while ($row = $stmt->fetch()) {
+            if ($table !== $row['TABLE_NAME']) {
+                $table = $row['TABLE_NAME'];
+                $tableNoPref = substr($table, strlen($this->dbPrefix));
+                $result[$tableNoPref] = [];
+            }
+            $type = strtolower($row['DATA_TYPE']);
+            $result[$tableNoPref][$row['COLUMN_NAME']] = isset($this->types[$type]) ? $this->types[$type] : 's';
+        }
+        return $result;
+    }
 }

+ 8 - 3
app/Core/Func.php

@@ -24,6 +24,8 @@ class Func
 
     /**
      * Конструктор
+     *
+     * @param Container $container
      */
     public function __construct(Container $container)
     {
@@ -61,13 +63,16 @@ class Func
     }
 
     /**
+     * Пагинация
+     *
      * @param int $all
      * @param int $cur
      * @param string $marker
      * @param array $args
-     * @return array 
+     *
+     * @return array
      */
-    public function paginate($all, $cur, $marker, array $args = []) 
+    public function paginate($all, $cur, $marker, array $args = [])
     {
         $pages = [];
         if ($all < 2) {
@@ -88,7 +93,7 @@ class Func
                 }
                 $tpl[$all] = $all;
             } else {
-                $tpl = $all < 7 
+                $tpl = $all < 7
                     ? array_slice([2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6], 0, $all - 1)
                     : [2 => 2, 3 => 3, 4 => 4, $all => $all];
             }

+ 10 - 2
app/Core/Lang.php

@@ -27,6 +27,7 @@ class Lang
 
     /**
      * Конструктор
+     *
      * @param Container $container
      */
     public function __construct(Container $container)
@@ -37,8 +38,10 @@ class Lang
 
     /**
      * Ищет сообщение в загруженных переводах
+     *
      * @param string $message
      * @param string $lang
+     *
      * @return string|array
      */
     public function get($message, $lang = null)
@@ -58,6 +61,7 @@ class Lang
 
     /**
      * Загрузка языкового файла
+     *
      * @param string $name
      * @param string $lang
      * @param string $path
@@ -95,9 +99,12 @@ class Lang
 
     /**
      * Получение массива перевода из строки (.po файла)
+     *
      * @param string $str
-     * @return array
+     *
      * @throws RuntimeException
+     *
+     * @return array
      */
     protected function arrayFromStr($str)
     {
@@ -242,7 +249,9 @@ class Lang
     /**
      * Получение оригинальной строки с удалением кавычек
      * и преобразованием спецсимволов
+     *
      * @param string $line
+     *
      * @return string
      */
     protected function originalLine($line)
@@ -256,5 +265,4 @@ class Lang
             $line
         );
     }
-
 }

+ 42 - 0
app/Core/Mail.php

@@ -54,6 +54,7 @@ class Mail
 
     /**
      * Конструктор
+     *
      * @param mixed $host
      * @param mixed $user
      * @param mixed $pass
@@ -81,9 +82,11 @@ class Mail
 
     /**
      * Валидация email
+     *
      * @param mixed $email
      * @param bool $strict
      * @param bool $idna
+     *
      * @return false|string
      */
     public function valid($email, $strict = false, $idna = false)
@@ -113,6 +116,7 @@ class Mail
 
     /**
      * Сброс
+     *
      * @return Mail
      */
     public function reset()
@@ -125,7 +129,9 @@ class Mail
 
     /**
      * Задает тему письма
+     *
      * @param string $subject
+     *
      * @return Mail
      */
     public function setSubject($subject)
@@ -136,8 +142,10 @@ class Mail
 
     /**
      * Добавляет заголовок To
+     *
      * @param string|array $email
      * @param string $name
+     *
      * @return Mail
      */
     public function addTo($email, $name = null)
@@ -157,8 +165,10 @@ class Mail
 
     /**
      * Задает заголовок To
+     *
      * @param string|array $email
      * @param string $name
+     *
      * @return Mail
      */
     public function setTo($email, $name = null)
@@ -169,8 +179,10 @@ class Mail
 
     /**
      * Задает заголовок From
+     *
      * @param string $email
      * @param string $name
+     *
      * @return Mail
      */
     public function setFrom($email, $name = null)
@@ -184,8 +196,10 @@ class Mail
 
     /**
      * Задает заголовок Reply-To
+     *
      * @param string $email
      * @param string $name
+     *
      * @return Mail
      */
     public function setReplyTo($email, $name = null)
@@ -199,8 +213,10 @@ class Mail
 
     /**
      * Форматирование адреса
+     *
      * @param string|array $email
      * @param string $name
+     *
      * @return string
      */
     protected function formatAddress($email, $name = null)
@@ -215,7 +231,9 @@ class Mail
 
     /**
      * Кодирование заголовка/имени
+     *
      * @param string $str
+     *
      * @return string
      */
     protected function encodeText($str)
@@ -229,7 +247,9 @@ class Mail
 
     /**
      * Фильтрация имени
+     *
      * @param string $name
+     *
      * @return string
      */
     protected function filterName($name)
@@ -239,7 +259,9 @@ class Mail
 
     /**
      * Установка папки для поиска шаблонов писем
+     *
      * @param string $folder
+     *
      * @return Mail
      */
     public function setFolder($folder)
@@ -250,7 +272,9 @@ class Mail
 
     /**
      * Установка языка для поиска шаблонов писем
+     *
      * @param string $language
+     *
      * @return Mail
      */
     public function setLanguage($language)
@@ -261,9 +285,12 @@ class Mail
 
     /**
      * Задает сообщение по шаблону
+     *
      * @param string $tpl
      * @param array $data
+     *
      * @throws MailException
+     *
      * @return Mail
      */
     public function setTpl($tpl, array $data)
@@ -286,7 +313,9 @@ class Mail
 
     /**
      * Задает сообщение
+     *
      * @param string $message
+     *
      * @return Mail
      */
     public function setMessage($message)
@@ -300,7 +329,9 @@ class Mail
 
     /**
      * Отправляет письмо
+     *
      * @throws MailException
+     *
      * @return bool
      */
     public function send()
@@ -335,6 +366,7 @@ class Mail
 
     /**
      * Отправка письма через функцию mail
+     *
      * @return bool
      */
     protected function mail()
@@ -349,7 +381,9 @@ class Mail
 
     /**
      * Переводит заголовки из массива в строку
+     *
      * @param array $headers
+     *
      * @return string
      */
     protected function strHeaders(array $headers)
@@ -363,7 +397,9 @@ class Mail
 
     /**
      * Отправка письма через smtp
+     *
      * @throws SmtpException
+     *
      * @return bool
      */
     protected function smtp()
@@ -423,9 +459,12 @@ class Mail
      * Отправляет данные на сервер
      * Проверяет ответ
      * Возвращает код ответа
+     *
      * @param string $data
      * @param mixed $code
+     *
      * @throws SmtpException
+     *
      * @return string
      */
     protected function smtpData($data, $code)
@@ -454,7 +493,9 @@ class Mail
 
     /**
      * Выделяет email из заголовка
+     *
      * @param string $str
+     *
      * @return string
      */
     protected function getEmailFrom($str)
@@ -469,6 +510,7 @@ class Mail
 
     /**
      * Возвращает имя сервера или его ip
+     *
      * @return string
      */
     protected function hostname()

+ 0 - 244
app/Core/Request.php

@@ -1,244 +0,0 @@
-<?php
-
-namespace ForkBB\Core;
-
-class Request
-{
-    /**
-     * @var Secury
-     */
-    protected $secury;
-
-    /**
-     * Конструктор
-     *
-     * @param Secury $secury
-     */
-    public function __construct($secury)
-    {
-        $this->secury = $secury;
-    }
-
-    /**
-     * @param string $key
-     *
-     * @return bool
-     */
-    public function isRequest($key)
-    {
-        return $this->isPost($key) || $this->isGet($key);
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function request($key, $default = null)
-    {
-        if (($result = $this->post($key)) === null
-            && ($result = $this->get($key)) === null
-        ) {
-            return $default;
-        }
-        return $result;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function requestStr($key, $default = null)
-    {
-        if (($result = $this->postStr($key)) === null
-            && ($result = $this->getStr($key)) === null
-        ) {
-            return $default;
-        }
-        return $result;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function requestInt($key, $default = null)
-    {
-        if (($result = $this->postInt($key)) === null
-            && ($result = $this->getInt($key)) === null
-        ) {
-            return $default;
-        }
-        return $result;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function requestBool($key, $default = null)
-    {
-        if (($result = $this->postBool($key)) === null
-            && ($result = $this->getBool($key)) === null
-        ) {
-            return $default;
-        }
-        return $result;
-    }
-
-    /**
-     * @param string $key
-     *
-     * @return bool
-     */
-    public function isPost($key)
-    {
-        return isset($_POST[$key]);
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function post($key, $default = null)
-    {
-        if (isset($_POST[$key])) {
-            return $this->secury->replInvalidChars($_POST[$key]);
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function postStr($key, $default = null)
-    {
-        if (isset($_POST[$key]) && is_string($_POST[$key])) {
-            return (string) $this->secury->replInvalidChars($_POST[$key]);
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function postInt($key, $default = null)
-    {
-        if (isset($_POST[$key]) && is_numeric($_POST[$key]) && is_int(0 + $_POST[$key])) {
-            return (int) $_POST[$key];
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function postBool($key, $default = null)
-    {
-        if (isset($_POST[$key]) && is_string($_POST[$key])) {
-            return (bool) $_POST[$key];
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function postKey($key, $default = null)
-    {
-        if (isset($_POST[$key]) && is_array($_POST[$key])) {
-            $k = key($_POST[$key]);
-            if (null !== $k) {
-                return is_int($k) ? (int) $k : (string) $this->secury->replInvalidChars($k);
-            }
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     *
-     * @return bool
-     */
-    public function isGet($key)
-    {
-        return isset($_GET[$key]);
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function get($key, $default = null)
-    {
-        if (isset($_GET[$key])) {
-            return $this->secury->replInvalidChars($_GET[$key]);
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function getStr($key, $default = null)
-    {
-        if (isset($_GET[$key]) && is_string($_GET[$key])) {
-            return (string) $this->secury->replInvalidChars($_GET[$key]);
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function getInt($key, $default = null)
-    {
-        if (isset($_GET[$key]) && is_numeric($_GET[$key]) && is_int(0 + $_GET[$key])) {
-            return (int) $_GET[$key];
-        }
-        return $default;
-    }
-
-    /**
-     * @param string $key
-     * @param mixed $default
-     *
-     * @return mixed
-     */
-    public function getBool($key, $default = null)
-    {
-        if (isset($_GET[$key]) && is_string($_GET[$key])) {
-            return (bool) $_GET[$key];
-        }
-        return $default;
-    }
-}

+ 10 - 0
app/Core/Router.php

@@ -59,6 +59,7 @@ class Router
 
     /**
      * Конструктор
+     *
      * @param string $base
      */
     public function __construct($base)
@@ -71,9 +72,11 @@ class Router
 
     /**
      * Проверка url на принадлежность форуму
+     *
      * @param mixed $url
      * @param string $defMarker
      * @param array $defArgs
+     *
      * @return string
      */
     public function validate($url, $defMarker, array $defArgs = [])
@@ -91,8 +94,10 @@ class Router
 
     /**
      * Возвращает ссылку на основании маркера
+     *
      * @param string $marker
      * @param array $args
+     *
      * @return string
      */
     public function link($marker = null, array $args = [])
@@ -127,8 +132,10 @@ class Router
 
     /**
      * Метод определяет маршрут
+     *
      * @param string $method
      * @param string $uri
+     *
      * @return array
      */
     public function route($method, $uri)
@@ -195,6 +202,7 @@ class Router
 
     /**
      * Метод добавдяет маршрут
+     *
      * @param string|array $method
      * @param string $route
      * @param string $handler
@@ -244,7 +252,9 @@ class Router
 
     /**
      * Метод разбирает динамический маршрут
+     *
      * @param string $route
+     *
      * @return array|false
      */
     protected function parse($route)

+ 20 - 4
app/Core/Secury.php

@@ -16,9 +16,11 @@ class Secury
 
     /**
      * Конструктор
+     *
      * @param array $hmac
-     * @throws \InvalidArgumentException
-     * @throws \UnexpectedValueException
+     *
+     * @throws InvalidArgumentException
+     * @throws UnexpectedValueException
      */
     public function __construct(array $hmac)
     {
@@ -33,7 +35,9 @@ class Secury
 
     /**
      * Обертка для hash_hmac
+     *
      * @param string $data
+     *
      * @return string
      */
     public function hash($data)
@@ -43,10 +47,13 @@ class Secury
 
     /**
      * Обертка для hash_hmac
+     *
      * @param string $data
      * @param string $key
+     *
+     * @throws InvalidArgumentException
+     *
      * @return string
-     * @throws \InvalidArgumentException
      */
     public function hmac($data, $key)
     {
@@ -58,9 +65,12 @@ class Secury
 
     /**
      * Возвращает случайный набор байтов заданной длины
+     *
      * @param int $len
+     *
+     * @throws RuntimeException
+     *
      * @return string
-     * @throws \RuntimeException
      */
     public function randomKey($len)
     {
@@ -85,7 +95,9 @@ class Secury
 
     /**
      * Возвращает случайную строку заданной длины состоящую из символов 0-9 и a-f
+     *
      * @param int $len
+     *
      * @return string
      */
     public function randomHash($len)
@@ -96,7 +108,9 @@ class Secury
     /**
      * Возвращает случайную строку заданной длины состоящую из цифр, латиницы,
      * знака минус и символа подчеркивания
+     *
      * @param int $len
+     *
      * @return string
      */
     public function randomPass($len)
@@ -112,7 +126,9 @@ class Secury
 
     /**
      * Replacing invalid UTF-8 characters and remove control characters
+     *
      * @param string|array $data
+     *
      * @return string|array
      */
     public function replInvalidChars($data)

+ 34 - 6
app/Core/Validator.php

@@ -75,6 +75,7 @@ class Validator
 
     /**
      * Конструктор
+     *
      * @param Container $container
      */
     public function __construct(Container $container)
@@ -104,8 +105,10 @@ class Validator
 
     /**
      * Добавление новых валидаторов
+     *
      * @param array $validators
-     * @param Validator
+     *
+     * @return Validator
      */
     public function addValidators(array $validators)
     {
@@ -115,9 +118,12 @@ class Validator
 
     /**
      * Установка правил проверки
+     *
      * @param array $list
-     * @return Validator
+     *
      * @throws RuntimeException
+     *
+     * @return Validator
      */
     public function setRules(array $list)
     {
@@ -149,7 +155,9 @@ class Validator
 
     /**
      * Установка дополнительных аргументов для конкретных "имя поля"."имя правила".
+     *
      * @param array $arguments
+     *
      * @return Validator
      */
     public function setArguments(array $arguments)
@@ -160,7 +168,9 @@ class Validator
 
     /**
      * Установка сообщений для конкретных "имя поля"."имя правила".
+     *
      * @param array $messages
+     *
      * @return Validator
      */
     public function setMessages(array $messages)
@@ -171,7 +181,9 @@ class Validator
 
     /**
      * Установка псевдонимов имен полей для сообщений об ошибках
+     *
      * @param array $aliases
+     *
      * @return Validator
      */
     public function setAliases(array $aliases)
@@ -183,9 +195,12 @@ class Validator
     /**
      * Проверка данных
      * Удачная проверка возвращает true
+     *
      * @param array $raw
+     *
+     * @throws RuntimeException
+     *
      * @return bool
-     * @throws \RuntimeException
      */
     public function validation(array $raw)
     {
@@ -204,7 +219,9 @@ class Validator
 
     /**
      * Проверяет наличие поля
+     *
      * @param string $field
+     *
      * @return bool
      */
     public function __isset($field)
@@ -215,9 +232,12 @@ class Validator
     /**
      * Проверяет поле согласно заданным правилам
      * Возвращает значение запрашиваемого поля
+     *
      * @param string
+     *
+     * @throws RuntimeException
+     *
      * @return mixed
-     * @throws \RuntimeException
      */
     public function __get($field)
     {
@@ -259,8 +279,10 @@ class Validator
 
     /**
      * Получение дополнительных аргументов
+     *
      * @param string $field
-     * @param string $field
+     * @param string $rule
+     *
      * @return mixed
      */
     protected function getArguments($field, $rule)
@@ -276,6 +298,7 @@ class Validator
 
     /**
      * Обработка ошибки
+     *
      * @param mixed $error
      * @param string $field
      * @param string $rule
@@ -302,7 +325,9 @@ class Validator
 
     /**
      * Возвращает статус проверки поля
+     *
      * @param string $field
+     *
      * @return bool
      */
     public function getStatus($field)
@@ -316,8 +341,10 @@ class Validator
     /**
      * Возвращает проверенные данные
      * Поля с ошибками содержат значения по умолчанию или значения с ошибками
+     *
+     * @throws RuntimeException
+     *
      * @return array
-     * @throws \RuntimeException
      */
     public function getData()
     {
@@ -329,6 +356,7 @@ class Validator
 
     /**
      * Возращает массив ошибок
+     *
      * @return array
      */
     public function getErrors()

+ 29 - 12
app/Core/View.php

@@ -3,7 +3,7 @@
 namespace ForkBB\Core;
 
 use R2\Templating\Dirk;
-use ForkBB\Models\Pages\Page;
+use ForkBB\Models\Page;
 use RuntimeException;
 
 class View extends Dirk
@@ -24,7 +24,9 @@ class View extends Dirk
 
     /**
      * Трансформация скомпилированного шаблона
+     *
      * @param string $value
+     *
      * @return string
      */
     protected function compileTransformations($value)
@@ -41,23 +43,38 @@ class View extends Dirk
         );
     }
 
-    public function rendering(Page $page)
+    /**
+     * Return result of templating
+     *
+     * @param Page $p
+     *
+     * @return null|string
+     */
+    public function rendering(Page $p)
     {
-        if (! $page->isReady()) {
-            throw new RuntimeException('The page model does not contain data ready');
-        }
-
-        $headers = $page->httpHeaders();
-        foreach ($headers as $header) {
+        foreach ($p->httpHeaders as $header) {
             header($header);
         }
 
-        $tpl = $page->getNameTpl();
-        // переадресация
-        if (null === $tpl) {
+        if (null === $p->nameTpl) {
             return null;
         }
 
-        return $this->fetch($tpl, $page->getData());
+        $p->prepare();
+
+        $this->templates[] = $p->nameTpl;
+        while ($_name = array_shift($this->templates)) {
+            $this->beginBlock('content');
+            foreach ($this->composers as $_cname => $_cdata) {
+                if (preg_match($_cname, $_name)) {
+                    foreach ($_cdata as $_citem) {
+                        extract((is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
+                    }
+                }
+            }
+            require($this->prepare($_name));
+            $this->endBlock(true);
+        }
+        return $this->block('content');
     }
 }

+ 10 - 2
app/Models/Actions/CacheGenerator.php

@@ -86,20 +86,28 @@ class CacheGenerator
         return $this->c->DB->query('SELECT text, image FROM ::smilies ORDER BY disp_position')->fetchAll(\PDO::FETCH_KEY_PAIR); //???? text уникальное?
     }
 
+    /**
+     * Возвращает массив карты базы данных
+     */
+    public function dbMap()
+    {
+        return $this->c->DB->getMap();
+    }
+
     /**
      * Возвращает массив с описанием форумов для текущего пользователя
      * @return array
      */
     public function forums(User $user)
     {
-        $stmt = $this->c->DB->query('SELECT g_read_board FROM ::groups WHERE g_id=?i:id', [':id' => $user->gId]);
+        $stmt = $this->c->DB->query('SELECT g_read_board FROM ::groups WHERE g_id=?i:id', [':id' => $user->group_id]);
         $read = $stmt->fetchColumn();
         $stmt->closeCursor();
 
         $tree = $desc = $asc = [];
 
         if ($read) {
-            $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, fp.post_topics, fp.post_replies 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:gid) WHERE fp.read_forum IS NULL OR fp.read_forum=1 ORDER BY c.disp_position, c.id, f.disp_position', [':gid' => $user->groupId]);
+            $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, fp.post_topics, fp.post_replies 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:gid) WHERE fp.read_forum IS NULL OR fp.read_forum=1 ORDER BY c.disp_position, c.id, f.disp_position', [':gid' => $user->group_id]);
             while ($f = $stmt->fetch()) {
                 $tree[$f['parent_forum_id']][$f['fid']] = $f;
             }

+ 1 - 1
app/Models/Actions/CacheLoader.php

@@ -61,7 +61,7 @@ class CacheLoader
     public function loadForums()
     {
         $mark = $this->cache->get('forums_mark');
-        $key = 'forums_' . $this->c->user->gId;
+        $key = 'forums_' . $this->c->user->group_id;
 
         if (empty($mark)) {
             $this->cache->set('forums_mark', time());

+ 0 - 111
app/Models/Actions/CheckBans.php

@@ -1,111 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Actions;
-
-use ForkBB\Core\Container;
-
-class CheckBans
-{
-    /**
-     * Контейнер
-     * @var Container
-     */
-    protected $c;
-
-    /**
-     * Содержит массив с описание бана для проверяемого юзера
-     */
-    protected $ban;
-
-    /**
-     * Конструктор
-     * @param Container $container
-     */
-    public function __construct(Container $container)
-    {
-        $this->c = $container;
-    }
-
-    /**
-     * Возвращает массив с описанием бана или null
-     * @return null|array
-     */
-    public function check()
-    {
-        $user = $this->c->user;
-
-        if ($user->isAdmin) {
-            return null;
-        } elseif ($user->isGuest) {
-            $banned = $this->isBanned(null, null, $user->ip);
-        } else {
-            $banned = $this->isBanned($user->username, $user->email, $user->ip);
-        }
-
-        if ($banned) {
-            $this->c->Online->delete($user); //???? а зачем это надо?
-            return $this->ban;
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Проверяет наличие бана на основании имени юзера, email и(или) ip
-     * Удаляет просроченные баны
-     * @param string $username
-     * @param string $email
-     * @param string $userIp
-     * @return int
-     */
-    public function isBanned($username = null, $email = null, $userIp = null)
-    {
-        $bans = $this->c->bans;
-        if (empty($bans)) {
-            return 0;
-        }
-        if (isset($username)) {
-            $username = mb_strtolower($username, 'UTF-8');
-        }
-        if (isset($userIp)) {
-            // Add a dot or a colon (depending on IPv4/IPv6) at the end of the IP address to prevent banned address
-            // 192.168.0.5 from matching e.g. 192.168.0.50
-            $add = strpos($userIp, '.') !== false ? '.' : ':';
-            $userIp .= $add;
-        }
-
-        $banned = 0;
-        $remove = [];
-        $now = time();
-
-        foreach ($bans as $cur) {
-            if ($cur['expire'] != '' && $cur['expire'] < $now) {
-                $remove[] = $cur['id'];
-                continue;
-            } elseif (isset($username) && $cur['username'] != '' && $username == mb_strtolower($cur['username'])) {
-                $this->ban = $cur;
-                $banned = 1;
-                break;
-            } elseif (isset($email) && $cur['email'] != '' && $email == $cur['email']) {
-                $this->ban = $cur;
-                $banned = 2;
-                break;
-            } elseif (isset($userIp) && $cur['ip'] != '') {
-                foreach (explode(' ', $cur['ip']) as $ip) {
-                    $ip .= $add;
-                    if (substr($userIp, 0, strlen($ip)) == $ip) {
-                        $this->ban = $cur;
-                        $banned = 3;
-                        break 2;
-                    }
-                }
-            }
-        }
-        if (! empty($remove))
-        {
-            $this->c->DB->exec('DELETE FROM ::bans WHERE id IN (?ai:remove)', [':remove' => $remove]);
-            $this->c->{'bans update'};
-        }
-        return $banned;
-    }
-}

+ 23 - 0
app/Models/AdminList.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+
+class AdminList extends Model
+{
+    /**
+     * Загружает список id админов из кеша/БД
+     *
+     * @return AdminList
+     */
+    public function init()
+    {
+        if ($this->c->Cache->has('admins')) {
+            $this->list  = $this->c->Cache->get('admins');
+        } else {
+            $this->load();
+        }
+        return $this;
+    }
+}

+ 22 - 0
app/Models/AdminList/Load.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace ForkBB\Models\AdminList;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * Заполняет модель данными из БД
+     * Создает кеш
+     *
+     * @return AdminList
+     */
+    public function load()
+    {
+        $list = $this->c->DB->query('SELECT id FROM ::users WHERE group_id=?i', [$this->c->GROUP_ADMIN])->fetchAll(\PDO::FETCH_COLUMN);
+        $this->model->list = $list;
+        $this->c->Cache->set('admins', $list);
+        return $this->model;
+    }
+}

+ 46 - 0
app/Models/BanList.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+
+class BanList extends Model
+{
+    /**
+     * Загружает список банов из кеша/БД
+     *
+     * @return BanList
+     */
+    public function init()
+    {
+        if ($this->c->Cache->has('banlist')) {
+            $list = $this->c->Cache->get('banlist');
+            $this->otherList = $list['otherList'];
+            $this->userList  = $list['userList'];
+            $this->ipList    = $list['ipList'];
+        } else {
+            $this->load();
+        }
+        return $this;
+    }
+
+    /**
+     * Фильтрует значение
+     *
+     * @param mixed $val
+     * @param bool $toLower
+     *
+     * @return null|string
+     */
+    public function trimToNull($val, $toLower = false)
+    {
+        $val = trim($val);
+        if ($val == '') {
+            return null;
+        } elseif ($toLower) {
+            return mb_strtolower($val, 'UTF-8');
+        } else {
+            return $val;
+        }
+    }
+}

+ 67 - 0
app/Models/BanList/Check.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace ForkBB\Models\BanList;
+
+use ForkBB\Models\MethodModel;
+use ForkBB\Models\User;
+
+class Check extends MethodModel
+{
+    /**
+     * Проверяет наличие бана (для текущего пользователя) на основании имени пользователя/ip
+     * Удаляет просроченные баны
+     *
+     * @param User $user
+     *
+     * @return bool
+     */
+    public function check(User $user)
+    {
+        // админ
+        if ($user->isAdmin) {
+            return false;
+        }
+
+        // удаление просроченных банов
+        $now = time();
+        $ids = [];
+        foreach ($this->model->otherList as $id => $row) {
+            if (null !== $row['expire'] && $row['expire'] < $now) {
+                $ids[] = $id;
+            }
+        }
+        if (! empty($ids)) {
+            $this->model->delete($ids);
+        }
+
+        // проверка гостя
+        if ($user->isGuest) {
+            $ip = $this->model->trimToNull($user->ip);
+            if (null === $ip) {
+                return false; //????
+            }
+            $add = strpos($ip, ':') === false ? '.' : ':'; //????
+            $ip .= $add;
+            foreach ($this->model->ipList as $addr => $id) {
+                $addr .= $add;
+                if (substr($ip, 0, strlen($addr)) == $addr) {
+                    if (isset($this->model->otherList[$id])) {
+                        $user->__banInfo = $this->model->otherList[$id];
+                    }
+                    return true;
+                }
+            }
+        // проверка пользователя
+        } else {
+            $name = $this->model->trimToNull($user->username, true);
+            if (isset($this->model->userList[$name])) {
+                $id = $this->model->userList[$name];
+                if (isset($this->model->otherList[$id])) {
+                    $user->__banInfo = $this->model->otherList[$id];
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 25 - 0
app/Models/BanList/Delete.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace ForkBB\Models\BanList;
+
+use ForkBB\Models\MethodModel;
+
+class Delete extends MethodModel
+{
+    /**
+     * Удаляет из банов записи по списку номеров
+     * Обновляет кеш
+     *
+     * @param array $ids
+     *
+     * @return BanList
+     */
+    public function delete(array $ids)
+    {
+        if (! empty($ids)) {
+            $this->c->DB->exec('DELETE FROM ::bans WHERE id IN (?ai:ids)', [':ids' => $ids]);
+            $this->model->load();
+        }
+        return $this->model;
+    }
+}

+ 35 - 0
app/Models/BanList/IsBanned.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace ForkBB\Models\BanList;
+
+use ForkBB\Models\MethodModel;
+use ForkBB\Models\User;
+
+class IsBanned extends MethodModel
+{
+    /**
+     * Проверяет наличие бана на основании имени пользователя и(или) email
+     *
+     * @param User $user
+     *
+     * @return int
+     */
+    public function isBanned(User $user)
+    {
+        $name  = $this->trimToNull($this->model->username, true);
+        if (null !== $name && isset($this->model->userList[$name])) {
+            return 1;
+        }
+        $email = $this->trimToNull($this->model->email);
+        if (null !== $email) {
+            foreach ($this->model->otherList as $row) {
+                if (null === $row['email']) {
+                    continue;
+                } elseif ($email == $row['email']) {
+                    return 2;
+                }
+            }
+        }
+        return 0;
+    }
+}

+ 60 - 0
app/Models/BanList/Load.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace ForkBB\Models\BanList;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * Загружает список банов из БД
+     * Создает кеш
+     *
+     * @return BanList
+     */
+    public function load()
+    {
+        $userList  = [];
+        $ipList    = [];
+        $otherList = [];
+        $stmt = $this->c->DB->query('SELECT id, username, ip, email, message, expire FROM ::bans');
+        while ($row = $stmt->fetch()) {
+            $name = $this->model->trimToNull($row['username'], true);
+            if (null !== $name) {
+                $userList[$name] = $row['id'];
+            }
+
+            $ips = $this->model->trimToNull($row['ip']);
+            if (null !== $ips) {
+                foreach (explode(' ', $ips) as $ip) {
+                    $ip = trim($ip);
+                    if ($ip != '') {
+                        $ipList[$ip] = $row['id'];
+                    }
+                }
+            }
+
+            $email   = $this->model->trimToNull($row['email']);
+            $message = $this->model->trimToNull($row['message']);
+            $expire  = empty($row['expire']) ? null : $row['expire'];
+            if (! isset($email) && ! isset($message) && ! isset($expire)) {
+                continue;
+            }
+
+            $otherList[$row['id']] = [
+                'email'    => $email,
+                'message'  => $message,
+                'expire'   => $expire,
+            ];
+        }
+        $this->model->otherList = $otherList;
+        $this->model->userList  = $userList;
+        $this->model->ipList    = $ipList;
+        $this->c->Cache->set('banlist', [
+            'otherList' => $otherList,
+            'userList'  => $userList,
+            'ipList'    => $ipList,
+        ]);
+        return $this->model;
+    }
+}

+ 41 - 0
app/Models/CensorshipList.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+
+class CensorshipList extends Model
+{
+    /**
+     * Загружает список цензуры из кеша/БД
+     * 
+     * @return BanList
+     */
+    public function init()
+    {
+        if ($this->c->Cache->has('censorship')) {
+            $list = $this->c->Cache->get('censorship');
+            $this->searchList   = $list['searchList'];
+            $this->replaceList  = $list['replaceList'];
+        } else {
+            $this->load();
+        }
+        return $this;
+    }
+
+    /**
+     * Выполняет цензуру при необходимости
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function censor($str)
+    {
+        if ($this->c->config->o_censoring == '1') {
+            return (string) preg_replace($this->searchList, $this->replaceList,  $str);
+        } else {
+            return $str;
+        }
+    }
+}

+ 33 - 0
app/Models/CensorshipList/Load.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace ForkBB\Models\CensorshipList;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * Заполняет модель данными из БД
+     * Создает кеш
+     *
+     * @return CensorshipList
+     */
+    public function load()
+    {
+
+        $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->model->searchList   = $search;
+        $this->model->replaceList  = $replace;
+        $this->c->Cache->set('censorship', [
+            'searchList'  => $search,
+            'replaceList' => $replace,
+        ]);
+        return $this->model;
+    }
+}

+ 23 - 0
app/Models/Config.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\DataModel;
+
+class Config extends DataModel
+{
+    /**
+     * Заполняет модель данными из кеша/БД
+     *
+     * @return Config
+     */
+    public function init()
+    {
+        if ($this->c->Cache->has('config')) {
+            $this->setAttrs($this->c->Cache->get('config'));
+        } else {
+            $this->load();
+        }
+        return $this;
+    }
+}

+ 22 - 0
app/Models/Config/Load.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace ForkBB\Models\Config;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * Заполняет модель данными из БД
+     * Создает кеш
+     *
+     * @return Config
+     */
+    public function load()
+    {
+        $config = $this->c->DB->query('SELECT conf_name, conf_value FROM ::config')->fetchAll(\PDO::FETCH_KEY_PAIR);
+        $this->model->setAttrs($config);
+        $this->c->Cache->set('config', $config);
+        return $this->model;
+    }
+}

+ 37 - 0
app/Models/Config/Save.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace ForkBB\Models\Config;
+
+use ForkBB\Models\MethodModel;
+
+class Save extends MethodModel
+{
+    /**
+     * Сохраняет изменения модели в БД
+     * Удаляет кеш
+     *
+     * @return Config
+     */
+    public function save()
+    {
+        $modified = $this->model->getModified();
+        if (empty($modified)) {
+            return;
+        }
+
+        $values = $this->model->getAttrs();
+        foreach ($modified as $name) {
+            $vars = [
+                ':value' => $values[$name],
+                ':name'  => $name
+            ];
+            //????
+            $count = $this->c->DB->exec('UPDATE ::config SET conf_value=?s:value WHERE conf_name=?s:name', $vars);
+            if ($count === 0) {
+                $this->c->DB->exec('INSERT INTO ::config (conf_name, conf_value) VALUES (?s:name, ?s:value)', $vars);
+            }
+        }
+        $this->c->Cache->delete('config');
+        return $this->model;
+    }
+}

+ 212 - 0
app/Models/Cookie.php

@@ -0,0 +1,212 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+use ForkBB\Models\User;
+use ForkBB\Core\Container;
+use RuntimeException;
+
+class Cookie extends Model
+{
+    const NAME = 'user';
+
+    /**
+     * Флаг запрета записи свойств
+     * @var bool
+     */
+    protected $noSet = false;
+
+    /**
+     * Конструктор
+     *
+     * @param array $options
+     * @param Container $container
+     */
+    public function __construct(array $options, Container $container)
+    {
+        parent::__construct($container);
+        $this->a = $options + [
+            'prefix' => '',
+            'domain' => '',
+            'path'   => '',
+            'secure' => false,
+            'time'   => 31536000,
+            'key1'   => 'key1',
+            'key2'   => 'key2',
+        ];
+        $this->init();
+        $this->noSet = true;
+    }
+
+    /**
+     * Устанавливает куку
+     *
+     * @param string $name
+     * @param string $value
+     * @param int $expire
+     * @param string $path
+     * @param string $domain
+     * @param bool $secure
+     * @param bool $httponly
+     *
+     * @return bool
+     */
+    public function set($name, $value, $expire = 0, $path = null, $domain = null, $secure = false, $httponly = true)
+    {
+        $result = setcookie(
+            $this->prefix . $name,
+            $value,
+            $expire,
+            $path ?: $this->path,
+            $domain ?: $this->domain,
+            (bool) $this->secure || (bool) $secure,
+            (bool) $httponly
+        );
+        if ($result) {
+            $_COOKIE[$this->prefix . $name] = $value;
+        }
+        return $result;
+    }
+
+    /**
+     * Получает значение куки
+     *
+     * @param string $name
+     * @param mixed $default
+     *
+     * @return mixed
+     */
+    public function get($name, $default = null)
+    {
+        $name = $this->prefix . $name;
+        return isset($_COOKIE[$name]) ? $this->c->Secury->replInvalidChars($_COOKIE[$name]) : $default;
+    }
+
+    /**
+     * Удаляет куку
+     *
+     * @param string $name
+     * @param string $path
+     * @param string $domain
+     *
+     * @return bool
+     */
+    public function delete($name, $path = null, $domain = null)
+    {
+        $result = $this->set($name, '', 1, $path, $domain);
+        if ($result) {
+            unset($_COOKIE[$this->prefix . $name]);
+        }
+        return $result;
+    }
+
+    /**
+     * Выделение данных из куки аутентификации
+     */
+    protected function init()
+    {
+        $ckUser = $this->get(self::NAME);
+
+        if (null === $ckUser
+            || ! preg_match('%^(\-)?(\d{1,10})_(\d{10})_([a-f\d]{32,})_([a-f\d]{32,})$%Di', $ckUser, $ms)
+        ) {
+            return;
+        }
+
+        if (2 > $ms[2]
+            || time() > $ms[3]
+            || ! hash_equals(
+                     $this->c->Secury->hmac($ms[1] . $ms[2] . $ms[3] . $ms[4], $this->key1),
+                     $ms[5]
+                 )
+        ) {
+            return;
+        }
+
+        $this->uRemember = empty($ms[1]);
+        $this->uId       = (int) $ms[2];
+        $this->uExpire   = (int) $ms[3];
+        $this->uHash     = $ms[4];
+    }
+
+    /**
+     * Проверка хэша пароля
+     *
+     * @param User $user
+     *
+     * @return bool
+     */
+    public function verifyUser(User $user)
+    {
+        return $this->uId === (int) $user->id
+            && hash_equals(
+                   (string) $this->uHash,
+                   $this->c->Secury->hmac($user->password . $this->uExpire, $this->key2)
+               );
+    }
+
+    /**
+     * Установка куки аутентификации юзера
+     *
+     * @param User $user
+     * @param bool $remember
+     *
+     * @return bool
+     */
+    public function setUser(User $user, $remember = null)
+    {
+        if ($user->isGuest) {
+            return $this->deleteUser();
+        }
+
+        if ($remember
+            || (null === $remember
+                && $this->uId === (int) $user->id
+                && $this->uRemember
+            )
+        ) {
+            $expTime = time() + $this->time;
+            $expire = $expTime;
+            $pfx = '';
+        } else {
+            $expTime = time() + $this->c->config->o_timeout_visit;
+            $expire = 0;
+            $pfx = '-';
+        }
+        $passHash = $this->c->Secury->hmac($user->password . $expTime, $this->key2);
+        $ckHash = $this->c->Secury->hmac($pfx . $user->id . $expTime . $passHash, $this->key1);
+
+        return $this->set(self::NAME, $pfx . $user->id . '_' . $expTime . '_' . $passHash . '_' . $ckHash, $expire);
+    }
+
+    /**
+     * Удаление куки аутентификации юзера
+     *
+     * @return bool
+     */
+    public function deleteUser()
+    {
+        if (null === $this->get(self::NAME)) {
+            return true;
+        } else {
+            return $this->delete(self::NAME);
+        }
+    }
+
+    /**
+     * Устанавливает значение для свойства
+     *
+     * @param string $name
+     * @param mixed $val
+     *
+     * @throws RuntimeException
+     */
+    public function __set($name, $val)
+    {
+        if ($this->noSet) {
+            throw new RuntimeException('Model attributes in read-only mode');
+        }
+        parent::__set($name, $val);
+    }
+}

+ 25 - 0
app/Models/DBMap.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+
+class DBMap extends Model
+{
+    /**
+     * Загружает карту БД из кеша/БД
+     *
+     * @return DBMap
+     */
+    public function init()
+    {
+        if ($this->c->Cache->has('db_map')) {
+            $this->a = $this->c->Cache->get('db_map');
+        } else {
+            $map = $this->c->DB->getMap();
+            $this->c->Cache->set('db_map', $map);
+            $this->a = $map;
+        }
+        return $this;
+    }
+}

+ 91 - 0
app/Models/DataModel.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Core\Container;
+use ForkBB\Models\Model;
+use InvalidArgumentException;
+use RuntimeException;
+
+abstract class DataModel extends Model
+{
+    /**
+     * Массив флагов измененных свойств модели
+     * @var array
+     */
+    protected $modified = [];
+
+    /**
+     * Конструктор
+     *
+     * @param array $attrs
+     * @param Container $container
+     */
+    public function __construct(array $attrs, Container $container)
+    {
+        parent::__construct($container);
+        $this->a = $attrs;
+    }
+
+    /**
+     * Устанавливает значения для свойств
+     *
+     * @param array $attrs
+     *
+     * @return DataModel
+     */
+    public function setAttrs(array $attrs)
+    {
+        $this->a = $attrs; //????
+        $this->modified = [];
+        return $this;
+    }
+
+    /**
+     * Возвращает значения свойств в массиве
+     *
+     * @return array
+     */
+    public function getAttrs()
+    {
+        return $this->a; //????
+    }
+
+    /**
+     * Возвращает массив имен измененных свойств модели
+     *
+     * @return array
+     */
+    public function getModified()
+    {
+        return array_keys($this->modified);
+    }
+
+    /**
+     * Обнуляет массив флагов измененных свойств модели
+     */
+    public function resModified()
+    {
+        $this->modified = [];
+    }
+
+    /**
+     * Устанавливает значение для свойства
+     *
+     * @param string $name
+     * @param mixed $val
+     */
+    public function __set($name, $val)
+    {
+        // запись свойства без отслеживания изменений
+        if (strpos($name, '__') === 0) {
+            return parent::__set(substr($name, 2), $val);
+        }
+
+        $old = isset($this->a[$name]) ? $this->a[$name] : null;
+        parent::__set($name, $val);
+        if ($old !== $this->a[$name]) {
+            $this->modified[$name] = true;
+        }
+    }
+}

+ 54 - 0
app/Models/Forum.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\DataModel;
+use ForkBB\Core\Container;
+
+class Forum extends DataModel
+{
+    /**
+     * @param array $attrs
+     * 
+     * @return Forum
+     */
+    public function replAtttrs(array $attrs)
+    {
+        foreach ($attrs as $key => $val) {
+            $this->{'__' . $key} = $val; //????
+        }
+        $modified = array_diff(array_keys($this->modified), array_keys($attrs));
+        $this->modified = [];
+        foreach ($modified as $key) {
+            $this->modified[$key] = true;
+        }
+        return $this;
+    }
+
+    protected function getSubforums()
+    {
+        $sub = [];
+        if (! empty($this->a['subforums'])) {
+            foreach ($this->a['subforums'] as $id) {
+                $sub[$id] = $this->c->forums->forum($id);
+            }
+        }
+        return $sub;
+    }
+
+    protected function getDescendants()
+    {
+        $all = [];
+        if (! empty($this->a['descendants'])) {
+            foreach ($this->a['descendants'] as $id) {
+                $all[$id] = $this->c->forums->forum($id);
+            }
+        }
+        return $all;
+    }
+
+    protected function getParent()
+    {
+        return $this->c->forums->forum($this->parent_forum_id);
+    }
+}

+ 62 - 0
app/Models/ForumList.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+use RuntimeException;
+
+class ForumList extends Model
+{
+    /**
+     * Загружает список доступных разделов для текущего пользователя из кеша/БД
+     *
+     * @return ForumList
+     */
+    public function init()
+    {
+        $mark = $this->c->Cache->get('forums_mark');
+        if (empty($mark)) {
+            $this->c->Cache->set('forums_mark', time());
+            return $this->load();
+        }
+
+        $result = $this->c->Cache->get('forums_' . $this->c->user->group_id);
+        if (empty($result['time']) || $result['time'] < $mark) {
+            return $this->load();
+        }
+
+        $this->list = $result['list']; //????
+        return $this;
+    }
+
+    /**
+     * Проверяет доступность раздела
+     * 
+     * @param int $id
+     * 
+     * @return bool
+     */
+    public function isAvailable($id)
+    {
+        return isset($this->list[$id]); //????
+    }
+
+    /**
+     * 
+     * @param int $id
+     * 
+     * @return null|Forum
+     */
+    public function forum($id)
+    {
+        if (isset($this->forums[$id])) {
+            return $this->forums[$id];
+        } elseif ($this->isAvailable($id)) {
+            $forum = $this->c->ModelForum->setAttrs($this->list[$id]);
+            $this->a['forums'][$id] = $forum; //????
+            return $forum;
+        } else {
+            return null;
+        }
+    }
+}

+ 93 - 0
app/Models/ForumList/Load.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace ForkBB\Models\ForumList;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * @var array
+     */
+    protected $list = [];
+
+    /**
+     * Заполняет модель данными из БД для текущего пользователя
+     * Создает кеш
+     *
+     * @return ForumList
+     */
+    public function load()
+    {
+        $this->getList();
+        $this->model->list = $this->list; //????
+        $this->c->Cache->set('forums_' . $this->c->user->group_id, [
+            'time' => time(),
+            'list' => $this->list,
+        ]);
+        return $this->model;
+    }
+
+    /**
+     * Получает данные из БД
+     */
+    protected function getList()
+    {
+        if ($this->c->user->g_read_board != '1') {
+            return;
+        }
+        $list = [];
+        $vars = [':gid' => $this->c->user->group_id];
+        $sql = 'SELECT c.id AS cid, c.cat_name, f.id, f.forum_name, f.redirect_url, f.parent_forum_id,
+                       f.disp_position, fp.post_topics, fp.post_replies
+                FROM ::categories AS c
+                INNER JOIN ::forums AS f ON c.id=f.cat_id
+                LEFT JOIN ::forum_perms AS fp ON (fp.group_id=?i:gid AND fp.forum_id=f.id)
+                WHERE fp.read_forum IS NULL OR fp.read_forum=1
+                ORDER BY c.disp_position, c.id, f.disp_position';
+        $stmt = $this->c->DB->query($sql, $vars);
+        while ($row = $stmt->fetch()) {
+            $list[$row['id']] = $row;
+        }
+        if (empty($list)) {
+            return;
+        }
+        $this->createList($list);
+    }
+
+    /**
+     * Формирует список доступных разделов
+     *
+     * @param array $list
+     * @param int $parent
+     *
+     * @return array
+     */
+    protected function createList(array $list, $parent = 0)
+    {
+        $sub = [];
+        $all = [];
+        foreach ($list as $id => $f) {
+            if ($parent === $id || $parent !== $f['parent_forum_id']) {
+                continue;
+            }
+            $sub[] = $id;
+            $all   = array_merge($this->createList($list, $id), $all);
+        }
+        if ($parent === 0) {
+            if (empty($sub)) {
+                return [];
+            }
+            $list[0]['id']    = $parent;
+            $list[0]['ready'] = true;
+        }
+        $all = array_merge($sub, $all);
+        $list[$parent]['subforums']   = $sub ?: null;
+        $list[$parent]['descendants'] = $all ?: null;
+        
+        $this->list[$parent] = array_filter($list[$parent], function($val) {
+            return $val !== null;
+        });
+        return $all;
+    }
+}

+ 132 - 0
app/Models/ForumList/LoadTree.php

@@ -0,0 +1,132 @@
+<?php
+
+namespace ForkBB\Models\ForumList;
+
+use ForkBB\Models\MethodModel;
+
+class LoadTree extends MethodModel
+{
+    /**
+     * Загружает данные в модели разделов для указанного раздела и всех его потомков
+     * 
+     * @param int $rootId
+     * 
+     * @return null|Forum
+     */
+    public function loadTree($rootId)
+    {
+        $root = $this->model->forum($rootId);
+        if (null === $root) {
+            return null;
+        }
+
+        $list = [];
+        if (! $root->ready) {
+            $list[] = $rootId;
+        }
+        foreach ($root->descendants as $id => $descendant) {
+            if (! $descendant->ready) {
+                $list[] = $id;
+            }
+        }
+
+        $this->loadData($list);
+
+        if (! $this->c->user->isGuest) {
+            $this->checkForNew(array_keys($root->descendants)); //????
+        }
+
+        return $root;
+    }
+
+    /**
+     * Загружает данные из БД по списку id разделов
+     * 
+     * @param array $list
+     */
+    public function loadData(array $list)
+    {
+        if (empty($list)) {
+            return;
+        }
+
+        $vars = [
+            ':uid'    => $this->c->user->id,
+            ':forums' => $list,
+        ];
+
+        if ($this->c->user->isGuest) {
+            $sql = 'SELECT f.id, f.forum_desc, f.moderators, f.num_topics, f.sort_by,
+                           f.num_posts, f.last_post, f.last_post_id, f.last_poster, f.last_topic
+                    FROM ::forums AS f
+                    WHERE id IN (?ai:forums)';
+        } elseif ($this->c->config->o_forum_subscriptions == '1') {
+            $sql = 'SELECT f.id, f.forum_desc, f.moderators, f.num_topics, f.sort_by,
+                           f.num_posts, f.last_post, f.last_post_id, f.last_poster, f.last_topic,
+                           mof.mf_mark_all_read, s.user_id AS is_subscribed
+                    FROM ::forums AS f 
+                    LEFT JOIN ::forum_subscriptions AS s ON (s.user_id=?i:uid AND s.forum_id=f.id) 
+                    LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND mof.fid=f.id)
+                    WHERE f.id IN (?ai:forums)';
+        } else {
+            $sql = 'SELECT f.id, f.forum_desc, f.moderators, f.num_topics, f.sort_by,
+                           f.num_posts, f.last_post, f.last_post_id, f.last_poster, f.last_topic,
+                           mof.mf_mark_all_read 
+                    FROM ::forums AS f 
+                    LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:id AND mof.fid=f.id)
+                    WHERE f.id IN (?ai:forums)';
+        }
+
+        $stmt = $this->c->DB->query($sql, $vars);
+        while ($cur = $stmt->fetch()) {
+            $this->model->forum($cur['id'])->replAtttrs($cur)->ready = true;
+        }
+    }
+
+    /**
+     * Проверяет наличие новых сообщений в разделах по списку id
+     * 
+     * @param array $list
+     */
+    protected function checkForNew(array $list)
+    {
+        if (empty($list) || $this->c->user->isGuest) {
+            return;
+        }
+
+        // предварительная проверка разделов
+        $time = [];
+        $max = max((int) $this->c->user->last_visit, (int) $this->c->user->u_mark_all_read);
+        foreach ($list as $id) {
+            $forum = $this->model->forum($id);
+            $t = max($max, (int) $forum->mf_mark_all_read);
+            if ($forum->last_post > $t) {
+                $time[$id] = $t;
+            }
+        }
+
+        if (empty($time)) {
+            return;
+        }
+
+        // проверка по темам
+        $vars = [
+            ':uid'    => $this->c->user->id,
+            ':forums' => array_keys($time),
+            ':max'    => $max,
+        ];
+        $sql = 'SELECT t.forum_id, t.last_post 
+                FROM ::topics AS t 
+                LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid 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_last_visit IS NULL OR t.last_post>mot.mt_last_visit)';
+        $stmt = $this->c->DB->query($sql, $vars);
+        while ($cur = $stmt->fetch()) {
+            if ($cur['last_post'] > $time[$cur['forum_id']]) {
+                $this->model->forum($cur['forum_id'])->newMessages = true; //????
+            }
+        }
+    }
+}

+ 33 - 0
app/Models/MethodModel.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Core\Container;
+use ForkBB\Models\Model;
+
+abstract class MethodModel
+{
+    /**
+     * Контейнер
+     * @var Container
+     */
+    protected $c;
+
+    /**
+     * Модель
+     * @var Model
+     */
+    protected $model;
+
+    /**
+     * Конструктор
+     *
+     * @param Model $model
+     * @param Container $container
+     */
+    public function __construct(Model $model, Container $container)
+    {
+        $this->model = $model;
+        $this->c = $container;
+    }
+}

+ 115 - 0
app/Models/Model.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Core\Container;
+use InvalidArgumentException;
+use RuntimeException;
+
+abstract class Model
+{
+    /**
+     * Контейнер
+     * @var Container
+     */
+    protected $c;
+
+    /**
+     * Данные модели
+     * @var array
+     */
+    protected $a = [];
+
+    /**
+     * Конструктор
+     *
+     * @param Container $container
+     */
+    public function __construct(Container $container)
+    {
+        $this->c = $container;
+    }
+
+    /**
+     * Проверяет наличие свойства
+     *
+     * @param mixed $name
+     *
+     * @return bool
+     */
+    public function __isset($name)
+    {
+        return isset($this->a[$name]); //???? array_key_exists($name, $this->a)
+    }
+
+    /**
+     * Удаляет свойство
+     *
+     * @param mixed $name
+     */
+    public function __unset($name)
+    {
+        unset($this->a[$name]);
+    }
+
+    /**
+     * Устанавливает значение для свойства
+     *
+     * @param string $name
+     * @param mixed $val
+     */
+    public function __set($name, $val)
+    {
+        $method = 'set' . ucfirst($name);
+        if (method_exists($this, $method)) {
+            $this->$method($val);
+        } else {
+            $this->a[$name] = $val;
+        }
+    }
+
+    /**
+     * Возвращает значение свойства
+     *
+     * @param string $name
+     *
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        $method = 'get' . ucfirst($name);
+        if (method_exists($this, $method)) {
+            return $this->$method();
+        } else {
+            return isset($this->a[$name]) ? $this->a[$name] : null;
+        }
+    }
+
+    /**
+     * Выполняет подгружаемый метод при его наличии
+     *
+     * @param string $name
+     * @param array $args
+     *
+     * @throws RuntimeException
+     *
+     * @return mixed
+     */
+    public function __call($name, array $args)
+    {
+        $key = str_replace(['ForkBB\\', '\\'], '', get_class($this));
+
+        if (empty($this->c->METHODS[$key][$name])) {
+            throw new RuntimeException("The {$name} method was not found");
+        }
+
+        $link = explode(':', $this->c->METHODS[$key][$name], 2);
+        $factory = new $link[0]($this, $this->c);
+
+        if (isset($link[1])) {
+            return $factory->{$link[1]}(...$args);
+        } else {
+            return $factory->$name(...$args);
+        }
+    }
+}

+ 31 - 39
app/Models/Online.php

@@ -3,71 +3,60 @@
 namespace ForkBB\Models;
 
 use ForkBB\Core\Container;
+use ForkBB\Models\Model;
 use ForkBB\Models\User;
-use ForkBB\Models\Pages\Page;
+use ForkBB\Models\Page;
 
-class Online
+class Online extends Model
 {
-    /**
-     * Контейнер
-     * @var Container
-     */
-    protected $c;
-
-    /**
-     * Флаг выполнения
-     * @var bool
-     */
-    protected $done = false;
-
-    /**
-     * @var array
-     */
-    protected $config;
-
     /**
      * Конструктор
-     * @param array $config
+     *
      * @param Container $container
      */
     public function __construct(Container $container)
     {
-        $this->c = $container;
-        $this->config = $container->config;
+        parent::__construct($container);
+        $this->users  = [];
+        $this->guests = [];
+        $this->bots   = [];
     }
 
     /**
      * Обработка данных пользователей онлайн
      * Обновление данных текущего пользователя
      * Возврат данных по пользователям онлайн
+     *
      * @param Page $page
-     * @return array
      */
-    public function handle(Page $page)
+    public function calc(Page $page)
     {
         if ($this->done) {
-            return [[], [], []]; //????
+            return;
         }
         $this->done = true;
 
-        //  string|null  bool   bool
-        list($position, $type, $filter) = $page->getDataForOnline();  //???? возможно стоит возвращать полное имя страницы для отображение
+        $position = $page->onlinePos;
         if (null === $position) {
-            return [[], [], []]; //????
+            return;
         }
+        $type     = $page->onlineType;
+        $filter   = $page->onlineFilter;
 
         $this->updateUser($position);
 
-        $all = 0;
-        $now = time();
-        $tOnline = $now - $this->config['o_timeout_online'];
-        $tVisit = $now - $this->config['o_timeout_visit'];
-        $users = $guests = $bots = [];
+        $all     = 0;
+        $now     = time();
+        $tOnline = $now - $this->c->config->o_timeout_online;
+        $tVisit  = $now - $this->c->config->o_timeout_visit;
+        $users   = [];
+        $guests  = [];
+        $bots    = [];
         $deleteG = false;
         $deleteU = false;
         $setIdle = false;
 
-        if ($this->config['o_users_online'] == '1' && $type) {
+        if ($this->c->config->o_users_online == '1' && $type) {
             $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle, o_position, o_name FROM ::online ORDER BY logged');
         } elseif ($type) {
             $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle FROM ::online ORDER BY logged');
@@ -142,16 +131,19 @@ class Online
         }
 
         // обновление максимального значение пользоватеелй онлайн
-        if ($this->config['st_max_users'] < $all) {
-            $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'};
+        if ($this->c->config->st_max_users < $all) {
+            $this->c->config->st_max_users      = $all;
+            $this->c->config->st_max_users_time = $now;
+            $this->c->config->save();
         }
-        return [$users, $guests, $bots];
+        $this->users  = $users;
+        $this->guests = $guests;
+        $this->bots   = $bots;
     }
 
     /**
      * Обновление данных текущего пользователя
+     *
      * @param string $position
      */
     protected function updateUser($position)

+ 144 - 267
app/Models/Pages/Page.php → app/Models/Page.php

@@ -1,252 +1,58 @@
 <?php
 
-namespace ForkBB\Models\Pages;
+namespace ForkBB\Models;
 
 use ForkBB\Core\Container;
+use ForkBB\Models\Model;
 use RuntimeException;
 
-abstract class Page
+abstract class Page extends Model
 {
-    /**
-     * Контейнер
-     * @var Container
-     */
-    protected $c;
-
-    /**
-     * Конфигурация форума
-     * @var array
-     */
-    protected $config;
-
-    /**
-     * HTTP статус ответа для данной страницы
-     * @var int
-     */
-    protected $httpStatus = 200;
-
-    /**
-     * HTTP заголовки отличные от статуса
-     * @var array
-     */
-    protected $httpHeaders = [];
-
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl;
-
-    /**
-     * Указатель на активный пункт навигации
-     * @var string
-     */
-    protected $index = 'index';
-
-    /**
-     * Массив титула страницы
-     * @var array
-     */
-    protected $titles = [];
-
-    /**
-     * Подготовленные данные для шаблона
-     * @var array
-     */
-    protected $data = [];
-
-    /**
-     * Массив info, success, warning, error, validation информации
-     * @var array
-     */
-    protected $iswev = [];
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = '';
-
-    /**
-     * Тип обработки пользователей онлайн
-     * Если false, то идет обновление данных
-     * Если true, то идет возврат данных (смотрите $onlineFilter)
-     * @var bool
-     */
-    protected $onlineType = false;
-
-    /**
-     * Тип возврата данных при onlineType === true
-     * Если true, то из online должны вернутся только пользователи находящиеся на этой же странице
-     * Если false, то все пользователи online
-     * @var bool
-     */
-    protected $onlineFilter = true;
-
-    /**
-     * Переменная для meta name="robots"
-     * @var string
-     */
-    protected $robots;
-
-    /**
-     * Переменная для link rel="canonical"
-     * @var string
-     */
-    protected $canonical;
-
     /**
      * Конструктор
+     *
      * @param Container $container
      */
     public function __construct(Container $container)
     {
-        $this->c = $container;
-        $this->config = $container->config;
-        $container->Lang->load('common');
-    }
-
-    /**
-     * Возвращает HTTP заголовки страницы
-     * @return array
-     */
-    public function httpHeaders()
-    {
-        $headers = $this->httpHeaders;
-        if (! empty($status = $this->httpStatus())) {
-            $headers[] = $status;
-        }
-        return $headers;
-    }
-
-    /**
-     * Возвращает HTTP статус страницы или null
-     * @return null|string
-     */
-    protected function httpStatus()
-    {
-        $list = [
-            403 => '403 Forbidden',
-            404 => '404 Not Found',
-            405 => '405 Method Not Allowed',
-            501 => '501 Not Implemented',
-            503 => '503 Service Unavailable',
-        ];
-
-        if (isset($list[$this->httpStatus])) {
-            $status = 'HTTP/1.0 ';
-
-            if (isset($_SERVER['SERVER_PROTOCOL'])
-                && preg_match('%^HTTP/([12]\.[01])%', $_SERVER['SERVER_PROTOCOL'], $match)
-            ) {
-                $status = 'HTTP/' . $match[1] . ' ';
-            }
-
-            return $status . $list[$this->httpStatus];
-        }
-    }
-
-    /**
-     * Возвращает флаг готовности данных
-     * @return bool
-     */
-    public function isReady()
-    {
-        return ! empty($this->data);
-    }
-
-    /**
-     * Возвращает имя шаблона
-     * @return string
-     */
-    public function getNameTpl()
-    {
-        return $this->nameTpl;
-    }
-
-    /**
-     * Возвращает данные для шаблона
-     * @return array
-     */
-    public function getData()
-    {
-        return [
-            'pageTitle' => $this->pageTitle(),
-            'pageHeaders' => $this->pageHeaders(),
-            'fTitle' => $this->config['o_board_title'],
-            'fDescription' => $this->config['o_board_desc'],
-            'fNavigation' => $this->fNavigation(),
-            'fIndex' => $this->index,
-            'fAnnounce' => $this->fAnnounce(),
-            'fRootLink' => $this->c->Router->link('Index'),
-            'fIswev' => $this->getIswev(),
-        ] + $this->data;
-    }
-
-    /**
-     * Возврат info, success, warning, error, validation информации
-     * @return array
-     */
-    protected function getIswev()
-    {
-        if ($this->config['o_maintenance'] == '1' && $this->c->user->isAdmin) {
-            $this->iswev['w']['maintenance'] = __('Maintenance mode enabled', $this->c->Router->link('AdminOptions', ['#' => 'maintenance']));
-        }
-        return $this->iswev;
-    }
-
-    /**
-     * Установка info, success, warning, error, validation информации из вне
-     * @param array $iswev
-     * @return Page
-     */
-    public function setIswev(array $iswev)
-    {
-        $this->iswev = $iswev;
-        return $this;
-    }
+        parent::__construct($container);
 
-    /**
-     * Формирует title страницы
-     * @param array $titles
-     * @return string
-     */
-    protected function pageTitle(array $titles = [])
-    {
-        if (empty($titles)) {
-            $titles = $this->titles;
-        }
-        $titles[] = $this->config['o_board_title'];
-        return implode(__('Title separator'), $titles);
-    }
+        $container->Lang->load('common');
 
-    /**
-     * Генерация массива заголовков страницы
-     * @return array
-     */
-    protected function pageHeaders()
-    {
-        $headers = ['link rel="stylesheet" type="text/css" href="' . $this->c->PUBLIC_URL . '/style/' . $this->c->user->style . '/style.css' . '"'];
-        if ($this->robots) {
-            $headers[] = 'meta name="robots" content="' . $this->robots . '"';
-        }
-        if ($this->canonical) {
-            $headers[] = 'link rel="canonical" href="' . $this->canonical . '"';
-        }
-        return $headers;
+        $this->fIndex       = 'index'; # string      Указатель на активный пункт навигации
+        $this->httpStatus   = 200;     # int         HTTP статус ответа для данной страницы
+        $this->httpHeaders  = [];      # array       HTTP заголовки отличные от статуса
+#       $this->nameTpl      = null;    # null|string Имя шаблона
+#       $this->titles       = [];      # array       Массив титула страницы | setTitles()
+        $this->fIswev       = [];      # array       Массив info, success, warning, error, validation информации
+#       $this->onlinePos    = '';      # null|string Позиция для таблицы онлайн текущего пользователя
+        $this->onlineType   = false;   # bool        Тип обработки пользователей онлайн
+                                       #             Если false, то идет обновление данных
+                                       #             Если true, то идет возврат данных (смотрите onlineFilter)
+        $this->onlineFilter = true;    # bool        Тип возврата данных при onlineType === true
+                                       #             Если true, то из online должны вернутся только пользователи находящиеся на этой же странице
+                                       #             Если false, то все пользователи online
+#       $this->robots       = '';      # string      Переменная для meta name="robots"
+#       $this->canonical    = '';      # string      Переменная для link rel="canonical"
+
+        $this->fTitle       = $container->config->o_board_title;
+        $this->fDescription = $container->config->o_board_desc;
+        $this->fAnnounce    = $container->config->o_announcement_message;
+        $this->fRootLink    = $container->Router->link('Index');
     }
 
     /**
-     * Возвращает текст объявления или null
-     * @return null|string
+     * Подготовка страницы к отображению
      */
-    protected function fAnnounce()
+    public function prepare()
     {
-        return empty($this->config['o_announcement']) ? null : $this->config['o_announcement_message'];
+        $this->fNavigation = $this->fNavigation();
+        $this->maintenance();
     }
 
     /**
      * Возвращает массив ссылок с описанием для построения навигации
+     *
      * @return array
      */
     protected function fNavigation()
@@ -258,15 +64,15 @@ abstract class Page
             'index' => [$r->link('Index'), __('Index')]
         ];
 
-        if ($user->gReadBoard == '1' && $user->gViewUsers == '1') {
+        if ($user->g_read_board == '1' && $user->g_view_users == '1') {
             $nav['userlist'] = [$r->link('Userlist'), __('User list')];
         }
 
-        if ($this->config['o_rules'] == '1' && (! $user->isGuest || $user->gReadBoard == '1' || $this->config['o_regs_allow'] == '1')) {
+        if ($this->c->config->o_rules == '1' && (! $user->isGuest || $user->g_read_board == '1' || $this->c->config->o_regs_allow == '1')) {
             $nav['rules'] = [$r->link('Rules'), __('Rules')];
         }
 
-        if ($user->gReadBoard == '1' && $user->gSearch == '1') {
+        if ($user->g_read_board == '1' && $user->g_search == '1') {
             $nav['search'] = [$r->link('Search'), __('Search')];
         }
 
@@ -279,8 +85,8 @@ abstract class Page
                 'name' => $user->username,
             ]), __('Profile')];
             // New PMS
-            if ($this->config['o_pms_enabled'] == '1' && ($user->isAdmin || $user->messagesNew > 0)) { //????
-                $nav['pmsnew'] = ['pmsnew.php', __('PM')]; //'<li id="nav"'.((PUN_ACTIVE_PAGE == 'pms_new' || $user['messages_new'] > 0) ? ' class="isactive"' : '').'><a href="pmsnew.php">'.__('PM').(($user['messages_new'] > 0) ? ' (<span'.((empty($this->config['o_pms_flasher']) || PUN_ACTIVE_PAGE == 'pms_new') ? '' : ' class="remflasher"' ).'>'.$user['messages_new'].'</span>)' : '').'</a></li>';
+            if ($this->c->config->o_pms_enabled == '1' && ($user->isAdmin || $user->messages_new > 0)) { //????
+                $nav['pmsnew'] = ['pmsnew.php', __('PM')]; //'<li id="nav"'.((PUN_ACTIVE_PAGE == 'pms_new' || $user['messages_new'] > 0) ? ' class="isactive"' : '').'><a href="pmsnew.php">'.__('PM').(($user['messages_new'] > 0) ? ' (<span'.((empty($this->c->config->o_pms_flasher) || PUN_ACTIVE_PAGE == 'pms_new') ? '' : ' class="remflasher"' ).'>'.$user['messages_new'].'</span>)' : '').'</a></li>';
             }
             // New PMS
 
@@ -293,9 +99,9 @@ abstract class Page
             ]), __('Logout')];
         }
 
-        if ($user->gReadBoard == '1' && $this->config['o_additional_navlinks'] != '') {
+        if ($user->g_read_board == '1' && $this->c->config->o_additional_navlinks != '') {
             // position|name|link[|id]\n
-            if (preg_match_all('%^(\d+)\|([^\|\n\r]+)\|([^\|\n\r]+)(?:\|([^\|\n\r]+))?$%m', $this->config['o_additional_navlinks']."\n", $matches)) {
+            if (preg_match_all('%^(\d+)\|([^\|\n\r]+)\|([^\|\n\r]+)(?:\|([^\|\n\r]+))?$%m', $this->c->config->o_additional_navlinks."\n", $matches)) {
                $k = count($matches[0]);
                for ($i = 0; $i < $k; ++$i) {
                    if (empty($matches[4][$i])) {
@@ -317,20 +123,113 @@ abstract class Page
     }
 
     /**
-     * Возращает данные для управления обработкой пользователей онлайн
-     * @param bool $short
-     * @return bool|array
+     * Вывод информации о режиме обслуживания для админа
+     */
+    protected function maintenance()
+    {
+        if ($this->c->config->o_maintenance == '1' && $this->c->user->isAdmin) {
+            $this->a['fIswev']['w']['maintenance'] = __('Maintenance mode enabled', $this->c->Router->link('AdminOptions', ['#' => 'maintenance']));
+        }
+    }
+
+    /**
+     * Возвращает title страницы
+     * $this->pageTitle
+     *
+     * @param array $titles
+     *
+     * @return string
+     */
+    protected function getPageTitle(array $titles = [])
+    {
+        if (empty($titles)) {
+            $titles = $this->titles;
+        }
+        $titles[] = $this->c->config->o_board_title;
+        return implode(__('Title separator'), $titles);
+    }
+
+    /**
+     * Возвращает массива заголовков страницы
+     * $this->pageHeaders
+     *
+     * @return array
+     */
+    protected function getPageHeaders()
+    {
+        $headers = ['link rel="stylesheet" type="text/css" href="' . $this->c->PUBLIC_URL . '/style/' . $this->c->user->style . '/style.css' . '"'];
+        if ($this->robots) {
+            $headers[] = 'meta name="robots" content="' . $this->robots . '"';
+        }
+        if ($this->canonical) {
+            $headers[] = 'link rel="canonical" href="' . $this->canonical . '"';
+        }
+        return $headers;
+    }
+
+    /**
+     * Возвращает HTTP заголовки страницы
+     * $this->httpHeaders
+     *
+     * @return array
      */
-    public function getDataForOnline($short = false)
+    protected function getHttpHeaders()
     {
-        return $short
-            ? null !== $this->onlinePos
-            : [$this->onlinePos, $this->onlineType, $this->onlineFilter];
+        $headers = $this->a['httpHeaders'];
+        if (! empty($status = $this->httpStatus())) {
+            $headers[] = $status;
+        }
+        return $headers;
+    }
+
+    /**
+     * Возвращает HTTP статус страницы или null
+     *
+     * @return null|string
+     */
+    protected function httpStatus()
+    {
+        $list = [
+            403 => '403 Forbidden',
+            404 => '404 Not Found',
+            405 => '405 Method Not Allowed',
+            501 => '501 Not Implemented',
+            503 => '503 Service Unavailable',
+        ];
+
+        if (isset($list[$this->httpStatus])) {
+            $status = 'HTTP/1.0 ';
+
+            if (isset($_SERVER['SERVER_PROTOCOL'])
+                && preg_match('%^HTTP/([12]\.[01])%', $_SERVER['SERVER_PROTOCOL'], $match)
+            ) {
+                $status = 'HTTP/' . $match[1] . ' ';
+            }
+
+            return $status . $list[$this->httpStatus];
+        }
+    }
+
+    /**
+     * Дописывает в массив титула страницы новый элемент
+     * $this->titles
+     *
+     * @param string @val
+     */
+    public function setTitles($val)
+    {
+        if (empty($this->a['titles'])) {
+            $this->a['titles'] = [$val];
+        } else {
+            $this->a['titles'][] = $val;
+        }
     }
 
     /**
      * Возвращает размер в байтах, Кбайтах, ...
+     *
      * @param int $size
+     *
      * @return string
      */
     protected function size($size)
@@ -346,8 +245,10 @@ abstract class Page
 
     /**
      * Возвращает число в формате языка текущего пользователя
+     *
      * @param mixed $number
      * @param int $decimals
+     *
      * @return string
      */
     protected function number($number, $decimals = 0)
@@ -357,15 +258,16 @@ abstract class Page
             : 'not a number';
     }
 
-
     /**
      * Возвращает время в формате текущего пользователя
+     *
      * @param int|string $timestamp
      * @param bool $dateOnly
      * @param string $dateFormat
      * @param string $timeFormat
      * @param bool $timeOnly
      * @param bool $noText
+     *
      * @return string
      */
     protected function time($timestamp, $dateOnly = false, $dateFormat = null, $timeFormat = null, $timeOnly = false, $noText = false)
@@ -380,10 +282,10 @@ abstract class Page
         $timestamp += $diff;
 
         if (null === $dateFormat) {
-            $dateFormat = $this->c->DATE_FORMATS[$user->dateFormat];
+            $dateFormat = $this->c->DATE_FORMATS[$user->date_format];
         }
         if(null === $timeFormat) {
-            $timeFormat = $this->c->TIME_FORMATS[$user->timeFormat];
+            $timeFormat = $this->c->TIME_FORMATS[$user->time_format];
         }
 
         $date = gmdate($dateFormat, $timestamp);
@@ -406,29 +308,4 @@ abstract class Page
             return $date . ' ' . gmdate($timeFormat, $timestamp);
         }
     }
-
-    /**
-     * Выполняет цензуру при необходимости
-     * @param string $str
-     * @return string
-     */
-    protected function censor($str)
-    {
-        if ($this->config['o_censoring'] == '1') {
-            return (string) preg_replace($this->c->censoring[0], $this->c->censoring[1],  $str);
-        } else {
-            return $str;
-        }
-    }
-
-    /**
-     * Заглушка
-     * @param string $name
-     * @param array $arguments
-     * @throws \RuntimeException
-     */
-    public function __call($name, array $arguments)
-    {
-        throw new RuntimeException("'{$name}' method not found.");
-    }
 }

+ 90 - 0
app/Models/Pages/Admin.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+use ForkBB\Core\Container;
+use ForkBB\Models\Page;
+
+abstract class Admin extends Page
+{
+    /**
+     * Конструктор
+     * 
+     * @param Container $container
+     */
+    public function __construct(Container $container)
+    {
+        parent::__construct($container);
+        
+        $this->aIndex    = 'index'; # string Указатель на активный пункт навигации в меню админки
+        $this->fIndex    = 'admin';
+        $this->onlinePos = 'admin';
+        $this->robots    = 'noindex, nofollow';
+
+        $container->Lang->load('admin');
+    }
+
+    /**
+     * Подготовка страницы к отображению
+     */
+    public function prepare()
+    {
+        $this->aNavigation = $this->aNavigation();
+        parent::prepare();
+    }
+
+    /**
+     * Возвращает массив ссылок с описанием для построения навигации админки
+     * 
+     * @return array
+     */
+    protected function aNavigation()
+    {
+        $user = $this->c->user;
+        $r = $this->c->Router;
+
+        $nav = [
+            'Moderator menu'  => [
+                'index' => [$r->link('Admin'), __('Admin index')],
+                'users' => ['admin_users.php', __('Users')],
+            ],
+        ];
+        if ($user->isAdmin || $user->g_mod_ban_users == '1') {
+            $nav['Moderator menu']['bans'] = ['admin_bans.php', __('Bans')];
+        }
+        if ($user->isAdmin || $this->c->config->o_report_method == '0' || $this->c->config->o_report_method == '2') {
+            $nav['Moderator menu']['reports'] = ['admin_reports.php', __('Reports')];
+        }
+
+        if ($user->isAdmin) {
+            $nav['Admin menu'] = [
+                'options'     => ['admin_options.php', __('Admin options')],
+                'permissions' => ['admin_permissions.php', __('Permissions')],
+                'categories'  => ['admin_categories.php', __('Categories')],
+                'forums'      => ['admin_forums.php', __('Forums')],
+                'groups'      => [$r->link('AdminGroups'), __('User groups')],
+                'censoring'   => ['admin_censoring.php', __('Censoring')],
+                'maintenance' => ['admin_maintenance.php', __('Maintenance')]
+            ];
+        }
+
+        return $nav;
+    }
+
+    /**
+     * Возвращает title страницы
+     * $this->pageTitle
+     * 
+     * @param array $titles
+     * 
+     * @return string
+     */
+    protected function getPageTitle(array $titles = [])
+    {
+        if (empty($titles)) {
+            $titles = $this->titles;
+        }
+        $titles[] = __('Admin title');
+        return parent::getPageTitle($titles);
+    }
+}

+ 0 - 105
app/Models/Pages/Admin/Admin.php

@@ -1,105 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Pages\Admin;
-
-use ForkBB\Core\Container;
-use ForkBB\Models\Pages\Page;
-
-abstract class Admin extends Page
-{
-    /**
-     * Указатель на активный пункт навигации админки
-     * @var string
-     */
-    protected $adminIndex;
-
-    /**
-     * Указатель на активный пункт навигации
-     * @var string
-     */
-    protected $index = 'admin';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var string
-     */
-    protected $onlinePos = 'admin';
-
-    /**
-     * Переменная для meta name="robots"
-     * @var string
-     */
-    protected $robots = 'noindex, nofollow';
-
-    /**
-     * Конструктор
-     * @param Container $container
-     */
-    public function __construct(Container $container)
-    {
-        parent::__construct($container);
-        $container->Lang->load('admin');
-    }
-
-    /**
-     * Возвращает данные для шаблона
-     * @return array
-     */
-    public function getData()
-    {
-        $data = parent::getData();
-        $data['aNavigation'] = $this->aNavigation();
-        $data['aIndex'] = $this->adminIndex;
-        return $data;
-    }
-
-    /**
-     * Формирует title страницы
-     * @param array $titles
-     * @return string
-     */
-    protected function pageTitle(array $titles = [])
-    {
-        $titles = $this->titles;
-        $titles[] = __('Admin title');
-        return parent::pageTitle($titles);
-    }
-
-    /**
-     * Возвращает массив ссылок с описанием для построения навигации админки
-     * @return array
-     */
-    protected function aNavigation()
-    {
-        $user = $this->c->user;
-        $r = $this->c->Router;
-
-        $nav = [
-            'Moderator menu'  => [
-                'index' => [$r->link('Admin'), __('Admin index')],
-                'users' => ['admin_users.php', __('Users')],
-            ],
-        ];
-        if ($user->isAdmin || $user->gModBanUsers == '1') {
-            $nav['Moderator menu']['bans'] = ['admin_bans.php', __('Bans')];
-        }
-        if ($user->isAdmin || $this->config['o_report_method'] == '0' || $this->config['o_report_method'] == '2') {
-            $nav['Moderator menu']['reports'] = ['admin_reports.php', __('Reports')];
-        }
-
-        if ($user->isAdmin) {
-            $nav['Admin menu'] = [
-                'options' => ['admin_options.php', __('Admin options')],
-                'permissions' => ['admin_permissions.php', __('Permissions')],
-                'categories' => ['admin_categories.php', __('Categories')],
-                'forums' => ['admin_forums.php', __('Forums')],
-                'groups' => [$r->link('AdminGroups'), __('User groups')],
-                'censoring' => ['admin_censoring.php', __('Censoring')],
-                'maintenance' => ['admin_maintenance.php', __('Maintenance')]
-            ];
-        }
-
-        return $nav;
-    }
-
-}

+ 35 - 39
app/Models/Pages/Admin/Groups.php

@@ -4,21 +4,10 @@ namespace ForkBB\Models\Pages\Admin;
 
 use ForkBB\Core\Container;
 use ForkBB\Core\Validator;
+use ForkBB\Models\Pages\Admin;
 
 class Groups extends Admin
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'admin/groups';
-
-    /**
-     * Указатель на активный пункт навигации админки
-     * @var string
-     */
-    protected $adminIndex = 'groups';
-
     /**
      * Массив групп
      * @var array
@@ -45,6 +34,7 @@ class Groups extends Admin
 
     /**
      * Конструктор
+     * 
      * @param Container $container
      */
     public function __construct(Container $container)
@@ -66,6 +56,7 @@ class Groups extends Admin
                 }
             }
         }
+        $this->aIndex = 'groups';
     }
 
     /**
@@ -84,6 +75,7 @@ class Groups extends Admin
 
     /**
      * Подготавливает данные для шаблона
+     * 
      * @return Page
      */
     public function view()
@@ -109,23 +101,23 @@ class Groups extends Admin
 
         $this->c->Lang->load('admin_groups');
 
-        $this->data = [
-            'formActionNew'     => $this->c->Router->link('AdminGroupsNew'),
-            'formTokenNew'      => $this->c->Csrf->create('AdminGroupsNew'),
-            'formActionDefault' => $this->c->Router->link('AdminGroupsDefault'),
-            'formTokenDefault'  => $this->c->Csrf->create('AdminGroupsDefault'),
-            'defaultGroup'      => $this->config['o_default_user_group'],
-            'groupsNew'         => $groupsNew,
-            'groupsDefault'     => $groupsDefault,
-            'groupsList'        => $groupsList,
-            'tabindex'          => 0,
-        ];
+        $this->nameTpl = 'admin/groups';
+        $this->formActionNew     = $this->c->Router->link('AdminGroupsNew');
+        $this->formTokenNew      = $this->c->Csrf->create('AdminGroupsNew');
+        $this->formActionDefault = $this->c->Router->link('AdminGroupsDefault');
+        $this->formTokenDefault  = $this->c->Csrf->create('AdminGroupsDefault');
+        $this->defaultGroup      = $this->c->config->o_default_user_group;
+        $this->groupsNew         = $groupsNew;
+        $this->groupsDefault     = $groupsDefault;
+        $this->groupsList        = $groupsList;
+        $this->tabindex          = 0;
 
         return $this;
     }
 
     /**
      * Устанавливает группу по умолчанию
+     * 
      * @return Page
      */
     public function defaultPost()
@@ -138,18 +130,20 @@ class Groups extends Admin
         ]);
 
         if (! $v->validation($_POST)) {
-            $this->iswev = $v->getErrors();
+            $this->fIswev = $v->getErrors();
             return $this->view();
         }
-        $this->c->DB->exec('UPDATE ::config SET conf_value=?s:id WHERE conf_name=\'o_default_user_group\'', [':id' => $v->defaultgroup]);
-        $this->c->{'config update'};
-        return $this->c->Redirect->setPage('AdminGroups')->setMessage(__('Default group redirect'));
+        $this->c->config->o_default_user_group = $v->defaultgroup;
+        $this->c->config->save();
+        return $this->c->Redirect->page('AdminGroups')->message(__('Default group redirect'));
     }
 
     /**
      * Подготавливает данные для создание группы
      * Создает новую группу
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function newPost(array $args)
@@ -163,7 +157,7 @@ class Groups extends Admin
             ]);
 
             if (! $v->validation($_POST)) {
-                $this->iswev = $v->getErrors();
+                $this->fIswev = $v->getErrors();
                 return $this->view();
             } else {
                 return $this->edit(['id' => $v->basegroup, '_new' => true]);
@@ -175,7 +169,9 @@ class Groups extends Admin
 
     /**
      * Подготавливает данные для шаблона редактирования группы
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function edit(array $args)
@@ -203,20 +199,20 @@ class Groups extends Admin
 
         $this->c->Lang->load('admin_groups');
 
-        $this->data = [
-            'formAction' => $this->c->Router->link($marker, $vars),
-            'formToken'  => $this->c->Csrf->create($marker, $vars),
-            'form'       => $this->viewForm($id, $groups[$args['id']]),
-            'warn'       => empty($groups[$args['id']]['g_moderator']) ? null : __('Moderator info'),
-            'tabindex'   => 0,
-        ];
+        $this->formAction = $this->c->Router->link($marker, $vars);
+        $this->formToken  = $this->c->Csrf->create($marker, $vars);
+        $this->form       = $this->viewForm($id, $groups[$args['id']]);
+        $this->warn       = empty($groups[$args['id']]['g_moderator']) ? null : __('Moderator info');
+        $this->tabindex   = 0;
 
         return $this;
     }
 
     /**
      * Запись данных по новой/измененной группе
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function editPost(array $args)
@@ -274,7 +270,7 @@ class Groups extends Admin
         ]);
 
         if (! $v->validation($_POST)) {
-            $this->iswev = $v->getErrors();
+            $this->fIswev = $v->getErrors();
             $args['_data'] = $v->getData();
             return $this->edit($args);
         }
@@ -334,8 +330,8 @@ class Groups extends Admin
         $this->c->Cache->delete('forums_mark');
 
         return $this->c->Redirect
-            ->setPage('AdminGroups')
-            ->setMessage($id === -1 ? __('Group added redirect') : __('Group edited redirect'));
+            ->page('AdminGroups')
+            ->message($id === -1 ? __('Group added redirect') : __('Group edited redirect'));
     }
 
     /**
@@ -397,7 +393,7 @@ class Groups extends Admin
 
         $y = __('Yes');
         $n = __('No');
-        if ($id !== $this->c->GROUP_GUEST && $id != $this->config['o_default_user_group']) {
+        if ($id !== $this->c->GROUP_GUEST && $id != $this->c->config->o_default_user_group) {
             $form['g_moderator'] = [
                 'type' => 'radio',
                 'value' => isset($data['g_moderator']) ? $data['g_moderator'] : 0,

+ 9 - 17
app/Models/Pages/Admin/Index.php

@@ -2,32 +2,24 @@
 
 namespace ForkBB\Models\Pages\Admin;
 
+use ForkBB\Models\Pages\Admin;
+
 class Index extends Admin
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'admin/index';
-
-    /**
-     * Указатель на активный пункт навигации админки
-     * @var string
-     */
-    protected $adminIndex = 'index';
-
     /**
      * Подготавливает данные для шаблона
+     * 
      * @return Page
      */
     public function index()
     {
         $this->c->Lang->load('admin_index');
-        $this->data = [
-            'revision' => $this->config['i_fork_revision'],
-            'linkStat' => $this->c->Router->link('AdminStatistics'),
-        ];
-        $this->titles[] = __('Admin index');
+
+        $this->nameTpl  = 'admin/index';
+        $this->titles   = __('Admin index');
+        $this->revision = $this->c->config->i_fork_revision;
+        $this->linkStat = $this->c->Router->link('AdminStatistics');
+
         return $this;
     }
 }

+ 24 - 30
app/Models/Pages/Admin/Statistics.php

@@ -2,22 +2,13 @@
 
 namespace ForkBB\Models\Pages\Admin;
 
+use ForkBB\Models\Pages\Admin;
+
 class Statistics extends Admin
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'admin/statistics';
-
-    /**
-     * Указатель на активный пункт навигации админки
-     * @var string
-     */
-    protected $adminIndex = 'index';
-
     /**
      * phpinfo
+     * 
      * @return Page|null
      */
     public function info()
@@ -33,14 +24,17 @@ class Statistics extends Admin
 
     /**
      * Подготавливает данные для шаблона
+     * 
      * @return Page
      */
     public function statistics()
     {
         $this->c->Lang->load('admin_index');
-        $this->titles[] = __('Server statistics');
-        $this->data['isAdmin'] = $this->c->user->isAdmin;
-        $this->data['linkInfo'] = $this->c->Router->link('AdminInfo');
+
+        $this->nameTpl  = 'admin/statistics';
+        $this->titles   = __('Server statistics');
+        $this->isAdmin  = $this->c->user->isAdmin;
+        $this->linkInfo = $this->c->Router->link('AdminInfo');
 
         // Get the server load averages (if possible)
         if (@file_exists('/proc/loadavg') && is_readable('/proc/loadavg')) {
@@ -57,38 +51,38 @@ class Statistics extends Admin
             }
 
             $ave = @explode(' ', $ave);
-            $this->data['serverLoad'] = isset($ave[2]) ? $ave[0].' '.$ave[1].' '.$ave[2] : __('Not available');
+            $this->serverLoad = isset($ave[2]) ? $ave[0].' '.$ave[1].' '.$ave[2] : __('Not available');
         } elseif (!in_array(PHP_OS, array('WINNT', 'WIN32')) && preg_match('%averages?: ([\d\.]+),?\s+([\d\.]+),?\s+([\d\.]+)%i', @exec('uptime'), $ave)) {
-            $this->data['serverLoad'] = $ave[1].' '.$ave[2].' '.$ave[3];
+            $this->serverLoad = $ave[1].' '.$ave[2].' '.$ave[3];
         } else {
-            $this->data['serverLoad'] = __('Not available');
+            $this->serverLoad = __('Not available');
         }
 
         // Get number of current visitors
-        $this->data['numOnline'] = $this->c->DB->query('SELECT COUNT(user_id) FROM ::online WHERE idle=0')->fetchColumn();
+        $this->numOnline = $this->c->DB->query('SELECT COUNT(user_id) FROM ::online WHERE idle=0')->fetchColumn();
 
         $stat = $this->c->DB->statistics();
-        $this->data['dbVersion'] = $stat['db'];
-        $this->data['tSize'] = $this->size($stat['size']);
-        $this->data['tRecords'] = $this->number($stat['records']);
+        $this->dbVersion = $stat['db'];
+        $this->tSize     = $this->size($stat['size']);
+        $this->tRecords  = $this->number($stat['records']);
         unset($stat['db'], $stat['size'], $stat['records']);
-        $this->data['tOther'] = $stat;
+        $this->tOther    = $stat;
 
         // Check for the existence of various PHP opcode caches/optimizers
         if (function_exists('mmcache')) {
-            $this->data['accelerator'] = '<a href="http://' . __('Turck MMCache link') . '">' . __('Turck MMCache') . '</a>';
+            $this->accelerator = '<a href="http://' . __('Turck MMCache link') . '">' . __('Turck MMCache') . '</a>';
         } elseif (isset($_PHPA)) {
-            $this->data['accelerator'] = '<a href="http://' . __('ionCube PHP Accelerator link') . '">' . __('ionCube PHP Accelerator') . '</a>';
+            $this->accelerator = '<a href="http://' . __('ionCube PHP Accelerator link') . '">' . __('ionCube PHP Accelerator') . '</a>';
         } elseif (ini_get('apc.enabled')) {
-            $this->data['accelerator'] ='<a href="http://' . __('Alternative PHP Cache (APC) link') . '">' . __('Alternative PHP Cache (APC)') . '</a>';
+            $this->accelerator ='<a href="http://' . __('Alternative PHP Cache (APC) link') . '">' . __('Alternative PHP Cache (APC)') . '</a>';
         } elseif (ini_get('zend_optimizer.optimization_level')) {
-            $this->data['accelerator'] = '<a href="http://' . __('Zend Optimizer link') . '">' . __('Zend Optimizer') . '</a>';
+            $this->accelerator = '<a href="http://' . __('Zend Optimizer link') . '">' . __('Zend Optimizer') . '</a>';
         } elseif (ini_get('eaccelerator.enable')) {
-            $this->data['accelerator'] = '<a href="http://' . __('eAccelerator link') . '">' . __('eAccelerator') . '</a>';
+            $this->accelerator = '<a href="http://' . __('eAccelerator link') . '">' . __('eAccelerator') . '</a>';
         } elseif (ini_get('xcache.cacher')) {
-            $this->data['accelerator'] = '<a href="http://' . __('XCache link') . '">' . __('XCache') . '</a>';
+            $this->accelerator = '<a href="http://' . __('XCache link') . '">' . __('XCache') . '</a>';
         } else {
-            $this->data['accelerator'] = __('NA');
+            $this->accelerator = __('NA');
         }
 
         return $this;

+ 102 - 99
app/Models/Pages/Auth.php

@@ -4,62 +4,43 @@ namespace ForkBB\Models\Pages;
 
 use ForkBB\Core\Validator;
 use ForkBB\Core\Exceptions\MailException;
+use ForkBB\Models\Page;
 use ForkBB\Models\User;
 
 class Auth extends Page
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'login';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = 'login';
-
-    /**
-     * Указатель на активный пункт навигации
-     * @var string
-     */
-    protected $index = 'login';
-
     /**
      * Для передачи User из vCheckEmail() в forgetPost()
      * @var User
      */
-    protected $tmpUser;
-
-    /**
-     * Переменная для meta name="robots"
-     * @var string
-     */
-    protected $robots = 'noindex';
+    protected $tmpUser; //????
 
     /**
      * Выход пользователя
+     * 
      * @param array $args
-     * @retrun Page
+     * 
+     * @return Page
      */
     public function logout($args)
     {
         if (empty($args['token']) || ! $this->c->Csrf->verify($args['token'], 'Logout', $args)) {
-            return $this->c->Redirect->setPage('Index')->setMessage(__('Bad token'));
+            return $this->c->Redirect->page('Index')->message(__('Bad token'));
         }
 
-        $this->c->UserCookie->deleteUserCookie();
+        $this->c->Cookie->deleteUser();
         $this->c->Online->delete($this->c->user);
-        $this->c->UserMapper->updateLastVisit($this->c->user);
+        $this->c->user->updateLastVisit();
 
         $this->c->Lang->load('auth');
-        return $this->c->Redirect->setPage('Index')->setMessage(__('Logout redirect'));
+        return $this->c->Redirect->page('Index')->message(__('Logout redirect'));
     }
 
     /**
      * Подготовка данных для страницы входа на форум
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function login(array $args)
@@ -74,24 +55,25 @@ class Auth extends Page
             $args['_redirect'] = $this->c->Router->validate($args['_redirect'], 'Index');
         }
 
-        $this->titles[] = __('Login');
-        $this->data = [
-            'formAction' => $this->c->Router->link('Login'),
-            'formToken' => $this->c->Csrf->create('Login'),
-            'forgetLink' => $this->c->Router->link('Forget'),
-            'regLink' => $this->config['o_regs_allow'] == '1'
-                ? $this->c->Router->link('Register')
-                : null,
-            'username' => $args['_username'],
-            'redirect' => $args['_redirect'],
-            'save' => ! empty($args['_save'])
-        ];
+        $this->fIndex     = 'login';
+        $this->nameTpl    = 'login';
+        $this->onlinePos  = 'login';
+        $this->robots     = 'noindex';
+        $this->titles     = __('Login');
+        $this->formAction = $this->c->Router->link('Login');
+        $this->formToken  = $this->c->Csrf->create('Login');
+        $this->forgetLink = $this->c->Router->link('Forget');
+        $this->regLink    = $this->c->config->o_regs_allow == '1' ? $this->c->Router->link('Register') : null;
+        $this->username   = $args['_username'];
+        $this->redirect   = $args['_redirect'];
+        $this->save       = ! empty($args['_save']);
 
         return $this;
     }
 
     /**
      * Вход на форум
+     * 
      * @return Page
      */
     public function loginPost()
@@ -109,9 +91,9 @@ class Auth extends Page
         ]);
 
         if ($v->validation($_POST)) {
-            return $this->c->Redirect->setUrl($v->redirect)->setMessage(__('Login redirect'));
+            return $this->c->Redirect->url($v->redirect)->message(__('Login redirect'));
         } else {
-            $this->iswev = $v->getErrors();
+            $this->fIswev = $v->getErrors();
             return $this->login([
                 '_username' => $v->username,
                 '_redirect' => $v->redirect,
@@ -122,27 +104,28 @@ class Auth extends Page
 
     /**
      * Проверка по базе и вход на форум
+     * 
      * @param Validator $v
      * @param string $password
+     * 
      * @return array
      */
     public function vLoginProcess(Validator $v, $password)
     {
         $error = false;
         if (! empty($v->getErrors())) {
-        } elseif (! ($user = $this->c->UserMapper->getUser($v->username, 'username')) instanceof User) {
+        } elseif (! ($user = $this->c->ModelUser->load($v->username, 'username')) instanceof User) {
             $error = __('Wrong user/pass');
         } elseif ($user->isUnverified) {
             $error = [__('Account is not activated'), 'w'];
         } else {
             $authorized = false;
             $hash = $user->password;
-            $update = [];
             // For FluxBB by Visman 1.5.10.74 and above
             if (strlen($hash) == 40) {
                 if (hash_equals($hash, sha1($password . $this->c->SALT1))) {
                     $hash = password_hash($password, PASSWORD_DEFAULT);
-                    $update['password'] = $hash;
+                    $user->password = $hash;
                     $authorized = true;
                 }
             } else {
@@ -154,16 +137,16 @@ class Auth extends Page
             } else {
                 // перезаписываем ip админа и модератора - Visman
                 if ($user->isAdmMod
-                    && $this->config['o_check_ip']
-                    && $user->registrationIp != $this->c->user->ip
+                    && $this->c->config->o_check_ip
+                    && $user->registration_ip != $this->c->user->ip
                 ) {
-                    $update['registration_ip'] = $this->c->user->ip;
+                    $user->registration_ip = $this->c->user->ip;
                 }
                 // изменения юзера в базе
-                $this->c->UserMapper->updateUser($user->id, $update);
+                $user->update();
 
                 $this->c->Online->delete($this->c->user);
-                $this->c->UserCookie->setUserCookie($user->id, $hash, $v->save);
+                $this->c->Cookie->setUser($user);
             }
         }
         return [$password, $error];
@@ -171,32 +154,34 @@ class Auth extends Page
 
     /**
      * Подготовка данных для страницы восстановления пароля
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function forget(array $args)
     {
-        $this->nameTpl = 'passphrase_reset';
-        $this->onlinePos = 'passphrase_reset';
+        $this->c->Lang->load('auth');
 
         if (! isset($args['_email'])) {
             $args['_email'] = '';
         }
 
-        $this->c->Lang->load('auth');
-
-        $this->titles[] = __('Passphrase reset');
-        $this->data = [
-            'formAction' => $this->c->Router->link('Forget'),
-            'formToken' => $this->c->Csrf->create('Forget'),
-            'email' => $args['_email'],
-        ];
+        $this->fIndex     = 'login';
+        $this->nameTpl    = 'passphrase_reset';
+        $this->onlinePos  = 'passphrase_reset';
+        $this->robots     = 'noindex';
+        $this->titles     = __('Passphrase reset');
+        $this->formAction = $this->c->Router->link('Forget');
+        $this->formToken  = $this->c->Csrf->create('Forget');
+        $this->email      = $args['_email'];
 
         return $this;
     }
 
     /**
      * Отправка письма для восстановления пароля
+     * 
      * @return Page
      */
     public function forgetPost()
@@ -213,7 +198,7 @@ class Auth extends Page
         ]);
 
         if (! $v->validation($_POST)) {
-            $this->iswev = $v->getErrors();
+            $this->fIswev = $v->getErrors();
             return $this->forget([
                 '_email' => $v->email,
             ]);
@@ -224,7 +209,7 @@ class Auth extends Page
         $link = $this->c->Router->link('ChangePassword', ['email' => $v->email, 'key' => $key, 'hash' => $hash]);
         $tplData = [
             'fRootLink' => $this->c->Router->link('Index'),
-            'fMailer' => __('Mailer', $this->config['o_board_title']),
+            'fMailer' => __('Mailer', $this->c->config->o_board_title),
             'username' => $this->tmpUser->username,
             'link' => $link,
         ];
@@ -235,7 +220,7 @@ class Auth extends Page
                 ->setFolder($this->c->DIR_LANG)
                 ->setLanguage($this->tmpUser->language)
                 ->setTo($v->email, $this->tmpUser->username)
-                ->setFrom($this->config['o_webmaster_email'], __('Mailer', $this->config['o_board_title']))
+                ->setFrom($this->c->config->o_webmaster_email, __('Mailer', $this->c->config->o_board_title))
                 ->setTpl('passphrase_reset.tpl', $tplData)
                 ->send();
         } catch (MailException $e) {
@@ -243,33 +228,42 @@ class Auth extends Page
         }
 
         if ($isSent) {
-            $this->c->UserMapper->updateUser($this->tmpUser->id, ['activate_string' => $key, 'last_email_sent' => time()]);
-            return $this->c->Message->message(__('Forget mail', $this->config['o_admin_email']), false, 200);
+            $this->tmpUser->activate_string = $key;
+            $this->tmpUser->last_email_sent = time();
+            $this->tmpUser->update();
+            return $this->c->Message->message(__('Forget mail', $this->c->config->o_admin_email), false, 200);
         } else {
-            return $this->c->Message->message(__('Error mail', $this->config['o_admin_email']), true, 200);
+            return $this->c->Message->message(__('Error mail', $this->c->config->o_admin_email), true, 200);
         }
     }
 
     /**
      * Дополнительная проверка email
+     * 
      * @param Validator $v
-     * @param string $username
+     * @param string $email
+     * 
      * @return array
      */
     public function vCheckEmail(Validator $v, $email)
     {
-        $error = false;
-        // есть ошибки
         if (! empty($v->getErrors())) {
+            return [$email, false];
+        }
+            
+        $error = false;
+        $user = $this->c->ModelUser;
+        $user->__email = $email;
+
         // email забанен
-        } elseif ($this->c->CheckBans->isBanned(null, $email) > 0) {
+        if ($this->c->bans->isBanned($user) > 0) {
             $error = __('Banned email');
         // нет пользователя с таким email
-        } elseif (! ($user = $this->c->UserMapper->getUser($email, 'email')) instanceof User) {
+        } elseif (! $user->load($email, 'email') instanceof User) {
             $error = __('Invalid email');
         // за последний час уже был запрос на этот email
-        } elseif (! empty($user->lastEmailSent) && time() - $user->lastEmailSent < 3600) {
-            $error = [__('Email flood', (int) (($user->lastEmailSent + 3600 - time()) / 60)), 'e'];
+        } elseif (! empty($user->last_email_sent) && time() - $user->last_email_sent < 3600) {
+            $error = [__('Email flood', (int) (($user->last_email_sent + 3600 - time()) / 60)), 'e'];
         } else {
             $this->tmpUser = $user;
         }
@@ -278,23 +272,23 @@ class Auth extends Page
 
     /**
      * Подготовка данных для формы изменения пароля
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function changePass(array $args)
     {
-        $this->nameTpl = 'change_passphrase';
-        $this->onlinePos = 'change_passphrase';
-
-        if (isset($args['_ok'])) {
-            unset($args['_ok']);
+        if (isset($args['_user'])) {
+            $user = $args['_user'];
+            unset($args['_user']);
         } else {
             // что-то пошло не так
             if (! hash_equals($args['hash'], $this->c->Secury->hash($args['email'] . $args['key']))
-                || ! ($user = $this->c->UserMapper->getUser($args['email'], 'email')) instanceof User
-                || empty($user->activateString)
-                || $user->activateString{0} !== 'p'
-                || ! hash_equals($user->activateString, $args['key'])
+                || ! ($user = $this->c->ModelUser->load($args['email'], 'email')) instanceof User
+                || empty($user->activate_string)
+                || $user->activate_string{0} !== 'p'
+                || ! hash_equals($user->activate_string, $args['key'])
             ) {
                 return $this->c->Message->message(__('Bad request'), false);
             }
@@ -303,33 +297,39 @@ class Auth extends Page
         $this->c->Lang->load('auth');
 
         if ($user->isUnverified) {
-            $this->c->UserMapper->updateUser($user->id, ['group_id' => $this->config['o_default_user_group'], 'email_confirmed' => 1]);
+            $user->group_id = $this->c->config->o_default_user_group;
+            $user->email_confirmed = 1;
+            $user->update();
             $this->c->{'users_info update'};
-            $this->iswev['i'][] = __('Account activated');
+            $this->a['fIswev']['i'][] = __('Account activated');
         }
 
-        $this->titles[] = __('Change pass');
-        $this->data = [
-            'formAction' => $this->c->Router->link('ChangePassword', $args),
-            'formToken' => $this->c->Csrf->create('ChangePassword', $args),
-        ];
+        $this->fIndex     = 'login';
+        $this->nameTpl    = 'change_passphrase';
+        $this->onlinePos  = 'change_passphrase';
+        $this->robots     = 'noindex';
+        $this->titles     = __('Passphrase reset');
+        $this->formAction = $this->c->Router->link('ChangePassword', $args);
+        $this->formToken  = $this->c->Csrf->create('ChangePassword', $args);
 
         return $this;
     }
 
     /**
      * Смена пароля
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function changePassPost(array $args)
     {
         // что-то пошло не так
         if (! hash_equals($args['hash'], $this->c->Secury->hash($args['email'] . $args['key']))
-            || ! ($user = $this->c->UserMapper->getUser($args['email'], 'email')) instanceof User
-            || empty($user->activateString)
-            || $user->activateString{0} !== 'p'
-            || ! hash_equals($user->activateString, $args['key'])
+            || ! ($user = $this->c->ModelUser->load($args['email'], 'email')) instanceof User
+            || empty($user->activate_string)
+            || $user->activate_string{0} !== 'p'
+            || ! hash_equals($user->activate_string, $args['key'])
         ) {
             return $this->c->Message->message(__('Bad request'), false);
         }
@@ -349,15 +349,18 @@ class Auth extends Page
         ]);
 
         if (! $v->validation($_POST)) {
-            $this->iswev = $v->getErrors();
-            $args['_ok'] = true;
+            $this->fIswev = $v->getErrors();
+            $args['_user'] = $user;
             return $this->changePass($args);
         }
         $data = $v->getData();
 
-        $this->c->UserMapper->updateUser($user->id, ['password' => password_hash($data['password'], PASSWORD_DEFAULT), 'email_confirmed' => 1, 'activate_string' => null]);
+        $user->password = password_hash($data['password'], PASSWORD_DEFAULT);
+        $user->email_confirmed = 1;
+        $user->activate_string = null;
+        $user->update();
 
-        $this->iswev['s'][] = __('Pass updated');
+        $this->a['fIswev']['s'][] = __('Pass updated');
         return $this->login(['_redirect' => $this->c->Router->link('Index')]);
     }
 }

+ 26 - 27
app/Models/Pages/Ban.php

@@ -2,42 +2,41 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Page;
+use ForkBB\Models\User;
+
 class Ban extends Page
 {
     /**
-     * Имя шаблона
-     * @var string
+     * Подготавливает данные для шаблона
+     * 
+     * @param User $user
+     * 
+     * @return Page
      */
-    protected $nameTpl = 'ban';
+    public function ban(User $user)
+    {
+        $ban = $user->banInfo;
+        
+        if (! empty($ban['expire'])) {
+            $ban['expire'] = strtolower($this->time($ban['expire'], true));
+        }
 
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = 'ban';
+        $this->httpStatus = 403;
+        $this->nameTpl    = 'ban';
+#       $this->onlinePos  = 'ban';
+#       $this->robots     = 'noindex';
+        $this->titles     = __('Info');
+        $this->ban        = $ban;
+        $this->adminEmail = $this->c->config->o_admin_email;
 
-    /**
-     * HTTP статус ответа для данной страницы
-     * @var int
-     */
-    protected $httpStatus = 403;
+        return $this;
+    }
 
     /**
-     * Подготавливает данные для шаблона
-     * @param array $banned
-     * @return Page
+     * Подготовка страницы к отображению
      */
-    public function ban(array $banned)
+    public function prepare()
     {
-        $this->titles[] = __('Info');
-
-        if (! empty($banned['expire'])) {
-             $banned['expire'] = strtolower($this->time($banned['expire'], true));
-        }
-        $this->data = [
-            'banned' => $banned,
-            'adminEmail' => $this->config['o_admin_email'],
-        ];
-        return $this;
     }
 }

+ 27 - 4
app/Models/Pages/CrumbTrait.php

@@ -6,22 +6,44 @@ trait CrumbTrait
 {
     /**
      * Возвращает массив хлебных крошек
+     * 
      * @param mixed $args
+     * 
      * @return array
      */
-    protected function getCrumbs(...$args)
+    protected function crumbs(...$args)
     {
         $crumbs = [];
         $active = true;
 
         foreach ($args as $arg) {
+            if (isset($arg->forum_name)) {
+                while ($arg->id > 0) {
+                    $this->titles = $arg->forum_name;
+                    $crumbs[] = [
+                        $this->c->Router->link('Forum', ['id' => $arg->id, 'name' => $arg->forum_name]),
+                        $arg->forum_name,
+                        $active,
+                    ];
+                    $active = null;
+                    $arg = $arg->parent;
+                }
+            } else {
+                $this->titles = (string) $arg;
+                $crumbs[] = [
+                    null,
+                    (string) $arg,
+                    $active,
+                ];
+            }
+/*
             if (is_array($arg)) {
                 $cur = array_shift($arg);
                 // массив разделов
                 if (is_array($cur)) {
                     $id = $arg[0];
                     while (true) {
-                        $this->titles[] = $cur[$id]['forum_name'];
+                        $this->titles = $cur[$id]['forum_name'];
                         $crumbs[] = [
                             $this->c->Router->link('Forum', ['id' => $id, 'name' => $cur[$id]['forum_name']]),
                             $cur[$id]['forum_name'], 
@@ -48,7 +70,7 @@ trait CrumbTrait
                     } else {
                         continue;
                     }
-                    $this->titles[] = $name;
+                    $this->titles = $name;
                     $crumbs[] = [
                         $this->c->Router->link($cur, $vars),
                         $name, 
@@ -57,13 +79,14 @@ trait CrumbTrait
                 }
             // предположительно идет только название, без ссылки
             } else {
-                $this->titles[] = (string) $arg;
+                $this->titles = (string) $arg;
                 $crumbs[] = [
                     null,
                     (string) $arg,
                     $active,
                 ];
             }
+*/
             $active = null;
         }
         // главная страница

+ 20 - 29
app/Models/Pages/Debug.php

@@ -2,36 +2,20 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Page;
+
 class Debug extends Page
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'layouts/debug';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = null;
-
     /**
      * Подготавливает данные для шаблона
+     * 
      * @return Page
      */
     public function debug()
     {
-        $this->data = [
-            'time' => $this->number(microtime(true) - $this->c->START, 3),
-            'numQueries' => $this->c->DB->getCount(),
-            'memory' => $this->size(memory_get_usage()),
-            'peak' => $this->size(memory_get_peak_usage()),
-        ];
-
         if ($this->c->DEBUG > 1) {
             $total = 0;
-            $this->data['queries'] = array_map(
+            $this->queries = array_map(
                 function($a) use (&$total) {
                     $total += $a[1];
                     $a[1] = $this->number($a[1], 3);
@@ -39,29 +23,36 @@ class Debug extends Page
                 }, 
                 $this->c->DB->getQueries()
             );
-            $this->data['total'] = $this->number($total, 3);
+            $this->total = $this->number($total, 3);
         } else {
-            $this->data['queries'] = null;
+            $this->queries = null;
         }
 
+        $this->nameTpl    = 'layouts/debug';
+        $this->onlinePos  = null;
+        $this->numQueries = $this->c->DB->getCount();
+        $this->memory     = $this->size(memory_get_usage());
+        $this->peak       = $this->size(memory_get_peak_usage());
+        $this->time       = $this->number(microtime(true) - $this->c->START, 3);
+        
         return $this;
     }
 
     /**
-     * Возвращает HTTP заголовки страницы
-     * @return array
+     * Подготовка страницы к отображению
      */
-    public function httpHeaders()
+    public function prepare()
     {
-        return [];
     }
 
     /**
-     * Возвращает данные для шаблона
+     * Возвращает HTTP заголовки страницы
+     * $this->httpHeaders
+     * 
      * @return array
      */
-    public function getData()
+    protected function getHttpHeaders()
     {
-        return $this->data;
+        return [];
     }
 }

+ 34 - 68
app/Models/Pages/Forum.php

@@ -2,23 +2,13 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Page;
+
 class Forum extends Page
 {
     use ForumsTrait;
     use CrumbTrait;
 
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'forum';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = 'forum';
-
     /**
      * Подготовка данных для шаблона
      * @param array $args
@@ -29,41 +19,19 @@ class Forum extends Page
         $this->c->Lang->load('forum');
         $this->c->Lang->load('subforums');
 
-        list($fTree, $fDesc, $fAsc) = $this->c->forums;
-
-        // раздел отсутствует в доступных
-        if (empty($fDesc[$args['id']])) {
+        $forum = $this->c->forums->loadTree($args['id']);
+        if (empty($forum)) {
             return $this->c->Message->message('Bad request');
         }
 
-        $parent = isset($fDesc[$args['id']][0]) ? $fDesc[$args['id']][0] : 0;
-        $perm = $fTree[$parent][$args['id']];
-
         // редирект, если раздел это ссылка
-        if (! empty($perm['redirect_url'])) {
-            return $this->c->Redirect->setUrl($perm['redirect_url']);
-        }
-        
-        $user = $this->c->user;
-        $vars = [
-            ':fid' => $args['id'],
-            ':uid' => $user->id,
-            ':gid' => $user->groupId,
-        ];
-        if ($user->isGuest) {
-            $sql = 'SELECT f.moderators, f.num_topics, f.sort_by, 0 AS is_subscribed FROM ::forums AS f WHERE f.id=?i:fid';
-        } else {
-            $sql = 'SELECT f.moderators, f.num_topics, f.sort_by, s.user_id AS is_subscribed, mof.mf_mark_all_read FROM ::forums AS f LEFT JOIN ::forum_subscriptions AS s ON (f.id=s.forum_id AND s.user_id=?i:uid) LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid) WHERE f.id=?i:fid';
-        }
-        $curForum = $this->c->DB->query($sql, $vars)->fetch();
-
-        // нет данных по данному разделу
-        if (empty($curForum)) {
-            return $this->c->Message->message('Bad request'); //???? может в лог ошибок?
+        if (! empty($forum->redirect_url)) {
+            return $this->c->Redirect->url($forum->redirect_url);
         }
 
+        $user = $this->c->user;
         $page = isset($args['page']) ? (int) $args['page'] : 1;
-        if (empty($curForum['num_topics'])) {
+        if (empty($forum->num_topics)) {
             // попытка открыть страницу которой нет
             if ($page !== 1) {
                 return $this->c->Message->message('Bad request');
@@ -73,16 +41,16 @@ class Forum extends Page
             $offset = 0;
             $topics = null;
         } else {
-            $pages = ceil($curForum['num_topics'] / $user->dispTopics);
+            $pages = ceil($forum->num_topics / $user->disp_topics);
 
             // попытка открыть страницу которой нет
             if ($page < 1 || $page > $pages) {
                 return $this->c->Message->message('Bad request');
             }
 
-            $offset = ($page - 1) * $user->dispTopics;
+            $offset = ($page - 1) * $user->disp_topics;
 
-            switch ($curForum['sort_by']) {
+            switch ($forum->sort_by) {
                 case 1:
                     $sortBy = 'posted DESC';
                     break;
@@ -96,9 +64,9 @@ class Forum extends Page
             }
 
             $vars = [
-                ':fid' => $args['id'],
+                ':fid'    => $args['id'],
                 ':offset' => $offset,
-                ':rows' => $user->dispTopics,
+                ':rows'   => $user->disp_topics,
             ];
             $topics = $this->c->DB
                 ->query("SELECT id FROM ::topics WHERE forum_id=?i:fid ORDER BY sticky DESC, {$sortBy}, id DESC LIMIT ?i:offset, ?i:rows", $vars)
@@ -107,11 +75,11 @@ class Forum extends Page
 
         if (! empty($topics)) {
             $vars = [
-                ':uid' => $user->id,
+                ':uid'    => $user->id,
                 ':topics' => $topics,
             ];
 
-            if (! $user->isGuest && $this->config['o_show_dot'] == '1') {
+            if (! $user->isGuest && $this->c->config->o_show_dot == '1') {
                 $dots = $this->c->DB
                     ->query('SELECT topic_id FROM ::posts WHERE poster_id=?i:uid AND topic_id IN (?ai:topics) GROUP BY topic_id', $vars)
                     ->fetchAll(\PDO::FETCH_COLUMN);
@@ -121,8 +89,8 @@ class Forum extends Page
             }
 
             if (! $user->isGuest) {
-                $lower = max((int) $user->uMarkAllRead, (int) $curForum['mf_mark_all_read']);
-                $upper = max($lower, (int) $user->lastVisit);
+                $lower = max((int) $user->u_mark_all_read, (int) $forum->mf_mark_all_read);
+                $upper = max($lower, (int) $user->last_visit);
             }
 
             if ($user->isGuest) {
@@ -133,7 +101,7 @@ class Forum extends Page
             $topics = $this->c->DB->query($sql, $vars)->fetchAll();
 
             foreach ($topics as &$cur) {
-                $cur['subject'] = $this->censor($cur['subject']);
+                $cur['subject'] = $this->c->censorship->censor($cur['subject']);
                 // перенос темы
                 if ($cur['moved_to']) {
                     $cur['link'] = $this->c->Router->link('Topic', ['id' => $cur['moved_to'], 'name' => $cur['subject']]);
@@ -144,7 +112,7 @@ class Forum extends Page
                     continue;
                 }
                 // страницы темы
-                $tPages = ceil(($cur['num_replies'] + 1) / $user->dispPosts);
+                $tPages = ceil(($cur['num_replies'] + 1) / $user->disp_posts);
                 if ($tPages > 1) {
                     $cur['pages'] = $this->c->Func->paginate($tPages, -1, 'Topic', ['id' => $cur['id'], 'name' => $cur['subject']]);
                 } else {
@@ -153,7 +121,7 @@ class Forum extends Page
 
                 $cur['link'] = $this->c->Router->link('Topic', ['id' => $cur['id'], 'name' => $cur['subject']]);
                 $cur['link_last'] = $this->c->Router->link('ViewPost', ['id' => $cur['last_post_id']]);
-                $cur['views'] = $this->config['o_topic_views'] == '1' ? $this->number($cur['num_views']) : null;
+                $cur['views'] = $this->c->config->o_topic_views == '1' ? $this->number($cur['num_views']) : null;
                 $cur['replies'] = $this->number($cur['num_replies']);
                 $time = $cur['last_post'];
                 $cur['last_post'] = $this->time($cur['last_post']);
@@ -182,24 +150,22 @@ class Forum extends Page
             unset($cur);
         }
 
-        $moders = empty($curForum['moderators']) ? [] : array_flip(unserialize($curForum['moderators']));
-        $newOn = $perm['post_topics'] == 1 
-            || (null === $perm['post_topics'] && $user->gPostTopics == 1)
-            || $user->isAdmin 
+        $moders = empty($forum->moderators) ? [] : array_flip(unserialize($forum->moderators));
+        $newOn = $forum->post_topics == 1
+            || (null === $forum->post_topics && $user->g_post_topics == 1)
+            || $user->isAdmin
             || ($user->isAdmMod && isset($moders[$user->id]));
 
-        $this->onlinePos = 'forum-' . $args['id'];
-
-        $this->data = [
-            'forums' => $this->getForumsData($args['id']),
-            'topics' => $topics,
-            'crumbs' => $this->getCrumbs([$fDesc, $args['id']]),
-            'forumName' => $fDesc[$args['id']]['forum_name'],
-            'newTopic' => $newOn ? $this->c->Router->link('NewTopic', ['id' => $args['id']]) : null,
-            'pages' => $this->c->Func->paginate($pages, $page, 'Forum', ['id' => $args['id'], 'name' => $fDesc[$args['id']]['forum_name']]),
-        ];
-
-        $this->canonical = $this->c->Router->link('Forum', ['id' => $args['id'], 'name' => $fDesc[$args['id']]['forum_name'], 'page' => $page]);
+        $this->fIndex     = 'index';
+        $this->nameTpl    = 'forum';
+        $this->onlinePos  = 'forum-' . $args['id'];
+        $this->canonical  = $this->c->Router->link('Forum', ['id' => $args['id'], 'name' => $forum->forum_name, 'page' => $page]);
+        $this->forums     = $this->forumsData($args['id']);
+        $this->topics     = $topics;
+        $this->crumbs     = $this->crumbs($forum);
+        $this->forumName  = $forum->forum_name;
+        $this->newTopic   = $newOn ? $this->c->Router->link('NewTopic', ['id' => $args['id']]) : null;
+        $this->pages      = $this->c->Func->paginate($pages, $page, 'Forum', ['id' => $args['id'], 'name' => $forum->forum_name]);
 
         return $this;
     }

+ 48 - 94
app/Models/Pages/ForumsTrait.php

@@ -6,88 +6,29 @@ trait ForumsTrait
 {
     /**
      * Получение данных по разделам
-     * @param int $parent
+     * 
+     * @param int $rootId
+     * 
      * @return array
      */
-    protected function getForumsData($parent = 0)
+    protected function forumsData($rootId = 0)
     {
-        list($fTree, $fDesc, $fAsc) = $this->c->forums;
-
-        // раздел $parent не имеет подразделов для вывода или они не доступны
-        if (empty($fTree[$parent])) {
+        $root = $this->c->forums->loadTree($rootId);
+        if (empty($root)) {
             return [];
         }
 
-        $user = $this->c->user;
-
-        // текущие данные по подразделам
-        $vars = [
-            ':id' => $user->id,
-            ':forums' => array_slice($fAsc[$parent], 1),
-        ];
-        if ($user->isGuest) {
-            $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 {
-            $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_mark_all_read 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 = $stmt->fetch()) {
-            $forums[$cur['id']] = $cur;
-        }
-
-        // поиск новых
-        $new = [];
-        if (! $user->isGuest) {
-            // предварительная проверка разделов
-            $max = max((int) $user->lastVisit, (int) $user->uMarkAllRead);
-            foreach ($forums as $id => $cur) {
-                $t = max($max, (int) $cur['mf_mark_all_read']);
-                if ($cur['last_post'] > $t) {
-                    $new[$id] = $t;
-                }
-            }
-            // проверка по темам
-            if (! empty($new)) {
-                $vars = [
-                    ':id' => $user->id,
-                    ':forums' => array_keys($new),
-                    ':max' => $max,
-                ];
-                $stmt = $this->c->DB->query('SELECT t.forum_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_last_visit IS NULL OR t.last_post>mot.mt_last_visit)', $vars);
-                $tmp = [];
-                while ($cur = $stmt->fetch()) {
-                    if ($cur['last_post'] > $new[$cur['forum_id']]) {
-                        $tmp[$cur['forum_id']] = true;
-                    }
-                }
-                $new = $tmp;
-            }
-        }
-
         $r = $this->c->Router;
 
         // формированием таблицы разделов
         $result = [];
-        foreach ($fTree[$parent] as $fId => $cur) {
-            // список подразделов
-            $subForums = [];
-            if (isset($fTree[$fId])) {
-                foreach ($fTree[$fId] as $f) {
-                    $subForums[] = [
-                        $r->link('Forum', [
-                            'id' => $f['fid'],
-                            'name' => $f['forum_name']
-                        ]),
-                        $f['forum_name']
-                    ];
-                }
-            }
+        foreach ($root->subforums as $forumId => $forum) {
             // модераторы
             $moderators = [];
-            if (!empty($forums[$fId]['moderators'])) {
-                $mods = unserialize($forums[$fId]['moderators']);
+            if (! empty($forum->moderators)) {
+                $mods = unserialize($forum->moderators);
                 foreach ($mods as $name => $id) {
-                    if ($user->gViewUsers == '1') {
+                    if ($this->c->user->g_view_users == '1') {
                         $moderators[] = [
                             $r->link('User', [
                                 'id' => $id,
@@ -100,36 +41,49 @@ trait ForumsTrait
                     }
                 }
             }
+
+            // список подразделов
+            $subForums = [];
+            foreach ($forum->subforums as $subId => $subforum) {
+                $subForums[] = [
+                    $r->link('Forum', [
+                        'id'   => $subId,
+                        'name' => $subforum->forum_name,
+                    ]),
+                    $subforum->forum_name,
+                ];
+            }
+
             // статистика по разделам
-            $numT = 0;
-            $numP = 0;
-            $time = 0;
-            $postId = 0;
-            $poster = '';
-            $topic = '';
-            $fnew = false;
-            foreach ($fAsc[$fId] as $id) {
-                $fnew = $fnew || isset($new[$id]);
-                $numT += $forums[$id]['num_topics'];
-                $numP += $forums[$id]['num_posts'];
-                if ($forums[$id]['last_post'] > $time) {
-                    $time   = $forums[$id]['last_post'];
-                    $postId = $forums[$id]['last_post_id'];
-                    $poster = $forums[$id]['last_poster'];
-                    $topic  = $forums[$id]['last_topic'];
+            $numT   = (int) $forum->num_topics;
+            $numP   = (int) $forum->num_posts;
+            $time   = (int) $forum->last_post;
+            $postId = (int) $forum->last_post_id;
+            $poster = $forum->last_poster;
+            $topic  = $forum->last_topic;
+            $fnew   = $forum->newMessages;
+            foreach ($forum->descendants as $chId => $children) {
+                $fnew  = $fnew || $children->newMessages;
+                $numT += $children->num_topics;
+                $numP += $children->num_posts;
+                if ($children->last_post > $time) {
+                    $time   = $children->last_post;
+                    $postId = $children->last_post_id;
+                    $poster = $children->last_poster;
+                    $topic  = $children->last_topic;
                 }
             }
 
-            $result[$cur['cid']]['name'] = $cur['cat_name'];
-            $result[$cur['cid']]['forums'][] = [
-                'fid'          => $fId,
-                'forum_name'   => $cur['forum_name'],
-                'forum_desc'   => $forums[$fId]['forum_desc'],
+            $result[$forum->cid]['name'] = $forum->cat_name;
+            $result[$forum->cid]['forums'][] = [
+                'fid'          => $forumId,
+                'forum_name'   => $forum->forum_name,
+                'forum_desc'   => $forum->forum_desc,
                 'forum_link'   => $r->link('Forum', [
-                    'id' => $fId,
-                    'name' => $cur['forum_name']
+                    'id'   => $forumId,
+                    'name' => $forum->forum_name,
                 ]),
-                'redirect_url' => $cur['redirect_url'],
+                'redirect_url' => $forum->redirect_url,
                 'subforums'    => $subForums,
                 'moderators'   => $moderators,
                 'num_topics'   => $numT,
@@ -139,7 +93,7 @@ trait ForumsTrait
                 'last_post'    => $this->time($time),
                 'last_post_id' => $postId > 0 ? $r->link('ViewPost', ['id' => $postId]) : null,
                 'last_poster'  => $poster,
-                'last_topic'   => $topic,
+                'last_topic'   => $this->c->censorship->censor($topic),
                 'new'          => $fnew,
             ];
         }

+ 21 - 42
app/Models/Pages/Index.php

@@ -2,39 +2,16 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Page;
+
 class Index extends Page
 {
     use ForumsTrait;
     use OnlineTrait;
 
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'index';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = 'index';
-
-    /**
-     * Тип обработки пользователей онлайн
-     * @var bool
-     */
-    protected $onlineType = true;
-
-    /**
-     * Тип возврата данных при onlineType === true
-     * Если true, то из online должны вернутся только пользователи находящиеся на этой же странице
-     * Если false, то все пользователи online
-     * @var bool
-     */
-    protected $onlineFilter = false;
-
     /**
      * Подготовка данных для шаблона
+     * 
      * @return Page
      */
     public function view()
@@ -42,30 +19,32 @@ class Index extends Page
         $this->c->Lang->load('index');
         $this->c->Lang->load('subforums');
 
-        $stats = $this->c->users_info;
-
-        $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 = [];
+        $stats['total_users']  = $this->number($this->c->stats->userTotal);
+        $stats['total_posts']  = $this->number($this->c->stats->postTotal);
+        $stats['total_topics'] = $this->number($this->c->stats->topicTotal);
 
-        $stats['total_users'] = $this->number($stats['total_users']);
-
-        if ($this->c->user->gViewUsers == '1') {
+        if ($this->c->user->g_view_users == '1') {
             $stats['newest_user'] = [
                 $this->c->Router->link('User', [
-                    'id' => $stats['last_user']['id'],
-                    'name' => $stats['last_user']['username'],
+                    'id'   => $this->c->stats->userLast['id'],
+                    'name' => $this->c->stats->userLast['username'],
                 ]),
-                $stats['last_user']['username']
+                $this->c->stats->userLast['username']
             ];
         } else {
-            $stats['newest_user'] = $stats['last_user']['username'];
+            $stats['newest_user'] = $this->c->stats->userLast['username'];
         }
-        $this->data['stats'] = $stats;
-        $this->data['online'] = $this->getUsersOnlineInfo();
-        $this->data['forums'] = $this->getForumsData();
 
-        $this->canonical = $this->c->Router->link('Index');
-        
+        $this->nameTpl      = 'index';
+        $this->onlinePos    = 'index';
+        $this->onlineType   = true;
+        $this->onlineFilter = false;
+        $this->canonical    = $this->c->Router->link('Index');
+        $this->stats        = $stats;
+        $this->online       = $this->usersOnlineInfo();
+        $this->forums       = $this->forumsData();
+
         return $this;
     }
 }

+ 99 - 90
app/Models/Pages/Install.php

@@ -4,29 +4,14 @@ namespace ForkBB\Models\Pages;
 
 use ForkBB\Core\Container;
 use ForkBB\Core\Validator;
+use ForkBB\Models\Page;
 use PDO;
 use PDOException;
 use RuntimeException;
 
 class Install extends Page
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'layouts/install';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = null;
-
-    /**
-     * Переменная для meta name="robots"
-     * @var string
-     */
-    protected $robots = 'noindex';
+    const PHP_MIN = '5.6.0';
 
     /**
      * Для MySQL
@@ -36,33 +21,30 @@ class Install extends Page
 
     /**
      * Конструктор
+     * 
      * @param Container $container
      */
     public function __construct(Container $container)
     {
         $this->c = $container;
-        $this->config = $container->config;
-        $container->Lang->load('common', $this->config['o_default_lang']);
+        $container->Lang->load('common', $container->config->o_default_lang);
     }
 
     /**
-     * Возвращает данные для шаблона
-     * @return array
+     * Подготовка страницы к отображению
      */
-    public function getData()
+    public function prepare()
     {
-        return $this->data + [
-            'pageHeads' => $this->pageHeads(),
-            'fIswev' => $this->getIswev(),
-        ];
     }
 
     /**
      * Возращает типы БД поддерживаемые PDO
+     * 
      * @param string $curType
+     * 
      * @return array
      */
-    protected function getDBTypes($curType = null)
+    protected function DBTypes($curType = null)
     {
         $dbTypes = [];
         $pdoDrivers = PDO::getAvailableDrivers();
@@ -93,15 +75,47 @@ class Install extends Page
 
     /**
      * Подготовка данных для страницы установки форума
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function install(array $args)
     {
+        $this->nameTpl    = 'layouts/install';
+        $this->onlinePos  = null;
+        $this->robots     = 'noindex';
+        $this->rev        = $this->c->FORK_REVISION;
+        $this->formAction = $this->c->Router->link('Install');
+
+        // версия PHP
+        if (version_compare(PHP_VERSION, self::PHP_MIN, '<')) {
+            $this->a['fIswev']['e'][] = __('You are running error', 'PHP', PHP_VERSION, $this->c->FORK_REVISION, self::PHP_MIN);
+        }
+
+        // доступность папок на запись
+        $folders = [
+            $this->c->DIR_CONFIG,
+            $this->c->DIR_CACHE,
+            $this->c->DIR_PUBLIC . '/avatar',
+        ];
+        foreach ($folders as $folder) {
+            if (! is_writable($folder)) {
+                $this->a['fIswev']['e'][] = __('Alert folder', $folder);
+            }
+        }
+
+        // доступность шаблона конфигурации
+        $config = file_get_contents($this->c->DIR_CONFIG . '/main.dist.php');
+        if (false === $config) {
+            $this->a['fIswev']['e'][] = __('No access to main.dist.php');
+        }
+        unset($config);
+
         // язык
         $langs = $this->c->Func->getLangs();
         if (empty($langs)) {
-            $this->iswev['e'][] = 'No language pack.';
+            $this->a['fIswev']['e'][] = 'No language pack.';
             $installLang = $installLangs = $defaultLangs = 'English';
         } else {
             if (isset($args['installlang'])) {
@@ -123,27 +137,14 @@ class Install extends Page
             }
 
         }
-        unset($args['installlang']);
-        // версия PHP
-        $phpMin = '5.6.0';
-        if (version_compare(PHP_VERSION, $phpMin, '<')) {
-            $this->iswev['e'][] = __('You are running error', 'PHP', PHP_VERSION, $this->c->FORK_REVISION, $phpMin);
-        }
-        // доступность папок на запись
-        $folders = [
-            $this->c->DIR_CONFIG,
-            $this->c->DIR_CACHE,
-            $this->c->DIR_PUBLIC . '/avatar',
-        ];
-        foreach ($folders as $folder) {
-            if (! is_writable($folder)) {
-                $this->iswev['e'][] = __('Alert folder', $folder);
-            }
-        }
+        $this->installLangs = $installLangs;
+        $this->installLang  = $installLang;
+        $this->defaultLangs = $defaultLangs;
+
         // стиль
         $styles = $this->c->Func->getStyles();
         if (empty($styles)) {
-            $this->iswev['e'][] = __('No styles');
+            $this->a['fIswev']['e'][] = __('No styles');
             $defaultStyles = ['ForkBB'];
         } else {
             $defaultStyles = [];
@@ -152,43 +153,46 @@ class Install extends Page
                 $defaultStyles[] = $style == $defStyle ? [$style, 1] : [$style];
             }
         }
-        unset($args['defaultstyle']);
+        $this->defaultStyles = $defaultStyles;
+        
         // типы БД
-        $dbTypes = $this->getDBTypes(isset($args['dbtype']) ? $args['dbtype'] : null);
+        $dbTypes = $this->DBTypes(isset($args['dbtype']) ? $args['dbtype'] : null);
         if (empty($dbTypes)) {
-            $this->iswev['e'][] = __('No DB extensions');
+            $this->a['fIswev']['e'][] = __('No DB extensions');
         }
-        unset($args['dbtype']);
-        // доступность шаблона конфигурации
-        $config = file_get_contents($this->c->DIR_CONFIG . '/main.dist.php');
-        if (false === $config) {
-            $this->iswev['e'][] = __('No access to main.dist.php');
+        $this->dbTypes = $dbTypes;
+        
+
+        $this->a = $this->a + $args; //????
+
+        if (empty($args)) {
+            $this->dbhost   = 'localhost';
+            $this->dbname   = '';
+            $this->dbuser   = '';
+            $this->dbprefix = '';
+            $this->username = '';
+            $this->email    = '';
+            $this->title    = __('My ForkBB Forum');
+            $this->descr    = __('Description');
+            $this->baseurl  = $this->c->BASE_URL;
+        } else {
+            $this->dbhost   = $args['dbhost'];
+            $this->dbname   = $args['dbname'];
+            $this->dbuser   = $args['dbuser'];
+            $this->dbprefix = $args['dbprefix'];
+            $this->username = $args['username'];
+            $this->email    = $args['email'];
+            $this->title    = $args['title'];
+            $this->descr    = $args['descr'];
+            $this->baseurl  = $args['baseurl'];
         }
-        unset($config);
 
-        $this->data = $args + [
-            'rev' => $this->c->FORK_REVISION,
-            'formAction' => $this->c->Router->link('Install'),
-            'installLangs' => $installLangs,
-            'installLang' => $installLang,
-            'dbTypes' => $dbTypes,
-            'dbhost' => 'localhost',
-            'dbname' => '',
-            'dbuser' => '',
-            'dbprefix' => '',
-            'username' => '',
-            'email' => '',
-            'title' => __('My ForkBB Forum'),
-            'descr' => __('Description'),
-            'baseurl' => $this->c->BASE_URL,
-            'defaultLangs' => $defaultLangs,
-            'defaultStyles' => $defaultStyles,
-        ];
         return $this;
     }
 
     /**
      * Начальная стадия установки
+     * 
      * @return Page
      */
     public function installPost()
@@ -214,7 +218,7 @@ class Install extends Page
             'rtrim_url'    => [$this, 'vRtrimURL']
         ])->setRules([
             'installlang' => 'string:trim',
-            'dbtype' => ['required|string:trim|in:' . implode(',', array_keys($this->getDBTypes())), __('Database type')],
+            'dbtype' => ['required|string:trim|in:' . implode(',', array_keys($this->DBTypes())), __('Database type')],
             'dbhost' => ['required|string:trim|check_host', __('Database server hostname')],
             'dbname' => ['required|string:trim', __('Database name')],
             'dbuser' => ['string:trim', __('Database username')],
@@ -235,50 +239,53 @@ class Install extends Page
         if ($v->validation($_POST)) {
             return $this->installEnd($v);
         } else {
-            $this->iswev = $v->getErrors();
+            $this->fIswev = $v->getErrors();
             return $this->install($v->getData());
         }
     }
 
     /**
      * Обработка base URL
+     * 
      * @param Validator $v
      * @param string $url
-     * @param int $type
+     * 
      * @return array
      */
-    public function vRtrimURL(Validator $v, $url, $type)
+    public function vRtrimURL(Validator $v, $url)
     {
-        return [rtrim($url, '/'), $type, false];
+        return [rtrim($url, '/'), true];
     }
 
     /**
      * Дополнительная проверка префикса
+     * 
      * @param Validator $v
      * @param string $prefix
-     * @param int $type
+     * 
      * @return array
      */
-    public function vCheckPrefix(Validator $v, $prefix, $type)
+    public function vCheckPrefix(Validator $v, $prefix)
     {
-        $error = false;
+        $error = true;
         if (strlen($prefix) == 0) {
         } elseif (! preg_match('%^[a-z][a-z\d_]*$%i', $prefix)) {
             $error = __('Table prefix error', $prefix);
         } elseif ($v->dbtype == 'sqlite' && strtolower($prefix) == 'sqlite_') {
             $error = __('Prefix reserved');
         }
-        return [$prefix, $type, $error];
+        return [$prefix, $error];
     }
 
     /**
      * Полная проверка подключения к БД
+     * 
      * @param Validator $v
      * @param string $dbhost
-     * @param int $type
+     * 
      * @return array
      */
-    public function vCheckHost(Validator $v, $dbhost, $type)
+    public function vCheckHost(Validator $v, $dbhost)
     {
         $this->c->DB_USERNAME = $v->dbuser;
         $this->c->DB_PASSWORD = $v->dbpass;
@@ -287,7 +294,7 @@ class Install extends Page
         $dbname = $v->dbname;
         // есть ошибки, ни чего не проверяем
         if (! empty($v->getErrors())) {
-            return [$dbhost, $type, false];
+            return [$dbhost, true];
         }
         // настройки подключения БД
         $DBEngine = 'MyISAM';
@@ -314,13 +321,13 @@ class Install extends Page
         try {
             $stat = $this->c->DB->statistics();
         } catch (PDOException $e) {
-            return [$dbhost, $type, $e->getMessage()];
+            return [$dbhost, $e->getMessage()];
         }
         // проверка наличия таблицы пользователей в БД
         try {
             $stmt = $this->c->DB->query('SELECT 1 FROM ::users WHERE id=1 LIMIT 1');
             if (! empty($stmt->fetch())) {
-                return [$dbhost, $type, __('Existing table error', $v->dbprefix, $v->dbname)];
+                return [$dbhost, __('Existing table error', $v->dbprefix, $v->dbname)];
             }
         } catch (PDOException $e) {
             // все отлично, таблица пользователей не найдена
@@ -329,12 +336,14 @@ class Install extends Page
         if (isset($stat['character_set_database']) && $stat['character_set_database'] == 'utf8') {
             $this->c->DB_DSN = str_replace('charset=utf8mb4', 'charset=utf8', $this->c->DB_DSN);
         }
-        return [$dbhost, $type, false];
+        return [$dbhost, true];
     }
 
     /**
      * Завершение установки форума
+     * 
      * @param Validator $v
+     * 
      * @return Page
      */
     protected function installEnd(Validator $v)
@@ -1002,7 +1011,7 @@ class Install extends Page
             throw new RuntimeException('No access to main.dist.php.');
         }
 
-        $repl = [
+        $repl = [ //????
             '_BASE_URL_'      => $v->baseurl,
             '_DB_DSN_'        => $this->c->DB_DSN,
             '_DB_USERNAME_'   => $this->c->DB_USERNAME,
@@ -1016,7 +1025,7 @@ class Install extends Page
         }
         $result = file_put_contents($this->c->DIR_CONFIG . '/main.php', $config);
         if (false === $result) {
-            throw new RuntimeException('No write to main.php.');
+            throw new RuntimeException('No write to main.php');
         }
 
         return $this->c->Redirect->toIndex();

+ 14 - 45
app/Models/Pages/Maintenance.php

@@ -3,65 +3,34 @@
 namespace ForkBB\Models\Pages;
 
 use ForkBB\Core\Container;
+use ForkBB\Models\Page;
 
 class Maintenance extends Page
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'maintenance';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = null;
-
-    /**
-     * HTTP статус ответа для данной страницы
-     * @var int
-     */
-    protected $httpStatus = 503;
-
     /**
      * Конструктор
+     * 
      * @param Container $container
      */
     public function __construct(Container $container)
     {
-        $this->c = $container;
-        $this->config = $container->config;
-        $container->Lang->load('common', $this->config['o_default_lang']);
-    }
+        $container->Lang->load('common', $container->config->o_default_lang);
 
-    /**
-     * Возвращает флаг готовности данных
-     * @return bool
-     */
-    public function isReady()
-    {
-        return true;
+        parent::__construct($container);
+
+        $this->httpStatus         = 503;
+        $this->nameTpl            = 'maintenance';
+#       $this->onlinePos          = null; //????
+#       $this->robots             = 'noindex';
+        $this->titles             = __('Maintenance');
+#       $this->fNavigation        = null; //????
+        $this->maintenanceMessage = $this->c->config->o_maintenance_message;
     }
 
     /**
-     * Возвращает данные для шаблона
-     * @return array
+     * Подготовка страницы к отображению
      */
-    public function getData()
+    public function prepare()
     {
-        $this->titles[] = __('Maintenance');
-        return [
-            'maintenanceMessage' => $this->config['o_maintenance_message'],
-            'pageTitle' => $this->pageTitle(),
-            'pageHeaders' => $this->pageHeaders(),
-            'fTitle' => $this->config['o_board_title'],
-            'fDescription' => $this->config['o_board_desc'],
-            'fNavigation' => null,
-            'fIndex' => $this->index,
-            'fAnnounce' => null,
-            'fRootLink' => $this->c->Router->link('Index'),
-            'fIswev' => null,
-        ];
     }
 }

+ 12 - 9
app/Models/Pages/OnlineTrait.php

@@ -8,19 +8,22 @@ trait OnlineTrait
      * Получение информации об онлайн посетителях
      * @return null|array
      */
-    protected function getUsersOnlineInfo() 
+    protected function usersOnlineInfo() 
     {
-        if ($this->config['o_users_online'] == '1') {
+        if ($this->c->config->o_users_online == '1') {
+            // данные онлайн посетителей
+            $this->c->Online->calc($this);
+            $users  = $this->c->Online->users; //????
+            $guests = $this->c->Online->guests;
+            $bots   = $this->c->Online->bots;
+
+            $list   = [];
             $data = [
-                'max'      => $this->number($this->config['st_max_users']),
-                'max_time' => $this->time($this->config['st_max_users_time']),
+                'max'      => $this->number($this->c->config->st_max_users),
+                'max_time' => $this->time($this->c->config->st_max_users_time),
             ];
 
-            // данные онлайн посетителей
-            list($users, $guests, $bots) = $this->c->Online->handle($this);
-            $list = [];
-
-            if ($this->c->user->gViewUsers == '1') {
+            if ($this->c->user->g_view_users == '1') {
                 foreach ($users as $id => $cur) {
                     $list[] = [
                         $this->c->Router->link('User', [

+ 68 - 0
app/Models/Pages/Post.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+class Post extends Page
+{
+    use UsersTrait;
+    use OnlineTrait;
+    use CrumbTrait;
+
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'post';
+
+    /**
+     * Позиция для таблицы онлайн текущего пользователя
+     * @var null|string
+     */
+    protected $onlinePos = 'post';
+
+    /**
+     * Данные по текущей теме
+     * @var array
+     */
+    protected $topic;
+
+    /**
+     * Подготовка данных для шаблона
+     * @param array $args
+     * @return Page
+     */
+    public function newTopic(array $args)
+    {
+        list($fTree, $fDesc, $fAsc) = $this->c->forums;
+
+        // раздел отсутствует в доступных
+        if (empty($fDesc[$args['id']])) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        $parent = isset($fDesc[$args['id']][0]) ? $fDesc[$args['id']][0] : 0;
+        $perm = $fTree[$parent][$args['id']];
+
+        // раздел является ссылкой
+        if (null !== $perm['redirect_url']) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        $vars = [':fid' => $args['id']];
+        $sql = 'SELECT f.* FROM ::forums AS f WHERE f.id=?i:fid';
+
+        $forum = $this->c->DB->query($sql, $vars)->fetch();
+        $user = $this->c->user;
+
+        $moders = empty($forum['moderators']) ? [] : array_flip(unserialize($forum['moderators']));
+
+        if (! $user->isAdmin
+            && (! $user->isAdmMod || ! isset($moders[$user->id]))
+            && (null === $perm['post_topics'] && $user->g_post_topics == '0' || $perm['post_topics'] == '0')
+        ) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        return $this;
+    }
+}

+ 26 - 52
app/Models/Pages/Redirect.php

@@ -2,57 +2,29 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Page;
+
 class Redirect extends Page
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = null;
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = null;
-
-    /**
-     * Адрес перехода
-     * @var string
-     */
-    protected $link;
-
-    /**
-     * Переменная для meta name="robots"
-     * @var string
-     */
-    protected $robots = 'noindex';
-
-    /**
-     * Возвращает флаг готовности данных
-     * @return bool
-     */
-    public function isReady()
-    {
-        return ! empty($this->link);
-    }
-
     /**
      * Перенаправление на главную страницу форума
+     * 
      * @return Page
      */
     public function toIndex()
     {
-        return $this->setPage('Index')->setMessage(__('Redirecting to index'));
+        return $this->page('Index')->message(__('Redirecting to index'));
     }
 
     /**
      * Задает адрес перехода
+     * 
      * @param string $marker
      * @param array $args
+     * 
      * @return Page
      */
-    public function setPage($marker, array $args = [])
+    public function page($marker, array $args = [])
     {
         $this->link = $this->c->Router->link($marker, $args);
         return $this;
@@ -60,10 +32,12 @@ class Redirect extends Page
 
     /**
      * Задает ссылку для перехода
+     * 
      * @param string $url
+     * 
      * @return Page
      */
-    public function setUrl($url)
+    public function url($url)
     {
         $this->link = $url;
         return $this;
@@ -71,47 +45,47 @@ class Redirect extends Page
 
     /**
      * Задает сообщение
+     * 
      * @param string $message
+     * 
      * @return Page
      */
-    public function setMessage($message)
+    public function message($message)
     {
         // переадресация без вывода сообщения
-        if ($this->config['o_redirect_delay'] == '0') {
+        if ($this->c->config->o_redirect_delay == '0') {
             return $this;
         }
 
         $this->nameTpl = 'layouts/redirect';
-        $this->titles[] = __('Redirecting');
-        $this->data = [
-            'message' => $message,
-            'timeout' => (int) $this->config['o_redirect_delay'],  //???? перенести в заголовки?
-        ];
+        $this->titles  = __('Redirecting');
+        $this->robots  = 'noindex';
+        $this->message = $message;
+        $this->timeout = (int) $this->c->config->o_redirect_delay;  //???? перенести в заголовки?
+
         return $this;
     }
 
     /**
      * Возвращает HTTP заголовки страницы
+     * $this->httpHeaders
+     * 
      * @return array
      */
-    public function httpHeaders()
+    protected function getHttpHeaders()
     {
-        // переадресация без вывода сообщения
-        if (empty($this->data)) {
+        if (null === $this->nameTpl) {
             $this->httpHeaders = [
                 'Location: ' . $this->link, //????
             ];
         }
-        return parent::httpHeaders();
+        return parent::getHttpHeaders();
     }
 
     /**
-     * Возвращает данные для шаблона
-     * @return array
+     * Подготовка страницы к отображению
      */
-    public function getData()
+    public function prepare()
     {
-        $this->data['link'] = $this->link;
-        return parent::getData();
     }
 }

+ 85 - 92
app/Models/Pages/Register.php

@@ -4,37 +4,15 @@ namespace ForkBB\Models\Pages;
 
 use ForkBB\Core\Validator;
 use ForkBB\Core\Exceptions\MailException;
+use ForkBB\Models\Page;
 use ForkBB\Models\User;
 
 class Register extends Page
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'register';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = 'register';
-
-    /**
-     * Указатель на активный пункт навигации
-     * @var string
-     */
-    protected $index = 'register';
-
-    /**
-     * Переменная для meta name="robots"
-     * @var string
-     */
-    protected $robots = 'noindex';
-
     /**
      * Обработчик регистрации
-     * @retrun Page
+     * 
+     * @return Page
      */
     public function reg()
     {
@@ -62,40 +40,47 @@ class Register extends Page
             return $this->regEnd($v);
         }
 
-        $this->iswev = $v->getErrors();
+        $this->fIswev = $v->getErrors();
 
         // нет согласия с правилами
-        if (isset($this->iswev['cancel'])) {
-            return $this->c->Redirect->setPage('Index')->setMessage(__('Reg cancel redirect'));
+        if (isset($this->fIswev['cancel'])) {
+            return $this->c->Redirect->page('Index')->message(__('Reg cancel redirect'));
         }
 
-        $this->titles[] = __('Register');
-        $this->data = [
-            'formAction' => $this->c->Router->link('RegisterForm'),
-            'formToken' => $this->c->Csrf->create('RegisterForm'),
-            'agree' => $v->agree,
-            'on' => '1',
-            'email' => $v->email,
-            'username' => $v->username,
-        ];
+        $this->fIndex     = 'register';
+        $this->nameTpl    = 'register';
+        $this->onlinePos  = 'register';
+        $this->titles     = __('Register');
+        $this->robots     = 'noindex';
+        $this->formAction = $this->c->Router->link('RegisterForm');
+        $this->formToken  = $this->c->Csrf->create('RegisterForm');
+        $this->agree      = $v->agree;
+        $this->on         = '1';
+        $this->email      = $v->email;
+        $this->username  = $v->username;
 
         return $this;
     }
 
     /**
      * Дополнительная проверка email
+     * 
      * @param Validator $v
-     * @param string $username
+     * @param string $email
+     * 
      * @return array
      */
     public function vCheckEmail(Validator $v, $email)
     {
         $error = false;
+        $user = $this->c->ModelUser;
+        $user->__email = $email;
+
         // email забанен
-        if ($this->c->CheckBans->isBanned(null, $email) > 0) {
+        if ($this->c->bans->isBanned($user) > 0) {
             $error = __('Banned email');
         // найден хотя бы 1 юзер с таким же email
-        } elseif (empty($v->getErrors()) && $this->c->UserMapper->getUser($email, 'email') !== 0) {
+        } elseif (empty($v->getErrors()) && $user->load($email, 'email') !== 0) {
             $error = __('Dupe email');
         }
         return [$email, $error];
@@ -103,25 +88,30 @@ class Register extends Page
 
     /**
      * Дополнительная проверка username
+     * 
      * @param Validator $v
      * @param string $username
+     * 
      * @return array
      */
     public function vCheckUsername(Validator $v, $username)
     {
         $username = preg_replace('%\s+%su', ' ', $username);
         $error = false;
+        $user = $this->c->ModelUser;
+        $user->__username = $username;
+
         // username = Гость
         if (preg_match('%^(guest|' . preg_quote(__('Guest'), '%') . ')$%iu', $username)) {
             $error = __('Username guest');
         // цензура
-        } elseif ($this->censor($username) !== $username) {
+        } elseif ($this->c->censorship->censor($username) !== $username) {
             $error = __('Username censor');
         // username забанен
-        } elseif ($this->c->CheckBans->isBanned($username) > 0) {
+        } elseif ($this->c->bans->isBanned($user) > 0) {
             $error = __('Banned username');
         // есть пользователь с похожим именем
-        } elseif (empty($v->getErrors()) && ! $this->c->UserMapper->isUnique($username)) {
+        } elseif (empty($v->getErrors()) && ! $user->isUnique()) {
             $error = __('Username not unique');
         }
         return [$username, $error];
@@ -129,40 +119,50 @@ class Register extends Page
 
     /**
      * Завершение регистрации
+     * 
      * @param array @data
+     * 
      * @return Page
      */
     protected function regEnd(Validator $v)
     {
-        if ($this->config['o_regs_verify'] == '1') {
+        if ($this->c->config->o_regs_verify == '1') {
             $groupId = $this->c->GROUP_UNVERIFIED;
             $key = 'w' . $this->c->Secury->randomPass(79);
         } else {
-            $groupId = $this->config['o_default_user_group'];
+            $groupId = $this->c->config->o_default_user_group;
             $key = null;
         }
 
-        $newUserId = $this->c->UserMapper->newUser(new User([
-            'group_id' => $groupId,
-            'username' => $v->username,
-            'password' => password_hash($v->password, PASSWORD_DEFAULT),
-            'email' => $v->email,
-            'email_confirmed' => 0,
-            'activate_string' => $key,
-            'u_mark_all_read' => time(),
-        ], $this->c));
+        $user = $this->c->ModelUser;
+        $user->username        = $v->username;
+        $user->password        = password_hash($v->password, PASSWORD_DEFAULT);
+        $user->group_id        = $groupId;
+        $user->email           = $v->email;
+        $user->email_confirmed = 0;
+        $user->activate_string = $key;
+        $user->u_mark_all_read = time();
+        $user->email_setting   = $this->c->config->o_default_email_setting;
+        $user->timezone        = $this->c->config->o_default_timezone;
+        $user->dst             = $this->c->config->o_default_dst;
+        $user->language        = $user->language;
+        $user->style           = $user->style;
+        $user->registered      = time();
+        $user->registration_ip = $this->c->user->ip;
+            
+        $newUserId = $user->insert();
 
         // обновление статистики по пользователям
-        if ($this->config['o_regs_verify'] != '1') {
+        if ($this->c->config->o_regs_verify != '1') {
             $this->c->{'users_info update'};
         }
 
         // уведомление о регистрации
-        if ($this->config['o_regs_report'] == '1' && $this->config['o_mailing_list'] != '') {
+        if ($this->c->config->o_regs_report == '1' && $this->c->config->o_mailing_list != '') {
             $tplData = [
-                'fTitle' => $this->config['o_board_title'],
+                'fTitle' => $this->c->config->o_board_title,
                 'fRootLink' => $this->c->Router->link('Index'),
-                'fMailer' => __('Mailer', $this->config['o_board_title']),
+                'fMailer' => __('Mailer', $this->c->config->o_board_title),
                 'username' => $v->username,
                 'userLink' => $this->c->Router->link('User', ['id' => $newUserId, 'name' => $v->username]),
             ];
@@ -171,9 +171,9 @@ class Register extends Page
                 $this->c->Mail
                     ->reset()
                     ->setFolder($this->c->DIR_LANG)
-                    ->setLanguage($this->config['o_default_lang'])
-                    ->setTo($this->config['o_mailing_list'])
-                    ->setFrom($this->config['o_webmaster_email'], __('Mailer', $this->config['o_board_title']))
+                    ->setLanguage($this->c->config->o_default_lang)
+                    ->setTo($this->c->config->o_mailing_list)
+                    ->setFrom($this->c->config->o_webmaster_email, __('Mailer', $this->c->config->o_board_title))
                     ->setTpl('new_user.tpl', $tplData)
                     ->send();
             } catch (MailException $e) {
@@ -184,13 +184,13 @@ class Register extends Page
         $this->c->Lang->load('register');
 
         // отправка письма активации аккаунта
-        if ($this->config['o_regs_verify'] == '1') {
+        if ($this->c->config->o_regs_verify == '1') {
             $hash = $this->c->Secury->hash($newUserId . $key);
             $link = $this->c->Router->link('RegActivate', ['id' => $newUserId, 'key' => $key, 'hash' => $hash]);
             $tplData = [
-                'fTitle' => $this->config['o_board_title'],
+                'fTitle' => $this->c->config->o_board_title,
                 'fRootLink' => $this->c->Router->link('Index'),
-                'fMailer' => __('Mailer', $this->config['o_board_title']),
+                'fMailer' => __('Mailer', $this->c->config->o_board_title),
                 'username' => $v->username,
                 'link' => $link,
             ];
@@ -201,7 +201,7 @@ class Register extends Page
                     ->setFolder($this->c->DIR_LANG)
                     ->setLanguage($this->c->user->language)
                     ->setTo($v->email)
-                    ->setFrom($this->config['o_webmaster_email'], __('Mailer', $this->config['o_board_title']))
+                    ->setFrom($this->c->config->o_webmaster_email, __('Mailer', $this->c->config->o_board_title))
                     ->setTpl('welcome.tpl', $tplData)
                     ->send();
             } catch (MailException $e) {
@@ -210,56 +210,49 @@ class Register extends Page
 
             // письмо активации аккаунта отправлено
             if ($isSent) {
-                return $this->c->Message->message(__('Reg email', $this->config['o_admin_email']), false, 200);
+                return $this->c->Message->message(__('Reg email', $this->c->config->o_admin_email), false, 200);
             // форма сброса пароля
             } else {
-                return $this->c->Auth->setIswev([
-                    'w' => [
-                        __('Error welcom mail', $this->config['o_admin_email']),
-                    ],
-                ])->forget([
-                    '_email' => $v->email,
-                ]);
+                $auth = $this->c->Auth;
+                $auth->fIswev = ['w' => [__('Error welcom mail', $this->c->config->o_admin_email)]];
+                return $auth->forget(['_email' => $v->email]);
             }
         // форма логина
         } else {
-            return $this->c->Auth->setIswev([
-                's' => [
-                    __('Reg complete'),
-                ],
-            ])->login([
-                '_username' => $v->username,
-            ]);
+            $auth = $this->c->Auth;
+            $auth->fIswev = ['s' => [__('Reg complete')]];
+            return $auth->login(['_username' => $v->username]);
         }
     }
 
     /**
      * Активация аккаунта
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function activate(array $args)
     {
         if (! hash_equals($args['hash'], $this->c->Secury->hash($args['id'] . $args['key']))
-            || ! ($user = $this->c->UserMapper->getUser($args['id'])) instanceof User
-            || empty($user->activateString)
-            || $user->activateString{0} !== 'w'
-            || ! hash_equals($user->activateString, $args['key'])
+            || ! ($user = $this->c->ModelUser->load($args['id'])) instanceof User
+            || empty($user->activate_string)
+            || $user->activate_string{0} !== 'w'
+            || ! hash_equals($user->activate_string, $args['key'])
         ) {
             return $this->c->Message->message(__('Bad request'), false);
         }
 
-        $this->c->UserMapper->updateUser($user->id, ['group_id' => $this->config['o_default_user_group'], 'email_confirmed' => 1, 'activate_string' => null]);
+        $user->group_id = $this->c->config->o_default_user_group;
+        $user->email_confirmed = 1;
+        $user->activate_string = null;
+        $user->update();
         $this->c->{'users_info update'};
 
         $this->c->Lang->load('register');
 
-        return $this->c->Auth->setIswev([
-            's' => [
-                __('Reg complete'),
-            ],
-        ])->login([
-            '_username' => $user->username,
-        ]);
+        $auth = $this->c->Auth;
+        $auth->fIswev = ['s' => [__('Reg complete')]];
+        return $auth->login(['_username' => $v->username]);
     }
 }

+ 23 - 38
app/Models/Pages/Rules.php

@@ -2,64 +2,49 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Page;
+
 class Rules extends Page
 {
-    /**
-     * Имя шаблона
-     * @var string
-     */
-    protected $nameTpl = 'rules';
-
-    /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
-     */
-    protected $onlinePos = 'rules';
-
-    /**
-     * Указатель на активный пункт навигации
-     * @var string
-     */
-    protected $index = 'rules';
-
     /**
      * Подготавливает данные для шаблона
+     * 
      * @return Page
      */
     public function view()
     {
-        $this->titles[] = __('Forum rules');
-        $this->data = [
-            'title' => __('Forum rules'),
-            'rules' => $this->config['o_rules_message'],
-            'formAction' => null,
-        ];
-
-        $this->canonical = $this->c->Router->link('Rules');
+        $this->fIndex     = 'rules';
+        $this->nameTpl    = 'rules';
+        $this->onlinePos  = 'rules';
+        $this->canonical  = $this->c->Router->link('Rules');
+        $this->titles     = __('Forum rules');
+        $this->title      = __('Forum rules');
+        $this->rules      = $this->c->config->o_rules_message;
+        $this->formAction = null;
 
         return $this;
     }
 
     /**
      * Подготавливает данные для шаблона
+     * 
      * @return Page
      */
     public function confirmation()
     {
-        $this->index = 'register';
-        $this->robots = 'noindex';
         $this->c->Lang->load('register');
 
-        $this->titles[] = __('Forum rules');
-        $this->data = [
-            'title' => __('Forum rules'),
-            'rules' => $this->config['o_rules'] == '1' ?
-                $this->config['o_rules_message']
-                : __('If no rules'),
-            'formAction' => $this->c->Router->link('RegisterForm'),
-            'formToken' => $this->c->Csrf->create('RegisterForm'),
-            'formHash' => $this->c->Csrf->create('Register'),
-        ];
+        $this->fIndex     = 'register';
+        $this->nameTpl    = 'rules';
+        $this->onlinePos  = 'rules';
+        $this->robots     = 'noindex';
+        $this->titles     = __('Forum rules');
+        $this->title      = __('Forum rules');
+        $this->rules      = $this->c->config->o_rules == '1' ? $this->c->config->o_rules_message : __('If no rules');
+        $this->formAction = $this->c->Router->link('RegisterForm');
+        $this->formToken  = $this->c->Csrf->create('RegisterForm');
+        $this->formHash   = $this->c->Csrf->create('Register');
+
         return $this;
     }
 }

+ 243 - 162
app/Models/Pages/Topic.php

@@ -2,6 +2,8 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Page;
+
 class Topic extends Page
 {
     use UsersTrait;
@@ -9,60 +11,104 @@ class Topic extends Page
     use CrumbTrait;
 
     /**
-     * Имя шаблона
-     * @var string
+     * Данные по текущей теме
+     * @var array
      */
-    protected $nameTpl = 'topic';
+    protected $curTopic;
 
     /**
-     * Позиция для таблицы онлайн текущего пользователя
-     * @var null|string
+     * Переход к первому новому сообщению темы (или в конец)
+     * 
+     * @param array $args
+     * 
+     * @return Page
      */
-    protected $onlinePos = 'topic';
+    public function viewNew(array $args)
+    {
+        $topic = $this->curTopic($args['id']); 
+        if (false === $topic) {
+            return $this->c->Message->message('Bad request');
+        }
 
-    /**
-     * Тип обработки пользователей онлайн
-     * Если false, то идет обновление данных
-     * Если true, то идет возврат данных (смотрите $onlineFilter)
-     * @var bool
-     */
-    protected $onlineType = true;
+        if (! $this->c->user->isGuest) {
+            $upper = max(
+                (int) $this->c->user->last_visit,
+                (int) $this->c->user->u_mark_all_read,
+                (int) $topic['mf_mark_all_read'],
+                (int) $topic['mt_last_visit']
+            );
+
+            if ($upper < $topic['last_post']) {
+                $vars = [
+                    ':tid' => $args['id'],
+                    ':visit' => $upper,
+                ];
+                $sql = 'SELECT MIN(id) FROM ::posts WHERE topic_id=?i:tid AND posted>?i:visit';
 
-    /**
-     * Тип возврата данных при onlineType === true
-     * Если true, то из online должны вернутся только пользователи находящиеся на этой же странице
-     * Если false, то все пользователи online
-     * @var bool
-     */
-    protected $onlineFilter = true;
+                $pid = $this->c->DB->query($sql, $vars)->fetchColumn();
 
-    /**
-     * Подготовка данных для шаблона
-     * @param array $args
-     * @return Page
-     */
-     public function viewNew(array $args)
-     {
+                if (! empty($pid)) {
+                    return $this->c->Redirect->page('ViewPost', ['id' => $pid]);
+                }
+            }
+        }
 
-     }
+        return $this->viewLast(['id' => $topic['id']]);
+    }
 
     /**
-     * Подготовка данных для шаблона
+     * Переход к первому непрочитанному сообщению (или в конец)
+     * 
      * @param array $args
+     * 
      * @return Page
      */
-     public function viewUnread(array $args)
-     {
+    public function viewUnread(array $args)
+    {
+        $topic = $this->curTopic($args['id']); 
+        if (false === $topic) {
+            return $this->c->Message->message('Bad request');
+        }
 
-     }
+        if (! $this->c->user->isGuest) {
+            $lower = max(
+                (int) $this->c->user->u_mark_all_read,
+                (int) $topic['mf_mark_all_read'],
+                (int) $topic['mt_last_read']
+            );
+
+            if ($lower < $topic['last_post']) {
+                $vars = [
+                    ':tid' => $args['id'],
+                    ':visit' => $lower,
+                ];
+                $sql = 'SELECT MIN(id) FROM ::posts WHERE topic_id=?i:tid AND posted>?i:visit';
+
+                $pid = $this->c->DB->query($sql, $vars)->fetchColumn();
+
+                if (! empty($pid)) {
+                    return $this->c->Redirect->page('ViewPost', ['id' => $pid]);
+                }
+            }
+        }
+
+        return $this->viewLast(['id' => $topic['id']]);
+    }
 
     /**
      * Переход к последнему сообщению темы
+     * 
      * @param array $args
+     * 
      * @return Page
      */
-     public function viewLast(array $args)
-     {
+    public function viewLast(array $args)
+    {
+        $topic = $this->curTopic($args['id']); 
+        if (false === $topic) {
+            return $this->c->Message->message('Bad request');
+        }
+
         $vars = [
             ':tid' => $args['id'],
         ];
@@ -74,122 +120,154 @@ class Topic extends Page
             return $this->c->Message->message('Bad request');
         }
 
-        return $this->c->Redirect->setPage('ViewPost', ['id' => $pid]);
+        return $this->c->Redirect->page('ViewPost', ['id' => $pid]);
      }
 
-     /**
-     * Просмотр темы по номеру сообщения
-     * @param array $args
-     * @return Page
+    /**
+     * Получение данных по текущей теме
+     * 
+     * @param mixed $id
+     * @param mixed $pid
+     * 
+     * @return bool|array
      */
-     public function viewPost(array $args)
-     {
-        $vars = [
-            ':pid' => $args['id'],
-            ':uid' => $this->c->user->id,
-        ];
+    protected function curTopic($id, $pid = null)
+    {
+        if ($this->curTopic) {
+            return $this->curTopic;
+        }
+
+        if (isset($pid)) {
+            $vars = [
+                ':pid' => $pid,
+                ':uid' => $this->c->user->id,
+            ];
+            if ($this->c->user->isGuest) {
+                $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed, 0 AS mf_mark_all_read, 0 AS mt_last_visit, 0 AS mt_last_read
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        INNER JOIN ::posts AS p ON t.id=p.topic_id
+                        WHERE p.id=?i:pid AND t.moved_to IS NULL';
 
-        if ($this->c->user->isGuest) {
-            $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed, 0 AS mf_mark_all_read, 0 AS mt_last_visit, 0 AS mt_last_read
-                    FROM ::topics AS t
-                    INNER JOIN ::forums AS f ON f.id=t.forum_id
-                    INNER JOIN ::posts AS p ON t.id=p.topic_id
-                    WHERE p.id=?i:pid AND t.moved_to IS NULL';
+            } else {
+                $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        INNER JOIN ::posts AS p ON t.id=p.topic_id
+                        LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
+                        LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
+                        LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
+                        WHERE p.id=?i:pid AND t.moved_to IS NULL';
+            }
         } else {
-            $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
-                    FROM ::topics AS t
-                    INNER JOIN ::forums AS f ON f.id=t.forum_id
-                    INNER JOIN ::posts AS p ON t.id=p.topic_id
-                    LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
-                    LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
-                    LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
-                    WHERE p.id=?i:pid AND t.moved_to IS NULL';
+            $vars = [
+                ':tid' => $id,
+                ':uid' => $this->c->user->id,
+            ];
+            if ($this->c->user->isGuest) {
+                $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed, 0 AS mf_mark_all_read, 0 AS mt_last_visit, 0 AS mt_last_read
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        WHERE t.id=?i:tid AND t.moved_to IS NULL';
+
+            } else {
+                $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
+                        LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
+                        LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
+                        WHERE t.id=?i:tid AND t.moved_to IS NULL';
+            }
+        }
+
+        $topic = $this->c->DB->query($sql, $vars)->fetch();
+
+        // тема отсутствует или недоступна
+        if (empty($topic)) {
+            return false;
         }
 
-        return $this->view($sql, $vars, null);
+        list($fTree, $fDesc, $fAsc) = $this->c->forums;
+
+        // раздел отсутствует в доступных
+        if (empty($fDesc[$topic['forum_id']])) {
+            return false;
+        }
+
+        $this->curTopic = $topic;
+        return $topic;
+     }
+
+    /**
+     * Просмотр темы по номеру сообщения
+     * 
+     * @param array $args
+     * 
+     * @return Page
+     */
+    public function viewPost(array $args)
+    {
+        $topic = $this->curTopic(null, $args['id']);
+        if (false === $topic) {
+            return $this->c->Message->message('Bad request');
+        }
+        return $this->view($topic, $args['id']);
     }
 
     /**
      * Просмотр темы по ее номеру
+     * 
      * @param array $args
+     * 
      * @return Page
      */
     public function viewTopic(array $args)
     {
-        $vars = [
-            ':tid' => $args['id'],
-            ':uid' => $this->c->user->id,
-        ];
-
-        if ($this->c->user->isGuest) {
-            $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed, 0 AS mf_mark_all_read, 0 AS mt_last_visit, 0 AS mt_last_read
-                    FROM ::topics AS t
-                    INNER JOIN ::forums AS f ON f.id=t.forum_id
-                    WHERE t.id=?i:tid AND t.moved_to IS NULL';
-        } else {
-            $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
-                    FROM ::topics AS t
-                    INNER JOIN ::forums AS f ON f.id=t.forum_id
-                    LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
-                    LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
-                    LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
-                    WHERE t.id=?i:tid AND t.moved_to IS NULL';
+        $topic = $this->curTopic($args['id']);
+        if (false === $topic) {
+            return $this->c->Message->message('Bad request');
         }
-
         $page = isset($args['page']) ? (int) $args['page'] : 1;
-
-        return $this->view($sql, $vars, $page);
+        return $this->view($topic, null, $page);
     }
 
     /**
      * Подготовка данных для шаблона
-     * @param string $sql
-     * @param array $vars
+     * 
+     * @param array $topic
+     * @param int|null $pid
      * @param int|null $page
+     * 
      * @return Page
      */
-     protected function view($sql, array $vars, $page)
+     protected function view(array $topic, $pid, $page = null)
      {
         $user = $this->c->user;
 
-        $vars[':uid'] = $user->id;
-
-        $topic = $this->c->DB->query($sql, $vars)->fetch();
-
-        // тема отсутствует или недоступна
-        if (empty($topic)) {
-            return $this->c->Message->message('Bad request');
-        }
-
-        list($fTree, $fDesc, $fAsc) = $this->c->forums;
-
-        // раздел отсутствует в доступных
-        if (empty($fDesc[$topic['forum_id']])) {
-            return $this->c->Message->message('Bad request');
-        }
-
         if (null === $page) {
-            $vars[':tid'] = $topic['id'];
+            $vars = [
+                ':tid' => $topic['id'],
+                ':pid' => $pid,
+            ];
             $sql = 'SELECT COUNT(id) FROM ::posts WHERE topic_id=?i:tid AND id<?i:pid';
 
             $num = 1 + $this->c->DB->query($sql, $vars)->fetchColumn();
 
-            $page = ceil($num / $user->dispPosts);
+            $page = ceil($num / $user->disp_posts);
         }
 
-        $this->c->Lang->load('topic');
-
-        $pages = ceil(($topic['num_replies'] + 1) / $user->dispPosts);
+        $pages = ceil(($topic['num_replies'] + 1) / $user->disp_posts);
         // попытка открыть страницу которой нет
         if ($page < 1 || $page > $pages) {
             return $this->c->Message->message('Bad request');
         }
 
-        $offset = ($page - 1) * $user->dispPosts;
+        $offset = ($page - 1) * $user->disp_posts;
         $vars = [
             ':tid'    => $topic['id'],
             ':offset' => $offset,
-            ':rows'   => $user->dispPosts,
+            ':rows'   => $user->disp_posts,
         ];
         $sql = 'SELECT id
                 FROM ::posts
@@ -200,9 +278,13 @@ class Topic extends Page
 
         // нарушена синхронизация количества сообщений в темах
         if (empty($ids)) {
-            return $this->viewLast($topic['id']);
+            return $this->viewLast(['id' => $topic['id']]);
         }
 
+        $this->c->Lang->load('topic');
+
+        list($fTree, $fDesc, $fAsc) = $this->c->forums;
+
         $moders = empty($topic['moderators']) ? [] : array_flip(unserialize($topic['moderators']));
         $parent = isset($fDesc[$topic['forum_id']][0]) ? $fDesc[$topic['forum_id']][0] : 0;
         $perm = $fTree[$parent][$topic['forum_id']];
@@ -217,7 +299,7 @@ class Topic extends Page
         } elseif ($topic['closed'] == '1') {
             $newOn = false;
         } elseif ($perm['post_replies'] === 1
-            || (null === $perm['post_replies'] && $user->gPostReplies == '1')
+            || (null === $perm['post_replies'] && $user->g_post_replies == '1')
             || ($user->isAdmMod && isset($moders[$user->id]))
         ) {
             $newOn = true;
@@ -255,20 +337,20 @@ class Topic extends Page
 
         // парсер и его настройка для сообщений
         $bbcodes = include $this->c->DIR_CONFIG . '/defaultBBCode.php';
-        $smilies = $this->c->smilies;
+        $smilies = $this->c->smilies->list; //????
         foreach ($smilies as &$cur) {
             $cur = $this->c->PUBLIC_URL . '/img/sm/' . $cur;
         }
         unset($cur);
         $bbInfo = $this->c->BBCODE_INFO;
-        $bbWList = $this->config['p_message_bbcode'] == '1' ? null : [];
-        $bbBList = $this->config['p_message_img_tag'] == '1' ? [] : ['img'];
+        $bbWList = $this->c->config->p_message_bbcode == '1' ? null : [];
+        $bbBList = $this->c->config->p_message_img_tag == '1' ? [] : ['img'];
         $parser = $this->c->Parser;
         $parser->setBBCodes($bbcodes)
                ->setAttr('isSign', false)
                ->setWhiteList($bbWList)
                ->setBlackList($bbBList);
-        if ($user->showSmilies == '1') {
+        if ($user->show_smilies == '1') {
             $parser->setSmilies($smilies)
                    ->setSmTpl($bbInfo['smTpl'], $bbInfo['smTplTag'], $bbInfo['smTplBl']);
         }
@@ -287,7 +369,7 @@ class Topic extends Page
                 $post = [
                     'poster'            => $cur['username'],
                     'poster_id'         => $cur['poster_id'],
-                    'poster_title'      => $this->censor($this->userGetTitle($cur)),
+                    'poster_title'      => $this->c->censorship->censor($this->userGetTitle($cur)),
                     'poster_avatar'     => null,
                     'poster_registered' => null,
                     'poster_location'   => null,
@@ -299,13 +381,13 @@ class Topic extends Page
 
                 ];
                 if ($cur['poster_id'] > 1) {
-                    if ($user->gViewUsers == '1') {
+                    if ($user->g_view_users == '1') {
                         $post['poster_link'] = $this->c->Router->link('User', ['id' => $cur['poster_id'], 'name' => $cur['username']]);
                     }
-                    if ($this->config['o_avatars'] == '1' && $user->showAvatars == '1') {
+                    if ($this->c->config->o_avatars == '1' && $user->show_avatars == '1') {
                         $post['poster_avatar'] = $this->userGetAvatarLink($cur['poster_id']);
                     }
-                    if ($this->config['o_show_user_info'] == '1') {
+                    if ($this->c->config->o_show_user_info == '1') {
                         $post['poster_info_add'] = true;
 
                         $post['poster_registered'] = $this->time($cur['registered'], true);
@@ -314,7 +396,7 @@ class Topic extends Page
                         $post['poster_num_posts'] = $cur['num_posts'];
 
                         if ($cur['location'] != '') {
-                            $post['poster_location'] = $this->censor($cur['location']);
+                            $post['poster_location'] = $this->c->censorship->censor($cur['location']);
                         }
                         if (isset($genders[$cur['gender']])) {
                             $post['poster_gender'] = $genders[$cur['gender']];
@@ -324,9 +406,9 @@ class Topic extends Page
 
                     $posters[$cur['poster_id']] = $post;
 
-                    if ($this->config['o_signatures'] == '1'
+                    if ($this->c->config->o_signatures == '1'
                         && $cur['signature'] != ''
-                        && $user->showSig == '1'
+                        && $user->show_sig == '1'
                         && ! isset($signs[$cur['poster_id']])
                     ) {
                         $signs[$cur['poster_id']] = $cur['signature'];
@@ -342,8 +424,8 @@ class Topic extends Page
 
             $timeMax = max($timeMax, $cur['posted']);
 
-            $parser->parse($this->censor($cur['message']));
-            if ($this->config['o_smilies'] == '1' && $user->showSmilies == '1' && $cur['hide_smilies'] == '0') {
+            $parser->parse($this->c->censorship->censor($cur['message']));
+            if ($this->c->config->o_smilies == '1' && $user->show_smilies == '1' && $cur['hide_smilies'] == '0') {
                 $parser->detectSmilies();
             }
             $post['message'] = $parser->getHtml();
@@ -363,18 +445,18 @@ class Topic extends Page
                 $controls['report'] = [$this->c->Router->link('ReportPost', $vars), 'Report'];
             }
             if ($user->isAdmin
-                || ($user->isAdmMod && isset($moders[$user->id]) && ! in_array($cur['poster_id'], $this->c->admins))
+                || ($user->isAdmMod && isset($moders[$user->id]) && ! in_array($cur['poster_id'], $this->c->admins->list)) //????
             ) {
                 $controls['delete'] = [$this->c->Router->link('DeletePost', $vars), 'Delete'];
                 $controls['edit'] = [$this->c->Router->link('EditPost', $vars), 'Edit'];
             } elseif ($topic['closed'] != '1'
                 && $cur['poster_id'] == $user->id
-                && ($user->gDeleditInterval == '0' || $cur['edit_post'] == '1' || time() - $cur['posted'] < $user->gDeleditInterval)
+                && ($user->g_deledit_interval == '0' || $cur['edit_post'] == '1' || time() - $cur['posted'] < $user->g_deledit_interval)
             ) {
-                if (($cur['id'] == $topic['first_post_id'] && $user->gDeleteTopics == '1') || ($cur['id'] != $topic['first_post_id'] && $user->gDeletePosts == '1')) {
+                if (($cur['id'] == $topic['first_post_id'] && $user->g_delete_topics == '1') || ($cur['id'] != $topic['first_post_id'] && $user->g_delete_posts == '1')) {
                     $controls['delete'] = [$this->c->Router->link('DeletePost', $vars), 'Delete'];
                 }
-                if ($user->gEditPosts == '1') {
+                if ($user->g_edit_posts == '1') {
                     $controls['edit'] = [$this->c->Router->link('EditPost', $vars), 'Edit'];
                 }
             }
@@ -389,15 +471,15 @@ class Topic extends Page
 
         if ($signs) {
             // настройка парсера для подписей
-            $bbWList = $this->config['p_sig_bbcode'] == '1' ? $bbInfo['forSign'] : [];
-            $bbBList = $this->config['p_sig_img_tag'] == '1' ? [] : ['img'];
+            $bbWList = $this->c->config->p_sig_bbcode == '1' ? $bbInfo['forSign'] : [];
+            $bbBList = $this->c->config->p_sig_img_tag == '1' ? [] : ['img'];
             $parser->setAttr('isSign', true)
                    ->setWhiteList($bbWList)
                    ->setBlackList($bbBList);
 
             foreach ($signs as &$cur) {
-                $parser->parse($this->censor($cur));
-                if ($this->config['o_smilies_sig'] == '1' && $user->showSmilies == '1') {
+                $parser->parse($this->c->censorship->censor($cur));
+                if ($this->c->config->o_smilies_sig == '1' && $user->show_smilies == '1') {
                     $parser->detectSmilies();
                 }
                 $cur = $parser->getHtml();
@@ -405,13 +487,12 @@ class Topic extends Page
             unset($cur);
         }
 
-        $topic['subject'] = $this->censor($topic['subject']);
+        $topic['subject'] = $this->c->censorship->censor($topic['subject']);
 
-        $this->onlinePos = 'topic-' . $topic['id'];
 
         // данные для формы быстрого ответа
         $form = null;
-        if ($newOn && $this->config['o_quickpost'] == '1') {
+        if ($newOn && $this->c->config->o_quickpost == '1') {
             $form = [
                 'action' => $this->c->Router->link('NewReply', ['id' => $topic['id']]),
                 'hidden' => [
@@ -439,7 +520,7 @@ class Topic extends Page
                     'type'      => 'text',
                     'maxlength' => 80,
                     'title'     => __('Email'),
-                    'required'  => $this->config['p_force_guest_email'] == '1',
+                    'required'  => $this->c->config->p_force_guest_email == '1',
                     'pattern'   => '.+@.+',
                 ];
             }
@@ -449,10 +530,10 @@ class Topic extends Page
                 'title'    => __('Message'),
                 'required' => true,
                 'bb'       => [
-                    ['link', __('BBCode'), __($this->config['p_message_bbcode'] == '1' ? 'on' : 'off')],
-                    ['link', __('url tag'), __($this->config['p_message_bbcode'] == '1' && $user->gPostLinks == '1' ? 'on' : 'off')],
-                    ['link', __('img tag'), __($this->config['p_message_bbcode'] == '1' && $this->config['p_message_img_tag'] == '1' ? 'on' : 'off')],
-                    ['link', __('Smilies'), __($this->config['o_smilies'] == '1' ? 'on' : 'off')],
+                    ['link', __('BBCode'), __($this->c->config->p_message_bbcode == '1' ? 'on' : 'off')],
+                    ['link', __('url tag'), __($this->c->config->p_message_bbcode == '1' && $user->g_post_links == '1' ? 'on' : 'off')],
+                    ['link', __('img tag'), __($this->c->config->p_message_bbcode == '1' && $this->c->config->p_message_img_tag == '1' ? 'on' : 'off')],
+                    ['link', __('Smilies'), __($this->c->config->o_smilies == '1' ? 'on' : 'off')],
                 ],
             ];
             $form['sets'][] = [
@@ -476,26 +557,26 @@ class Topic extends Page
             }
         }
 
-        $this->data = [
-            'topic'    => $topic,
-            'posts'    => $posts,
-            'signs'    => $signs,
-            'warnings' => $warnings,
-            'crumbs'   => $this->getCrumbs(
-                ['Topic', ['id' => $topic['id'], 'name' => $topic['subject']]],
-                [$fDesc, $topic['forum_id']]
-            ),
-            'NewReply' => $newOn ? $this->c->Router->link('NewReply', ['id' => $topic['id']]) : $newOn,
-            'stickFP'  => $stickFP,
-            'pages'    => $this->c->Func->paginate($pages, $page, 'Topic', ['id' => $topic['id'], 'name' => $topic['subject']]),
-            'online'   => $this->getUsersOnlineInfo(),
-            'stats'    => null,
-            'form'     => $form,
-        ];
-
-        $this->canonical = $this->c->Router->link('Topic', ['id' => $topic['id'], 'name' => $topic['subject'], 'page' => $page]);
-
-        if ($this->config['o_topic_views'] == '1') {
+        $this->nameTpl    = 'topic';
+        $this->onlinePos  = 'topic-' . $topic['id'];
+        $this->onlineType = true;
+        $this->canonical  = $this->c->Router->link('Topic', ['id' => $topic['id'], 'name' => $topic['subject'], 'page' => $page]);
+        $this->topic      = $topic;
+        $this->posts      = $posts;
+        $this->signs      = $signs;
+        $this->warnings   = $warnings;
+        $this->crumbs     = $this->crumbs(
+            ['Topic', ['id' => $topic['id'], 'name' => $topic['subject']]],
+            [$fDesc, $topic['forum_id']]
+        );
+        $this->NewReply   = $newOn ? $this->c->Router->link('NewReply', ['id' => $topic['id']]) : $newOn;
+        $this->stickFP    = $stickFP;
+        $this->pages      = $this->c->Func->paginate($pages, $page, 'Topic', ['id' => $topic['id'], 'name' => $topic['subject']]);
+        $this->online     = $this->usersOnlineInfo();
+        $this->stats      = null;
+        $this->form       = $form;
+
+        if ($this->c->config->o_topic_views == '1') {
             $vars = [
                 ':tid' => $topic['id'],
             ];
@@ -512,12 +593,12 @@ class Topic extends Page
                 ':visit' => $topic['mt_last_visit'],
             ];
             $flag = false;
-            $lower = max((int) $user->uMarkAllRead, (int) $topic['mf_mark_all_read'], (int) $topic['mt_last_read']); //????
+            $lower = max((int) $user->u_mark_all_read, (int) $topic['mf_mark_all_read'], (int) $topic['mt_last_read']); //????
             if ($timeMax > $lower) {
                 $vars[':read'] = $timeMax;
                 $flag = true;
             }
-            $upper = max($lower, (int) $topic['mt_last_visit'], (int) $user->lastVisit); //????
+            $upper = max($lower, (int) $topic['mt_last_visit'], (int) $user->last_visit); //????
             if ($topic['last_post'] > $upper) {
                 $vars[':visit'] = $topic['last_post'];
                 $flag = true;
@@ -533,8 +614,8 @@ class Topic extends Page
                                         LIMIT 1', $vars);
                 } else {
                     $this->c->DB->exec('UPDATE ::mark_of_topic
-                    SET mt_last_visit=?i:visit, mt_last_read=?i:read
-                    WHERE uid=?i:uid AND tid=?i:tid', $vars);
+                                        SET mt_last_visit=?i:visit, mt_last_read=?i:read
+                                        WHERE uid=?i:uid AND tid=?i:tid', $vars);
                 }
             }
         }

+ 9 - 7
app/Models/Pages/UsersTrait.php

@@ -6,25 +6,25 @@ trait UsersTrait
 {
     /**
      * Имена забаненных пользователей
+     * 
      * @var array
      */
     protected $userBanNames;
 
     /**
      * Определение титула для пользователя
+     * 
      * @param array $data
+     * 
      * @return string
      */
     protected function userGetTitle(array $data) 
     {
         if (! isset($this->userBanNames)) {
-            $this->userBanNames = [];
-            foreach($this->c->bans as $cur) {
-                $this->userBanNames[mb_strtolower($cur['username'])] = true;
-            }
+            $this->userBanNames = $this->c->bans->userList; //????
         }
 
-        if(isset($this->userBanNames[$data['username']])) {
+        if (isset($this->userBanNames[mb_strtolower($data['username'])])) { //????
             return __('Banned');
         } elseif ($data['title'] != '') {
             return $data['title'];
@@ -39,7 +39,9 @@ trait UsersTrait
 
     /**
      * Определение ссылки на аватарку
+     * 
      * @param int $id
+     * 
      * @return string|null
      */
     protected function userGetAvatarLink($id)
@@ -47,10 +49,10 @@ trait UsersTrait
         $filetypes = array('jpg', 'gif', 'png');
     
         foreach ($filetypes as $type) {
-            $path = $this->c->DIR_PUBLIC . "/{$this->config['o_avatars_dir']}/{$id}.{$type}";
+            $path = $this->c->DIR_PUBLIC . "/{$this->c->config->o_avatars_dir}/{$id}.{$type}";
 
             if (file_exists($path) && getimagesize($path)) {
-                return $this->c->PUBLIC_URL . "/{$this->config['o_avatars_dir']}/{$id}.{$type}";
+                return $this->c->PUBLIC_URL . "/{$this->c->config->o_avatars_dir}/{$id}.{$type}";
             }
         }
 

+ 23 - 0
app/Models/SmileyList.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+
+class SmileyList extends Model
+{
+    /**
+     * Загружает список смайлов из кеша/БД
+     *
+     * @return SmileyList
+     */
+    public function init()
+    {
+        if ($this->c->Cache->has('smilies')) {
+            $this->list  = $this->c->Cache->get('smilies');
+        } else {
+            $this->load();
+        }
+        return $this;
+    }
+}

+ 22 - 0
app/Models/SmileyList/Load.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace ForkBB\Models\SmileyList;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * Заполняет модель данными из БД
+     * Создает кеш
+     *
+     * @return SmileyList
+     */
+    public function load()
+    {
+        $list = $this->c->DB->query('SELECT text, image FROM ::smilies ORDER BY disp_position')->fetchAll(\PDO::FETCH_KEY_PAIR); //???? text уникальное?
+        $this->model->list = $list;
+        $this->c->Cache->set('smilies', $list);
+        return $this->model;
+    }
+}

+ 30 - 0
app/Models/Stats.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+
+class Stats extends Model
+{
+    /**
+     * Загружает статистику из кеша/БД
+     *
+     * @return Stats
+     */
+    public function init()
+    {
+        if ($this->c->Cache->has('stats')) {
+            $list = $this->c->Cache->get('stats');
+            $this->userTotal = $list['total'];
+            $this->userLast  = $list['last'];
+        } else {
+            $this->load();
+        }
+
+        list($topics, $posts) = $this->c->DB->query('SELECT SUM(num_topics), SUM(num_posts) FROM ::forums')->fetch(\PDO::FETCH_NUM);
+        $this->postTotal  = $posts;
+        $this->topicTotal = $topics;
+
+        return $this;
+    }
+}

+ 27 - 0
app/Models/Stats/Load.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace ForkBB\Models\Stats;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * Заполняет модель данными из БД
+     * Создает кеш
+     *
+     * @return Stats
+     */
+    public function load()
+    {
+        $total = $this->c->DB->query('SELECT COUNT(id)-1 FROM ::users WHERE group_id!=?i', [$this->c->GROUP_UNVERIFIED])->fetchColumn();
+        $last  = $this->c->DB->query('SELECT id, username FROM ::users WHERE group_id!=?i ORDER BY registered DESC LIMIT 1', [$this->c->GROUP_UNVERIFIED])->fetch();
+        $this->model->userTotal = $total;
+        $this->model->userLast  = $last;
+        $this->c->Cache->set('stats', [
+            'total' => $total,
+            'last'  => $last,
+        ]);
+        return $this->model;
+    }
+}

+ 82 - 0
app/Models/Stopwords.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\Model;
+
+class Stopwords extends Model
+{
+    /**
+     * Загружает список игнорируемых при индексации слов из кеша/БД
+     *
+     * @return Stopwords
+     */
+    public function init()
+    {
+        $data = $this->c->Cache->get('stopwords');
+        if (isset($data['id'])
+            && isset($data['stopwords'])
+            && $data['id'] === $this->generateId()
+        ) {
+            $this->list = $data['stopwords'];
+        } else {
+            $this->load();
+        }
+        return $this;
+    }
+
+    /**
+     * Генерирует id кэша на основе найденных файлов stopwords.txt
+     *
+     * @return string
+     */
+    protected function generateId()
+    {
+        if (! empty($this->id)) {
+            return $this->id;
+        }
+
+        $files = glob($this->c->DIR_LANG . '/*/stopwords.txt');
+        if ($files === false) {
+            return 'cache_id_error';
+        }
+
+        $this->files = $files;
+        $hash = [];
+
+        foreach ($files as $file) {
+            $hash[] = $file;
+            $hash[] = filemtime($file);
+        }
+
+        return $this->id = sha1(implode('|', $hash));
+    }
+
+    /**
+     * Регенерация кэша массива слов с возвращением результата
+     *
+     * @return Stopwords
+     */
+    protected function load()
+    {
+        $id = $this->generateId();
+
+        if (! is_array($this->files)) {
+            $this->list = [];
+            return $this;
+        }
+
+        $stopwords = [];
+        foreach ($this->files as $file) {
+            $stopwords = array_merge($stopwords, file($file));
+        }
+
+        // Tidy up and filter the stopwords
+        $stopwords = array_map('trim', $stopwords);
+        $stopwords = array_filter($stopwords);
+
+        $this->c->Cache->set('stopwords', ['id' => $id, 'stopwords' => $stopwords]);
+        $this->list = $stopwords;
+        return $this;
+    }
+}

+ 21 - 39
app/Models/User.php

@@ -2,22 +2,11 @@
 
 namespace ForkBB\Models;
 
-use ForkBB\Core\AbstractModel;
+use ForkBB\Models\DataModel;
 use ForkBB\Core\Container;
 
-class User extends AbstractModel
+class User extends DataModel
 {
-    /**
-     * Контейнер
-     * @var Container
-     */
-    protected $c;
-
-    /**
-     * @var array
-     */
-    protected $config;
-
     /**
      * Время
      * @var int
@@ -26,63 +15,56 @@ class User extends AbstractModel
 
     /**
      * Конструктор
+     *
+     * @param array $data
+     * @param Container $container
      */
-    public function __construct(array $data, Container $container)
+    public function __construct(array $data = [], Container $container)
     {
         $this->now = time();
-        $this->c = $container;
-        $this->config = $container->config;
-        parent::__construct($data);
-    }
-
-    /**
-     * Выполняется до конструктора родителя
-     */
-    protected function beforeConstruct(array $data)
-    {
-        return $data;
+        parent::__construct($data, $container);
     }
 
     protected function getIsUnverified()
     {
-        return $this->groupId == $this->c->GROUP_UNVERIFIED;
+        return $this->group_id == $this->c->GROUP_UNVERIFIED;
     }
 
     protected function getIsGuest()
     {
-        return $this->groupId == $this->c->GROUP_GUEST
+        return $this->group_id == $this->c->GROUP_GUEST
             || $this->id < 2
-            || $this->groupId == $this->c->GROUP_UNVERIFIED;
+            || $this->group_id == $this->c->GROUP_UNVERIFIED;
     }
 
     protected function getIsAdmin()
     {
-        return $this->groupId == $this->c->GROUP_ADMIN;
+        return $this->group_id == $this->c->GROUP_ADMIN;
     }
 
     protected function getIsAdmMod()
     {
-        return $this->groupId == $this->c->GROUP_ADMIN
-            || $this->gModerator == '1';
+        return $this->group_id == $this->c->GROUP_ADMIN
+            || $this->g_moderator == '1';
     }
 
     protected function getLogged()
     {
-        return empty($this->data['logged']) ? $this->now : $this->data['logged'];
+        return empty($this->a['logged']) ? $this->now : $this->a['logged'];
     }
 
     protected function getIsLogged()
     {
-        return ! empty($this->data['logged']);
+        return ! empty($this->a['logged']);
     }
 
     protected function getLanguage()
     {
         $langs = $this->c->Func->getLangs();
 
-        $lang = $this->isGuest || empty($this->data['language']) || ! in_array($this->data['language'], $langs)
-            ? $this->config['o_default_lang']
-            : $this->data['language'];
+        $lang = $this->isGuest || empty($this->a['language']) || ! in_array($this->a['language'], $langs)
+            ? $this->c->config->o_default_lang
+            : $this->a['language'];
 
         if (in_array($lang, $langs)) {
             return $lang;
@@ -95,9 +77,9 @@ class User extends AbstractModel
     {
         $styles = $this->c->Func->getStyles();
 
-        $style = $this->isGuest || empty($this->data['style']) || ! in_array($this->data['style'], $styles)
-            ? $this->config['o_default_style']
-            : $this->data['style'];
+        $style = $this->isGuest || empty($this->a['style']) || ! in_array($this->a['style'], $styles)
+            ? $this->c->config->o_default_style
+            : $this->a['style'];
 
         if (in_array($style, $styles)) {
             return $style;

+ 26 - 0
app/Models/User/IsUnique.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace ForkBB\Models\User;
+
+use ForkBB\Models\MethodModel;
+
+class IsUnique extends MethodModel
+{
+    /**
+     * Проверка на уникальность имени пользователя
+     * @param string $username
+     * @return bool
+     */
+    public function isUnique($username = null)
+    {
+        if (null === $username) {
+            $username = $this->model->username;
+        }
+        $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);
+    }
+}

+ 46 - 0
app/Models/User/Load.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace ForkBB\Models\User;
+
+use ForkBB\Models\MethodModel;
+use InvalidArgumentException;
+
+class Load extends MethodModel
+{
+    /**
+     * Получение пользователя по условию
+     * @param mixed $value
+     * @param string $field
+     * @throws InvalidArgumentException
+     * @return int|User
+     */
+    public function load($value, $field = 'id')
+    {
+        switch ($field) {
+            case 'id':
+                $where = 'u.id= ?i';
+                break;
+            case 'username':
+                $where = 'u.username= ?s';
+                break;
+            case 'email':
+                $where = 'u.email= ?s';
+                break;
+            default:
+                throw new InvalidArgumentException('Field not supported');
+        }
+
+        $data = $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 (count($data) !== 1) {
+            return count($data);
+        }
+        // найден гость
+        if ($data[0]['id'] < 2) {
+            return 1;
+        }
+        return $this->model->setAttrs($data[0]);
+    }
+}

+ 70 - 53
app/Models/Actions/LoadUserFromCookie.php → app/Models/User/LoadUserFromCookie.php

@@ -1,63 +1,44 @@
 <?php
 
-namespace ForkBB\Models\Actions;
+namespace ForkBB\Models\User;
 
-use ForkBB\Models\UserCookie;
-use ForkBB\Models\UserMapper;
+use ForkBB\Models\MethodModel;
 use RuntimeException;
 
-class LoadUserFromCookie
+class LoadUserFromCookie extends MethodModel
 {
-    protected $mapper;
-    protected $cookie;
-    protected $config;
-
-    /**
-     * Конструктор
-     *
-     * @param UserMapper $mapper
-     * @param UserCookie $cookie
-     * @param array $config
-     */
-    public function __construct(UserMapper $mapper, UserCookie $cookie, array $config)
-    {
-        $this->mapper = $mapper;
-        $this->cookie = $cookie;
-        $this->config = $config;
-    }
-
     /**
      * Получение юзера на основе куки авторизации
      * Обновление куки аутентификации
      *
      * @return User
      */
-    public function load()
+    public function loadCurrent()
     {
-        $id = $this->cookie->id() ?: 1;
-        $user = $this->mapper->getCurrent($id);
-
-        if (! $user->isGuest) {
-            if (! $this->cookie->verifyHash($user->id, $user->password)) {
-                $user = $this->mapper->getCurrent(1);
-            } elseif ($this->config['o_check_ip'] == '1'
-                && $user->isAdmMod
-                && $user->registrationIp != $user->ip
+        $cookie = $this->c->Cookie;
+        $this->loadUser((int) $cookie->uId);
+
+        if (! $this->model->isGuest) {
+            if (! $cookie->verifyUser($this->model)) {
+                $this->model = $this->loadUser(1);
+            } elseif ($this->c->config->o_check_ip == '1'
+                && $this->model->isAdmMod
+                && $this->model->registration_ip != $this->model->ip
             ) {
-                $user = $this->mapper->getCurrent(1);
+                $this->model = $this->loadUser(1);
             }
         }
 
-        $this->cookie->setUserCookie($user->id, $user->password);
+        $cookie->setUser($this->model);
 
-        if ($user->isGuest) {
-            $user->isBot = $this->isBot();
-            $user->dispTopics = $this->config['o_disp_topics_default'];
-            $user->dispPosts = $this->config['o_disp_posts_default'];
-            $user->timezone = $this->config['o_default_timezone'];
-            $user->dst = $this->config['o_default_dst'];
-            $user->language = $this->config['o_default_lang'];
-            $user->style = $this->config['o_default_style'];
+        if ($this->model->isGuest) {
+            $this->model->__isBot = $this->isBot();
+            $this->model->__disp_topics = $this->c->config->o_disp_topics_default;
+            $this->model->__disp_posts = $this->c->config->o_disp_posts_default;
+            $this->model->__timezone = $this->c->config->o_default_timezone;
+            $this->model->__dst = $this->c->config->o_default_dst;
+#            $this->model->language = $this->c->config->o_default_lang;
+#            $this->model->style = $this->c->config->o_default_style;
 
             // быстрое переключение языка - Visman
 /*            $language = $this->cookie->get('glang');
@@ -65,30 +46,64 @@ class LoadUserFromCookie
                 $language = preg_replace('%[^a-zA-Z0-9_]%', '', $language);
                 $languages = forum_list_langs();
                 if (in_array($language, $languages)) {
-                    $user->language = $language;
+                    $this->model->language = $language;
                 }
             } */
         } else {
-            $user->isBot = false;
-            if (! $user->dispTopics) {
-                $user->dispTopics = $this->config['o_disp_topics_default'];
+            $this->model->__isBot = false;
+            if (! $this->model->disp_topics) {
+                $this->model->__disp_topics = $this->c->config->o_disp_topics_default;
             }
-            if (! $user->dispPosts) {
-                $user->dispPosts = $this->config['o_disp_posts_default'];
+            if (! $this->model->disp_posts) {
+                $this->model->__disp_posts = $this->c->config->o_disp_posts_default;
             }
             // Special case: We've timed out, but no other user has browsed the forums since we timed out
-            if ($user->isLogged && $user->logged < time() - $this->config['o_timeout_visit']) {
-                $this->mapper->updateLastVisit($user);
-                $user->lastVisit = $user->logged;
+            if ($this->model->isLogged && $this->model->logged < time() - $this->c->config->o_timeout_visit) {
+                $this->model->updateLastVisit();
             }
         }
 
-        return $user;
+        return $this->model;
+    }
+
+    /**
+     * Загрузка данных в модель пользователя из базы
+     *
+     * @param int $id
+     *
+     * @throws RuntimeException
+     */
+    public function loadUser($id)
+    {
+        $data = null;
+        $ip = $this->getIp();
+        if ($id > 1) {
+            $data = $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($data['id'])) {
+            $data = $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($data['id'])) {
+            throw new RuntimeException('Unable to fetch guest information. Your database must contain both a guest user and a guest user group.');
+        }
+        $this->model->setAttrs($data);
+        $this->model->__ip = $ip;
+    }
+
+    /**
+     * Возврат ip пользователя
+     *
+     * @return string
+     */
+    protected function getIp()
+    {
+       return filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) ?: 'unknow';
     }
 
     /**
      * Проверка на робота
      * Если робот, то возврат имени
+     *
      * @return false|string
      */
     protected function isBot()
@@ -125,9 +140,11 @@ class LoadUserFromCookie
 
     /**
      * Выделяет имя робота из юзерагента
+     *
      * @param string $agent
      * @param string $agentL
-     * @retrun string
+     *
+     * @return string
      */
     protected function nameBot($agent, $agentL)
     {

+ 71 - 0
app/Models/User/Save.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace ForkBB\Models\User;
+
+use ForkBB\Models\MethodModel;
+use RuntimeException;
+
+class Save extends MethodModel
+{
+    /**
+     * Обновляет данные пользователя
+     *
+     * @throws RuntimeException
+     */
+    public function save()
+    {
+        if (empty($this->model->id)) {
+            throw new RuntimeException('The model does not have ID');
+        }
+        $modified = $this->model->getModified();
+        if (empty($modified)) {
+            return;
+        }
+        $values = $this->model->getAttrs();
+        $fileds = $this->c->dbMap->users;
+        $set = $vars = [];
+        foreach ($modified as $name) {
+            if (! isset($fileds[$name])) {
+                continue;
+            }
+            $vars[] = $values[$name];
+            $set[] = $name . '=?' . $fileds[$name];
+        }
+        if (empty($set)) {
+            return;
+        }
+        $vars[] = $this->model->id;
+        $this->c->DB->query('UPDATE ::users SET ' . implode(', ', $set) . ' WHERE id=?i', $vars);
+        $this->model->resModified();
+    }
+
+    /**
+     * Добавляет новую запись в таблицу пользователей
+     *
+     * @throws RuntimeException
+     */
+    public function insert()
+    {
+        $modified = $this->model->getModified();
+        if (null !== $this->model->id || in_array('id', $modified)) {
+            throw new RuntimeException('The model has ID');
+        }
+        $values = $this->model->getAttrs();
+        $fileds = $this->c->dbMap->users;
+        $set = $set2 = $vars = [];
+        foreach ($modified as $name) {
+            if (! isset($fileds[$name])) {
+                continue;
+            }
+            $vars[] = $values[$name];
+            $set[] = $name;
+            $set2[] = '?' . $fileds[$name];
+        }
+        if (empty($set)) {
+            return;
+        }
+        $this->c->DB->query('INSERT INTO ::users (' . implode(', ', $set) . ') VALUES (' . implode(', ', $set2) . ')', $vars);
+        $this->model->resModified();
+        return $this->c->DB->lastInsertId();
+    }
+}

+ 19 - 0
app/Models/User/UpdateLastVisit.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace ForkBB\Models\User;
+
+use ForkBB\Models\MethodModel;
+
+class UpdateLastVisit extends MethodModel
+{
+    /**
+     * Обновляет время последнего визита для конкретного пользователя
+     */
+    public function updateLastVisit()
+    {
+        if ($this->model->isLogged) {
+            $this->c->DB->exec('UPDATE ::users SET last_visit=?i:loggid WHERE id=?i:id', [':loggid' => $this->model->logged, ':id' => $this->model->id]);
+            $this->model->__last_visit = $this->model->logged;
+        }
+    }
+}

+ 0 - 157
app/Models/UserCookie.php

@@ -1,157 +0,0 @@
-<?php
-
-namespace ForkBB\Models;
-
-use ForkBB\Core\Cookie;
-use ForkBB\Core\Secury;
-
-class UserCookie extends Cookie
-{
-    const NAME = 'user';
-    const KEY1 = 'key1';
-    const KEY2 = 'key2';
-
-    /**
-     * Флаг указывающий на режим "запомнить меня"
-     * @var bool
-     */
-    protected $remember;
-
-    /**
-     * Номер юзера из куки аутентификации
-     * @var int
-     */
-    protected $uId;
-
-    /**
-     * Время "протухания" куки аутентификации
-     * @var int
-     */
-    protected $expTime;
-
-    /**
-     * Хэш хэша пароля юзера из куки аутентификации
-     * @var string
-     */
-    protected $passHash;
-
-    /**
-     * Время жизни куки без запоминания
-     * @var int
-     */
-    protected $min;
-
-    /**
-     * Время жизни куки с запоминанием
-     * @var int
-     */
-    protected $max;
-
-    /**
-     * Конструктор
-     * @param Secury $secury
-     * @param array $options
-     * @param int $min
-     * @param int $max
-     */
-    public function __construct(Secury $secury, array $options, $min, $max)
-    {
-        parent::__construct($secury, $options);
-        $this->min = (int) $min;
-        $this->max = (int) $max;
-        $this->init();
-    }
-
-    /**
-     * Выделение данных из куки аутентификации
-     */
-    protected function init()
-    {
-        $ckUser = $this->get(self::NAME);
-
-        if (null === $ckUser
-            || ! preg_match('%^(\-)?(\d{1,10})_(\d{10})_([a-f\d]{32,})_([a-f\d]{32,})$%Di', $ckUser, $ms)
-        ) {
-            return;
-        }
-
-        if (2 > $ms[2]
-            || time() > $ms[3]
-            || ! hash_equals($this->secury->hmac($ms[1] . $ms[2] . $ms[3] . $ms[4], self::KEY1), $ms[5])
-        ) {
-            return;
-        }
-
-        $this->remember = empty($ms[1]);
-        $this->uId      = (int) $ms[2];
-        $this->expTime  = (int) $ms[3];
-        $this->passHash = $ms[4];
-    }
-
-    /**
-     * Возвращает id юзера из куки
-     * @return int|false
-     */
-    public function id()
-    {
-        return $this->uId ?: false;
-    }
-
-    /**
-     * Проверка хэша пароля
-     * @param int $id
-     * @param string $hash
-     * @return bool
-     */
-    public function verifyHash($id, $hash)
-    {
-        return $this->uId === (int) $id
-               && hash_equals($this->passHash, $this->secury->hmac($hash . $this->expTime, self::KEY2));
-    }
-
-    /**
-     * Установка куки аутентификации юзера
-     * @param int $id
-     * @param string $hash
-     * @param bool $remember
-     * @return bool
-     */
-    public function setUserCookie($id, $hash, $remember = null)
-    {
-        if ($id < 2) {
-            return $this->deleteUserCookie();
-        }
-
-        if ($remember
-            || (null === $remember
-                && $this->uId === (int) $id
-                && $this->remember
-            )
-        ) {
-            $expTime = time() + $this->max;
-            $expire = $expTime;
-            $pfx = '';
-        } else {
-            $expTime = time() + $this->min;
-            $expire = 0;
-            $pfx = '-';
-        }
-        $passHash = $this->secury->hmac($hash . $expTime, self::KEY2);
-        $ckHash = $this->secury->hmac($pfx . $id . $expTime . $passHash, self::KEY1);
-
-        return $this->set(self::NAME, $pfx . $id . '_' . $expTime . '_' . $passHash . '_' . $ckHash, $expire);
-    }
-
-    /**
-     * Удаление куки аутентификации юзера
-     * @return bool
-     */
-    public function deleteUserCookie()
-    {
-        if (null === $this->get(self::NAME)) {
-            return true;
-        } else {
-            return $this->delete(self::NAME);
-        }
-    }
-}

+ 0 - 177
app/Models/UserMapper.php

@@ -1,177 +0,0 @@
-<?php
-
-namespace ForkBB\Models;
-
-use ForkBB\Core\Container;
-use ForkBB\Models\User;
-use RuntimeException;
-use InvalidArgumentException;
-
-class UserMapper
-{
-    /**
-     * Контейнер
-     * @var Container
-     */
-    protected $c;
-
-    /**
-     * @var array
-     */
-    protected $config;
-
-    /**
-     * Конструктор
-     * @param Container $container
-     */
-    public function __construct(Container $container)
-    {
-        $this->c = $container;
-        $this->config = $container->config;
-    }
-
-    /**
-     * Возврат адреса пользователя
-     * @return string
-     */
-    protected function getIpAddress()
-    {
-       return filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) ?: 'unknow';
-    }
-
-    /**
-     * Получение данных для текущего пользователя/гостя
-     * @param int $id
-     * @return User
-     * @throws \RuntimeException
-     */
-    public function getCurrent($id = 1)
-    {
-        $ip = $this->getIpAddress();
-        $user = null;
-        if ($id > 1) {
-            $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'])) {
-            $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);
-    }
-
-    /**
-     * Обновляет время последнего визита для конкретного пользователя
-     * @param User $user
-     */
-    public function updateLastVisit(User $user)
-    {
-        if ($user->isLogged) {
-            $this->c->DB->exec('UPDATE ::users SET last_visit=?i:loggid WHERE id=?i:id', [':loggid' => $user->logged, ':id' => $user->id]);
-        }
-    }
-
-    /**
-     * Получение пользователя по условию
-     * @param int|string
-     * @param string $field
-     * @return int|User
-     * @throws \InvalidArgumentException
-     */
-    public function getUser($value, $field = 'id')
-    {
-        switch ($field) {
-            case 'id':
-                $where = 'u.id= ?i';
-                break;
-            case 'username':
-                $where = 'u.username= ?s';
-                break;
-            case 'email':
-                $where = 'u.email= ?s';
-                break;
-            default:
-                throw new InvalidArgumentException('Field not supported');
-        }
-        $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 (count($result) !== 1) {
-            return count($result);
-        }
-        // найден гость
-        if ($result[0]['id'] == 1) {
-            return 1;
-        }
-        return new User($result[0], $this->c);
-    }
-
-    /**
-     * Проверка на уникальность имени пользователя
-     * @param string $username
-     * @return bool
-     */
-    public function isUnique($username)
-    {
-        $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);
-    }
-
-    /**
-     * Обновить данные пользователя
-     * @param int $id
-     * @param array $update
-     */
-    public function updateUser($id, array $update)
-    {
-        $id = (int) $id;
-        if ($id < 2 || empty($update)) {
-            return;
-        }
-
-        $set = $vars = [];
-        foreach ($update as $field => $value) {
-            $vars[] = $value;
-            if (is_int($value)) {
-                $set[] = $field . ' = ?i';
-            } else {
-                $set[] = $field . ' = ?s';
-            }
-        }
-        $vars[] = $id;
-        $this->c->DB->query('UPDATE ::users SET ' . implode(', ', $set) . ' WHERE id=?i', $vars); //????
-    }
-
-    /**
-     * Создание нового пользователя
-     * @param User $user
-     * @throws
-     * @return int
-     */
-    public function newUser(User $user)
-    {
-        $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();
-    }
-}

+ 6 - 7
app/bootstrap.php

@@ -3,7 +3,7 @@
 namespace ForkBB;
 
 use ForkBB\Core\Container;
-use ForkBB\Models\Pages\Page;
+use ForkBB\Models\Page;
 use RuntimeException;
 
 // боевой
@@ -47,8 +47,8 @@ $c->DIR_CONFIG = __DIR__ . '/config';
 $c->DIR_CACHE  = __DIR__ . '/cache';
 $c->DIR_VIEWS  = __DIR__ . '/templates';
 $c->DIR_LANG   = __DIR__ . '/lang';
-$c->DATE_FORMATS = [$c->config['o_date_format'], 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y'];
-$c->TIME_FORMATS = [$c->config['o_time_format'], 'H:i:s', 'H:i', 'g:i:s a', 'g:i a'];
+$c->DATE_FORMATS = [$c->config->o_date_format, 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y'];
+$c->TIME_FORMATS = [$c->config->o_time_format, 'H:i:s', 'H:i', 'g:i:s a', 'g:i a'];
 
 $controllers = ['Routing', 'Primary'];
 $page = null;
@@ -56,13 +56,12 @@ while (! $page instanceof Page && $cur = array_pop($controllers)) {
     $page = $c->$cur;
 }
 
-if ($page->getDataForOnline(true)) {
-    $c->Online->handle($page);
+if (null !== $page->onlinePos) {
+    $c->Online->calc($page);
 }
 $tpl = $c->View->rendering($page);
 if ($tpl !== null && $c->DEBUG > 0) {
-    $debug = $c->Debug->debug();
-    $debug = $c->View->rendering($debug);
+    $debug = $c->View->rendering($c->Debug->debug());
     $tpl = str_replace('<!-- debuginfo -->', $debug, $tpl);
 }
 exit($tpl);

+ 2 - 2
app/lang/English/common.po

@@ -79,13 +79,13 @@ msgid "Ban message"
 msgstr "You are banned from this forum."
 
 msgid "Ban message 2"
-msgstr "The ban expires at the end of"
+msgstr "The ban expires at the end of %s"
 
 msgid "Ban message 3"
 msgstr "The administrator or moderator that banned you left the following message:"
 
 msgid "Ban message 4"
-msgstr "Please direct any inquiries to the forum administrator at"
+msgstr "Please direct any inquiries to the forum administrator at <a href=\"mailto:%1$s\">%1$s</a>."
 
 msgid "Never"
 msgstr "Never"

+ 3 - 3
app/lang/Russian/common.po

@@ -79,13 +79,13 @@ msgid "Ban message"
 msgstr "Ваша учетная запись заблокирована."
 
 msgid "Ban message 2"
-msgstr "Блокировка заканчивается"
+msgstr "Блокировка заканчивается %s"
 
 msgid "Ban message 3"
-msgstr "Причина блокирования:"
+msgstr "Администратор или модератор, установивший блокировку, оставил следующее сообщение:"
 
 msgid "Ban message 4"
-msgstr "Все возникшие вопросы отправляйте администратору форума по адресу"
+msgstr "Все возникшие вопросы отправляйте администратору форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."
 
 msgid "Never"
 msgstr "Нет"

+ 11 - 11
app/templates/admin/group.tpl

@@ -2,18 +2,18 @@
       <section class="f-admin">
         <h2>{!! __('Group settings head') !!}</h2>
         <div class="f-fdiv">
-          <form class="f-form" method="post" action="{!! $formAction !!}">
-            <input type="hidden" name="token" value="{!! $formToken !!}">
+          <form class="f-form" method="post" action="{!! $p->formAction !!}">
+            <input type="hidden" name="token" value="{!! $p->formToken !!}">
             <dl>
-@foreach($form as $key => $cur)
+@foreach($p->form as $key => $cur)
               <dt>{!! $cur['title'] !!}</dt>
               <dd>
 @if($cur['type'] == 'text')
-                <input class="f-ctrl" @if(isset($cur['required'])){!! 'required' !!}@endif type="text" name="{{ $key }}" maxlength="{!! $cur['maxlength'] !!}" value="{{ $cur['value'] }}" tabindex="{!! ++$tabindex !!}">
+                <input class="f-ctrl" @if(isset($cur['required'])){!! 'required' !!}@endif type="text" name="{{ $key }}" maxlength="{!! $cur['maxlength'] !!}" value="{{ $cur['value'] }}" tabindex="{!! ++$p->tabindex !!}">
 @elseif($cur['type'] == 'number')
-                <input class="f-ctrl" type="number" name="{{ $key }}" min="{!! $cur['min'] !!}" max="{!! $cur['max'] !!}" value="{{ $cur['value'] }}" tabindex="{!! ++$tabindex !!}">
+                <input class="f-ctrl" type="number" name="{{ $key }}" min="{!! $cur['min'] !!}" max="{!! $cur['max'] !!}" value="{{ $cur['value'] }}" tabindex="{!! ++$p->tabindex !!}">
 @elseif($cur['type'] == 'select')
-                <select class="f-ctrl" name="{{ $key }}" tabindex="{!! ++$tabindex !!}">
+                <select class="f-ctrl" name="{{ $key }}" tabindex="{!! ++$p->tabindex !!}">
 @foreach($cur['options'] as $v => $n)
 @if($v == $cur['value'])
                   <option value="{{ $v }}" selected>{{ $n }}</option>
@@ -25,9 +25,9 @@
 @elseif($cur['type'] == 'radio')
 @foreach($cur['values'] as $v => $n)
 @if($v == $cur['value'])
-                <label class="f-label"><input type="radio" name="{{ $key }}" value="{{ $v }}" checked tabindex="{!! ++$tabindex !!}">{{ $n }}</label>
+                <label class="f-label"><input type="radio" name="{{ $key }}" value="{{ $v }}" checked tabindex="{!! ++$p->tabindex !!}">{{ $n }}</label>
 @else
-                <label class="f-label"><input type="radio" name="{{ $key }}" value="{{ $v }}" tabindex="{!! ++$tabindex !!}">{{ $n }}</label>
+                <label class="f-label"><input type="radio" name="{{ $key }}" value="{{ $v }}" tabindex="{!! ++$p->tabindex !!}">{{ $n }}</label>
 @endif
 @endforeach
 @endif
@@ -37,11 +37,11 @@
               </dd>
 @endforeach
             </dl>
-@if($warn)
-            <p class="f-finfo">{!! $warn !!}</p>
+@if($p->warn)
+            <p class="f-finfo">{!! $p->warn !!}</p>
 @endif
             <div>
-              <input class="f-btn" type="submit" name="submit" value="{!! __('Save') !!}" tabindex="{!! ++$tabindex !!}">
+              <input class="f-btn" type="submit" name="submit" value="{!! __('Save') !!}" tabindex="{!! ++$p->tabindex !!}">
             </div>
           </form>
         </div>

+ 15 - 15
app/templates/admin/groups.tpl

@@ -2,14 +2,14 @@
       <section class="f-admin">
         <h2>{!! __('Add group subhead') !!}</h2>
         <div class="f-fdiv">
-          <form class="f-form" method="post" action="{!! $formActionNew !!}">
-            <input type="hidden" name="token" value="{!! $formTokenNew !!}">
+          <form class="f-form" method="post" action="{!! $p->formActionNew !!}">
+            <input type="hidden" name="token" value="{!! $p->formTokenNew !!}">
             <dl>
               <dt>{!! __('New group label') !!}</dt>
               <dd>
-                <select class="f-ctrl" id="id-basegroup" name="basegroup" tabindex="{!! ++$tabindex !!}">
-@foreach($groupsNew as $cur)
-@if ($cur[0] == $defaultGroup)
+                <select class="f-ctrl" id="id-basegroup" name="basegroup" tabindex="{!! ++$p->tabindex !!}">
+@foreach($p->groupsNew as $cur)
+@if ($cur[0] == $p->defaultGroup)
                   <option value="{!! $cur[0] !!}" selected>{{ $cur[1] }}</option>
 @else
                   <option value="{!! $cur[0] !!}">{{ $cur[1] }}</option>
@@ -20,7 +20,7 @@
               </dd>
             </dl>
             <div>
-              <input class="f-btn" type="submit" name="submit" value="{!! __('Add') !!}" tabindex="{!! ++$tabindex !!}">
+              <input class="f-btn" type="submit" name="submit" value="{!! __('Add') !!}" tabindex="{!! ++$p->tabindex !!}">
             </div>
           </form>
         </div>
@@ -28,14 +28,14 @@
       <section class="f-admin">
         <h2>{!! __('Default group subhead') !!}</h2>
         <div class="f-fdiv">
-          <form class="f-form" method="post" action="{!! $formActionDefault !!}">
-            <input type="hidden" name="token" value="{!! $formTokenDefault !!}">
+          <form class="f-form" method="post" action="{!! $p->formActionDefault !!}">
+            <input type="hidden" name="token" value="{!! $p->formTokenDefault !!}">
             <dl>
               <dt>{!! __('Default group label') !!}</dt>
               <dd>
-                <select class="f-ctrl" id="id-defaultgroup" name="defaultgroup" tabindex="{!! ++$tabindex !!}">
-@foreach($groupsDefault as $cur)
-@if ($cur[0] == $defaultGroup)
+                <select class="f-ctrl" id="id-defaultgroup" name="defaultgroup" tabindex="{!! ++$p->tabindex !!}">
+@foreach($p->groupsDefault as $cur)
+@if ($cur[0] == $p->defaultGroup)
                   <option value="{!! $cur[0] !!}" selected>{{ $cur[1] }}</option>
 @else
                   <option value="{!! $cur[0] !!}">{{ $cur[1] }}</option>
@@ -46,7 +46,7 @@
               </dd>
             </dl>
             <div>
-              <input class="f-btn" type="submit" name="submit" value="{!! __('Save') !!}" tabindex="{!! ++$tabindex !!}">
+              <input class="f-btn" type="submit" name="submit" value="{!! __('Save') !!}" tabindex="{!! ++$p->tabindex !!}">
             </div>
           </form>
         </div>
@@ -56,11 +56,11 @@
         <div>
           <p>{!! __('Edit groups info') !!}</p>
           <ol class="f-grlist">
-@foreach($groupsList as $cur)
+@foreach($p->groupsList as $cur)
             <li>
-              <a href="{!! $cur[0] !!}" tabindex="{!! ++$tabindex !!}">{{ $cur[1] }}</a>
+              <a href="{!! $cur[0] !!}" tabindex="{!! ++$p->tabindex !!}">{{ $cur[1] }}</a>
 @if($cur[2])
-              <a class="f-btn" href="{!! $cur[2] !!}" tabindex="{!! ++$tabindex !!}">{!! __('Delete link') !!}</a>
+              <a class="f-btn" href="{!! $cur[2] !!}" tabindex="{!! ++$p->tabindex !!}">{!! __('Delete link') !!}</a>
 @endif
             </li>
 @endforeach

+ 2 - 2
app/templates/admin/index.tpl

@@ -19,9 +19,9 @@
         <div>
           <dl>
             <dt>{!! __('ForkBB version label') !!}</dt>
-            <dd>{!! __('ForkBB version data', $revision) !!}</dd>
+            <dd>{!! __('ForkBB version data', $p->revision) !!}</dd>
             <dt>{!! __('Server statistics label') !!}</dt>
-            <dd><a href="{!! $linkStat !!}">{!! __('View server statistics') !!}</a></dd>
+            <dd><a href="{!! $p->linkStat !!}">{!! __('View server statistics') !!}</a></dd>
             <dt>{!! __('Support label') !!}</dt>
             <dd><a href="https://github.com/forkbb/forkbb">GitHub</a></dd>
           </dl>

+ 10 - 10
app/templates/admin/statistics.tpl

@@ -4,24 +4,24 @@
         <div>
           <dl>
             <dt>{!! __('Server load label') !!}</dt>
-            <dd>{!! __('Server load data', $serverLoad, $numOnline) !!}</dd>
-@if($isAdmin)
+            <dd>{!! __('Server load data', $p->serverLoad, $p->numOnline) !!}</dd>
+@if($p->isAdmin)
             <dt>{!! __('Environment label') !!}</dt>
             <dd>
               {!! __('Environment data OS', PHP_OS) !!}<br>
-              {!! __('Environment data version', phpversion(), '<a href="' . $linkInfo . '">'.__('Show info').'</a>') !!}<br>
-              {!! __('Environment data acc', $accelerator) !!}
+              {!! __('Environment data version', phpversion(), '<a href="' . $p->linkInfo . '">'.__('Show info').'</a>') !!}<br>
+              {!! __('Environment data acc', $p->accelerator) !!}
             </dd>
             <dt>{!! __('Database label') !!}</dt>
             <dd>
-              {{ $dbVersion }}
-@if($tRecords && $tSize)
-              <br>{!! __('Database data rows', $tRecords) !!}
-              <br>{!! __('Database data size', $tSize) !!}
+              {{ $p->dbVersion }}
+@if($p->tRecords && $p->tSize)
+              <br>{!! __('Database data rows', $p->tRecords) !!}
+              <br>{!! __('Database data size', $p->tSize) !!}
 @endif
-@if($tOther)
+@if($p->tOther)
               <br><br>{!! __('Other')!!}
-@foreach($tOther as $key => $value)
+@foreach($p->tOther as $key => $value)
               <br>{{ $key }} = {{ $value }}
 @endforeach
 @endif

+ 5 - 5
app/templates/ban.tpl

@@ -2,12 +2,12 @@
     <section class="f-main f-message">
       <h2>{{ __('Info') }}</h2>
       <p>{!! __('Ban message') !!}</p>
-@if(! empty($banned['expire']))
-      <p>{!! __('Ban message 2') !!} {{ $banned['expire'] }}</p>
+@if(! empty($p->ban['expire']))
+      <p>{!! __('Ban message 2', $p->ban['expire']) !!}</p>
 @endif
-@if(! empty($banned['message']))
+@if(! empty($p->ban['message']))
       <p>{!! __('Ban message 3') !!}</p>
-      <p><strong>{{ $banned['message'] }}</strong></p>
+      <p><strong>{{ $p->ban['message'] }}</strong></p>
 @endif
-      <p>{!! __('Ban message 4'] !!) <a href="mailto:{{ $adminEmail }}">{{ $adminEmail }}</a>.</p>
+      <p>{!! __('Ban message 4', $p->adminEmail, $p->adminEmail) !!}</p>
     </section>

+ 2 - 2
app/templates/change_passphrase.tpl

@@ -2,8 +2,8 @@
     <section class="f-main f-login">
       <div class="f-fdiv f-lrdiv">
         <h2>{!! __('Change pass') !!}</h2>
-        <form class="f-form" method="post" action="{!! $formAction !!}">
-          <input type="hidden" name="token" value="{!! $formToken !!}">
+        <form class="f-form" method="post" action="{!! $p->formAction !!}">
+          <input type="hidden" name="token" value="{!! $p->formToken !!}">
           <fieldset>
             <dl>
               <dt><label class="f-child1 f-req" for="id-password">{!! __('New pass') !!}</label></dt>

+ 11 - 11
app/templates/forum.tpl

@@ -1,6 +1,6 @@
 @section('crumbs')
       <ul class="f-crumbs">
-@foreach($crumbs as $cur)
+@foreach($p->crumbs as $cur)
 @if($cur[2])
         <li class="f-crumb"><a href="{!! $cur[0] !!}" class="active">{{ $cur[1] }}</a></li>
 @else
@@ -10,15 +10,15 @@
       </ul>
 @endsection
 @section('linkpost')
-@if($newTopic)
+@if($p->newTopic)
         <div class="f-link-post">
-          <a class="f-btn" href="{!! $newTopic !!}">{!! __('Post topic') !!}</a>
+          <a class="f-btn" href="{!! $p->newTopic !!}">{!! __('Post topic') !!}</a>
         </div>
 @endif
 @endsection
 @section('pages')
         <nav class="f-pages">
-@foreach($pages as $cur)
+@foreach($p->pages as $cur)
 @if($cur[2])
           <span class="f-page active">{{ $cur[1] }}</span>
 @elseif($cur[1] === 'space')
@@ -34,13 +34,13 @@
         </nav>
 @endsection
 @extends('layouts/main')
-@if($forums)
+@if($p->forums)
     <div class="f-nav-links">
 @yield('crumbs')
     </div>
     <section class="f-subforums">
       <ol class="f-ftlist">
-@foreach($forums as $id => $cat)
+@foreach($p->forums as $id => $cat)
         <li id="id-subforums{!! $id !!}" class="f-category">
           <h2>{{ __('Sub forum', 2) }}</h2>
           <ol class="f-table">
@@ -58,20 +58,20 @@
 @endif
     <div class="f-nav-links">
 @yield('crumbs')
-@if($newTopic || $pages)
+@if($p->newTopic || $p->pages)
       <div class="f-links-b clearfix">
 @yield('pages')
 @yield('linkpost')
       </div>
 @endif
     </div>
-@if(empty($topics))
+@if(!$p->topics)
     <section class="f-main f-message">
       <h2>{!! __('Empty forum') !!}</h2>
     </section>
 @else
     <section class="f-main f-forum">
-      <h2>{{ $forumName }}</h2>
+      <h2>{{ $p->forumName }}</h2>
       <div class="f-ftlist">
         <ol class="f-table">
           <li class="f-row f-thead" value="0">
@@ -79,7 +79,7 @@
             <div class="f-hcell f-cstats">{!! __('Stats') !!}</div>
             <div class="f-hcell f-clast">{!! __('Last post') !!}</div>
           </li>
-@foreach($topics as $topic)
+@foreach($p->topics as $topic)
 @if($topic['moved_to'])
           <li id="topic-{!! $topic['id']!!}" class="f-row f-fredir">
             <div class="f-cell f-cmain">
@@ -160,7 +160,7 @@
       </div>
     </section>
     <div class="f-nav-links">
-@if($newTopic || $pages)
+@if($p->newTopic || $p->pages)
       <div class="f-links-a clearfix">
 @yield('linkpost')
 @yield('pages')

+ 2 - 2
app/templates/index.tpl

@@ -1,8 +1,8 @@
 @extends('layouts/main')
-@if($forums)
+@if($p->forums)
     <section class="f-main">
       <ol class="f-ftlist">
-@foreach($forums as $id => $cat)
+@foreach($p->forums as $id => $cat)
         <li id="cat-{!! $id !!}" class="f-category">
           <h2>{{ $cat['name'] }}</h2>
           <ol class="f-table">

+ 3 - 3
app/templates/layouts/admin.tpl

@@ -1,15 +1,15 @@
 @extends('layouts/main')
     <div class="f-main clearfix">
       <aside class="f-admin-menu">
-@if(!empty($aNavigation))
+@if($p->aNavigation)
         <nav class="admin-nav f-menu">
           <input id="admin-nav-checkbox" style="display: none;" type="checkbox">
           <label class="f-menu-toggle" for="admin-nav-checkbox"></label>
-@foreach($aNavigation as $aNameSub => $aNavigationSub)
+@foreach($p->aNavigation as $aNameSub => $aNavigationSub)
           <h2 class="f-menu-items">{!! __($aNameSub) !!}</h2>
           <ul class="f-menu-items">
 @foreach($aNavigationSub as $key => $val)
-@if($key == $aIndex)
+@if($key == $p->aIndex)
             <li><a id="anav-{{ $key }}" class="active" href="{!! $val[0] !!}">{!! $val[1] !!}</a></li>
 @else
             <li><a id="anav-{{ $key }}" href="{!! $val[0] !!}">{!! $val[1] !!}</a></li>

+ 4 - 4
app/templates/layouts/debug.tpl

@@ -1,7 +1,7 @@
     <section class="f-debug">
       <h2>{!! __('Debug table') !!}</h2>
-      <p class="f-debugtime">[ {!! __('Querytime', $time, $numQueries) !!} - {!! __('Memory usage', $memory) !!} {!! __('Peak usage', $peak) !!} ]</p>
-@if($queries)
+      <p class="f-debugtime">[ {!! __('Querytime', $p->time, $p->numQueries) !!} - {!! __('Memory usage', $p->memory) !!} {!! __('Peak usage', $p->peak) !!} ]</p>
+@if($p->queries)
       <table>
         <thead>
           <tr>
@@ -10,14 +10,14 @@
           </tr>
         </thead>
         <tbody>
-@foreach($queries as $cur)
+@foreach($p->queries as $cur)
           <tr>
             <td class="tcl">{{ $cur[1] }}</td>
             <td class="tcr">{{ $cur[0] }}</td>
           </tr>
 @endforeach
           <tr>
-            <td class="tcl">{{ $total }}</td>
+            <td class="tcl">{{ $p->total }}</td>
             <td class="tcr"></td>
           </tr>
         </tbody>

+ 3 - 3
app/templates/layouts/form.tpl

@@ -24,11 +24,11 @@
 @if($cur['type'] === 'textarea')
                 <textarea{!! empty($cur['required']) ? '' : ' required' !!} class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}">{{ $cur['value'] or '' }}</textarea>
 @if(isset($cur['bb']))
-                  <ul class="f-child5">
+                <ul class="f-child5">
 @foreach($cur['bb'] as $val)
-                    <li><span><a href="{!! $val[0] !!}">{!! $val[1] !!}</a> {!! $val[2] !!}</span></li>
+                  <li><span><a href="{!! $val[0] !!}">{!! $val[1] !!}</a> {!! $val[2] !!}</span></li>
 @endforeach
-                  </ul>
+                </ul>
 @endif
 @elseif($cur['type'] === 'text')
                 <input{!! empty($cur['required']) ? '' : ' required' !!} class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}" type="text" maxlength="{{ $cur['maxlength'] or '' }}" pattern="{{ $cur['pattern'] or '' }}" value="{{ $cur['value'] or '' }}">

+ 23 - 23
app/templates/layouts/install.tpl

@@ -4,7 +4,7 @@
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>{!! __('ForkBB Installation') !!}</title>
-@foreach($pageHeads as $cur)
+@foreach($p->pageHeads as $cur)
   <{!! $cur !!}>
 @endforeach
 </head>
@@ -16,18 +16,18 @@
         <p class="f-description">{!! __('Welcome') !!}</p>
       </div>
     </header>
-@if($fIswev)
+@if($p->fIswev)
 @include('layouts/iswev')
 @endif
-@if(is_array($installLangs))
+@if(is_array($p->installLangs))
     <section class="f-install">
       <div class="f-fdiv">
         <h2>{!! __('Choose install language') !!}</h2>
-        <form class="f-form" method="post" action="{!! $formAction !!}">
+        <form class="f-form" method="post" action="{!! $p->formAction !!}">
           <div>
             <label class="f-child1">{!! __('Install language') !!}</label>
             <select class="f-ctrl" id="id-installlang" name="installlang">
-@foreach($installLangs as $cur)
+@foreach($p->installLangs as $cur)
 @if(isset($cur[1]))
               <option value="{{ $cur[0] }}" selected>{{ $cur[0] }}</option>
 @else
@@ -44,12 +44,12 @@
       </div>
     </section>
 @endif
-@if(empty($fIswev['e']))
+@if(empty($p->fIswev['e']))
     <section class="f-main f-install">
       <div class="f-fdiv">
-        <h2>{!! __('Install', $rev) !!}</h2>
-        <form class="f-form" method="post" action="{!! $formAction !!}" autocomplete="off">
-          <input type="hidden" name="installlang" value="{!! $installLang !!}">
+        <h2>{!! __('Install', $p->rev) !!}</h2>
+        <form class="f-form" method="post" action="{!! $p->formAction !!}" autocomplete="off">
+          <input type="hidden" name="installlang" value="{!! $p->installLang !!}">
           <div class="f-finfo">
             <h3>{!! __('Database setup') !!}</h3>
             <p>{!! __('Info 1') !!}</p>
@@ -57,7 +57,7 @@
           <div>
             <label class="f-child1 f-req">{!! __('Database type') !!}</label>
             <select class="f-ctrl" id="id-dbtype" name="dbtype">
-@foreach($dbTypes as $key => $cur)
+@foreach($p->dbTypes as $key => $cur)
 @if(empty($cur[1]))
               <option value="{{ $key }}">{{ $cur[0] }}</option>
 @else
@@ -69,17 +69,17 @@
           </div>
           <div>
             <label class="f-child1 f-req" for="id-dbhost">{!! __('Database server hostname') !!}</label>
-            <input required class="f-ctrl" id="id-dbhost" type="text" name="dbhost" value="{{ $dbhost }}">
+            <input required class="f-ctrl" id="id-dbhost" type="text" name="dbhost" value="{{ $p->dbhost }}">
             <label class="f-child4">{!! __('Info 3') !!}</label>
           </div>
           <div>
             <label class="f-child1 f-req" for="id-dbname">{!! __('Database name') !!}</label>
-            <input required class="f-ctrl" id="id-dbname" type="text" name="dbname" value="{{ $dbname }}">
+            <input required class="f-ctrl" id="id-dbname" type="text" name="dbname" value="{{ $p->dbname }}">
             <label class="f-child4">{!! __('Info 4') !!}</label>
           </div>
           <div>
             <label class="f-child1" for="id-dbuser">{!! __('Database username') !!}</label>
-            <input class="f-ctrl" id="id-dbuser" type="text" name="dbuser" value="{{ $dbuser }}">
+            <input class="f-ctrl" id="id-dbuser" type="text" name="dbuser" value="{{ $p->dbuser }}">
           </div>
           <div>
             <label class="f-child1" for="id-dbpass">{!! __('Database password') !!}</label>
@@ -88,7 +88,7 @@
           </div>
           <div>
             <label class="f-child1" for="id-dbprefix">{!! __('Table prefix') !!}</label>
-            <input class="f-ctrl" id="id-dbprefix" type="text" name="dbprefix" value="{{ $dbprefix }}" maxlength="40" pattern="^[a-zA-Z][a-zA-Z\d_]*$">
+            <input class="f-ctrl" id="id-dbprefix" type="text" name="dbprefix" value="{{ $p->dbprefix }}" maxlength="40" pattern="^[a-zA-Z][a-zA-Z\d_]*$">
             <label class="f-child4">{!! __('Info 6') !!}</label>
           </div>
           <div class="f-finfo">
@@ -97,7 +97,7 @@
           </div>
           <div>
             <label class="f-child1 f-req" for="id-username">{!! __('Administrator username') !!}</label>
-            <input required class="f-ctrl" id="id-username" type="text" name="username" value="{{ $username }}" maxlength="25" pattern="^.{2,25}$">
+            <input required class="f-ctrl" id="id-username" type="text" name="username" value="{{ $p->username }}" maxlength="25" pattern="^.{2,25}$">
             <label class="f-child4">{!! __('Info 8') !!}</label>
           </div>
           <div>
@@ -107,7 +107,7 @@
           </div>
           <div>
             <label class="f-child1 f-req" for="id-email">{!! __('Administrator email') !!}</label>
-            <input required class="f-ctrl" id="id-email" type="text" name="email" value="{{ $email }}" maxlength="80" pattern=".+@.+">
+            <input required class="f-ctrl" id="id-email" type="text" name="email" value="{{ $p->email }}" maxlength="80" pattern=".+@.+">
             <label class="f-child4">{!! __('Info 10') !!}</label>
           </div>
           <div class="f-finfo">
@@ -116,21 +116,21 @@
           </div>
           <div>
             <label class="f-child1 f-req" for="id-title">{!! __('Board title') !!}</label>
-            <input required class="f-ctrl" id="id-title" type="text" name="title" value="{{ $title }}">
+            <input required class="f-ctrl" id="id-title" type="text" name="title" value="{{ $p->title }}">
           </div>
           <div>
             <label class="f-child1 f-req" for="id-descr">{!! __('Board description') !!}</label>
-            <input required class="f-ctrl" id="id-descr" type="text" name="descr" value="{{ $descr }}">
+            <input required class="f-ctrl" id="id-descr" type="text" name="descr" value="{{ $p->descr }}">
           </div>
           <div>
             <label class="f-child1 f-req" for="id-baseurl">{!! __('Base URL') !!}</label>
-            <input required class="f-ctrl" id="id-baseurl" type="text" name="baseurl" value="{{ $baseurl }}">
+            <input required class="f-ctrl" id="id-baseurl" type="text" name="baseurl" value="{{ $p->baseurl }}">
           </div>
-@if(is_array($defaultLangs))
+@if(is_array($p->defaultLangs))
           <div>
             <label class="f-child1 f-req">{!! __('Default language') !!}</label>
             <select class="f-ctrl" id="id-defaultlang" name="defaultlang">
-@foreach($defaultLangs as $cur)
+@foreach($p->defaultLangs as $cur)
 @if(isset($cur[1]))
               <option value="{{ $cur[0] }}" selected>{{ $cur[0] }}</option>
 @else
@@ -140,12 +140,12 @@
             </select>
           </div>
 @else
-          <input type="hidden" name="defaultlang" value="{!! $defaultLangs !!}">
+          <input type="hidden" name="defaultlang" value="{!! $p->defaultLangs !!}">
 @endif
           <div>
             <label class="f-child1 f-req">{!! __('Default style') !!}</label>
             <select class="f-ctrl" id="id-defaultstyle" name="defaultstyle">
-@foreach($defaultStyles as $cur)
+@foreach($p->defaultStyles as $cur)
 @if(isset($cur[1]))
               <option value="{{ $cur[0] }}" selected>{{ $cur[0] }}</option>
 @else

+ 10 - 10
app/templates/layouts/iswev.tpl

@@ -1,48 +1,48 @@
-@if(isset($fIswev['i']))
+@if(isset($p->fIswev['i']))
     <section class="f-iswev f-info">
       <h2>Info message</h2>
       <ul>
-@foreach($fIswev['i'] as $cur)
+@foreach($p->fIswev['i'] as $cur)
         <li class="f-icontent">{!! $cur !!}</li>
 @endforeach
       </ul>
     </section>
 @endif
-@if(isset($fIswev['s']))
+@if(isset($p->fIswev['s']))
     <section class="f-iswev f-success">
       <h2>Successful operation message</h2>
       <ul>
-@foreach($fIswev['s'] as $cur)
+@foreach($p->fIswev['s'] as $cur)
         <li class="f-scontent">{!! $cur !!}</li>
 @endforeach
       </ul>
     </section>
 @endif
-@if(isset($fIswev['w']))
+@if(isset($p->fIswev['w']))
     <section class="f-iswev f-warning">
       <h2>Warning message</h2>
       <ul>
-@foreach($fIswev['w'] as $cur)
+@foreach($p->fIswev['w'] as $cur)
         <li class="f-wcontent">{!! $cur !!}</li>
 @endforeach
       </ul>
     </section>
 @endif
-@if(isset($fIswev['e']))
+@if(isset($p->fIswev['e']))
     <section class="f-iswev f-error">
       <h2>Error message</h2>
       <ul>
-@foreach($fIswev['e'] as $cur)
+@foreach($p->fIswev['e'] as $cur)
         <li class="f-econtent">{!! $cur !!}</li>
 @endforeach
       </ul>
     </section>
 @endif
-@if(isset($fIswev['v']))
+@if(isset($p->fIswev['v']))
     <section class="f-iswev f-validation">
       <h2>Validation message</h2>
       <ul>
-@foreach($fIswev['v'] as $cur)
+@foreach($p->fIswev['v'] as $cur)
         <li class="f-vcontent">{!! $cur !!}</li>
 @endforeach
       </ul>

+ 10 - 10
app/templates/layouts/main.tpl

@@ -3,8 +3,8 @@
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title>{{ $pageTitle }}</title>
-@foreach($pageHeaders as $cur)
+  <title>{{ $p->pageTitle }}</title>
+@foreach($p->pageHeaders as $cur)
   <{!! $cur !!}>
 @endforeach
 </head>
@@ -12,16 +12,16 @@
   <div class="f-wrap">
     <header class="f-header">
       <div class="f-title">
-        <h1><a href="{!! $fRootLink !!}">{{ $fTitle }}</a></h1>
-        <p class="f-description">{!! $fDescription !!}</p>
+        <h1><a href="{!! $p->fRootLink !!}">{{ $p->fTitle }}</a></h1>
+        <p class="f-description">{!! $p->fDescription !!}</p>
       </div>
-@if(!empty($fNavigation))
+@if($p->fNavigation)
       <nav class="main-nav f-menu">
         <input id="main-nav-checkbox" style="display: none;" type="checkbox">
         <label class="f-menu-toggle" for="main-nav-checkbox"></label>
         <ul class="f-menu-items">
-@foreach($fNavigation as $key => $val)
-@if($key == $fIndex)
+@foreach($p->fNavigation as $key => $val)
+@if($key == $p->fIndex)
           <li><a id="nav-{{ $key }}" class="active" href="{!! $val[0] !!}">{!! $val[1] !!}</a></li>
 @else
           <li><a id="nav-{{ $key }}" href="{!! $val[0] !!}">{!! $val[1] !!}</a></li>
@@ -31,13 +31,13 @@
       </nav>
 @endif
     </header>
-@if($fAnnounce)
+@if($p->fAnnounce)
     <section class="f-announce">
       <h2>{!! __('Announcement') !!}</h2>
-      <p class="f-ancontent">{!! $fAnnounce !!}</p>
+      <p class="f-ancontent">{!! $p->fAnnounce !!}</p>
     </section>
 @endif
-@if($fIswev)
+@if($p->fIswev)
 @include('layouts/iswev')
 @endif
 @yield('content')

+ 5 - 5
app/templates/layouts/redirect.tpl

@@ -3,9 +3,9 @@
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <meta http-equiv="refresh" content="{!! $timeout !!};URL={{ $link }}">
-  <title>{{ $pageTitle }}</title>
-@foreach($pageHeaders as $cur)
+  <meta http-equiv="refresh" content="{!! $p->timeout !!};URL={{ $p->link }}">
+  <title>{{ $p->pageTitle }}</title>
+@foreach($p->pageHeaders as $cur)
   <{!! $cur !!}>
 @endforeach
 </head>
@@ -13,8 +13,8 @@
   <div class="f-wrap">
     <section class="f-main f-redirect">
       <h2>{!! __('Redirecting') !!}</h2>
-      <p>{!! $message !!}</p>
-      <p><a href="{{ $link }}">{!! __('Click redirect') !!}</a></p>
+      <p>{!! $p->message !!}</p>
+      <p><a href="{{ $p->link }}">{!! __('Click redirect') !!}</a></p>
     </section>
 <!-- debuginfo -->
   </div>

+ 14 - 14
app/templates/layouts/stats.tpl

@@ -1,32 +1,32 @@
     <section class="f-stats">
       <h2>{!! __('Stats info') !!}</h2>
       <div class="clearfix">
-@if($stats)
+@if($p->stats)
         <dl class="right">
           <dt>{!! __('Board stats') !!}</dt>
-          <dd>{!! __('No of users') !!} <strong>{!! $stats['total_users'] !!}</strong></dd>
-          <dd>{!! __('No of topics') !!} <strong>{!! $stats['total_topics'] !!}</strong></dd>
-          <dd>{!! __('No of posts') !!} <strong>{!! $stats['total_posts'] !!}</strong></dd>
+          <dd>{!! __('No of users') !!} <strong>{!! $p->stats['total_users'] !!}</strong></dd>
+          <dd>{!! __('No of topics') !!} <strong>{!! $p->stats['total_topics'] !!}</strong></dd>
+          <dd>{!! __('No of posts') !!} <strong>{!! $p->stats['total_posts'] !!}</strong></dd>
         </dl>
 @endif
         <dl class="left">
           <dt>{!! __('User info') !!}</dt>
-@if($stats && is_string($stats['newest_user']))
-          <dd>{!! __('Newest user')  !!} {{ $stats['newest_user'] }}</dd>
-@elseif($stats)
-          <dd>{!! __('Newest user')  !!} <a href="{!! $stats['newest_user'][0] !!}">{{ $stats['newest_user'][1] }}</a></dd>
+@if($p->stats && is_string($p->stats['newest_user']))
+          <dd>{!! __('Newest user')  !!} {{ $p->stats['newest_user'] }}</dd>
+@elseif($p->stats)
+          <dd>{!! __('Newest user')  !!} <a href="{!! $p->stats['newest_user'][0] !!}">{{ $p->stats['newest_user'][1] }}</a></dd>
 @endif
-@if($online)
-          <dd>{!! __('Visitors online', $online['number_of_users'], $online['number_of_guests']) !!}</dd>
+@if($p->online)
+          <dd>{!! __('Visitors online', $p->online['number_of_users'], $p->online['number_of_guests']) !!}</dd>
 @endif
-@if($stats)
-          <dd>{!! __('Most online', $online['max'], $online['max_time']) !!}</dd>
+@if($p->stats)
+          <dd>{!! __('Most online', $p->online['max'], $p->online['max_time']) !!}</dd>
 @endif
         </dl>
-@if($online && $online['list'])
+@if($p->online && $p->online['list'])
         <dl class="f-inline f-onlinelist"><!-- inline -->
           <dt>{!! __('Online users') !!}</dt>
-@foreach($online['list'] as $cur)
+@foreach($p->online['list'] as $cur)
 @if(is_string($cur))
           <dd>{{ $cur }}</dd>
 @else

+ 8 - 8
app/templates/login.tpl

@@ -2,25 +2,25 @@
     <section class="f-main f-login">
       <div class="f-fdiv f-lrdiv">
         <h2>{!! __('Login') !!}</h2>
-        <form class="f-form" method="post" action="{!! $formAction !!}">
-          <input type="hidden" name="token" value="{!! $formToken !!}">
-          <input type="hidden" name="redirect" value="{{ $redirect }}">
+        <form class="f-form" method="post" action="{!! $p->formAction !!}">
+          <input type="hidden" name="token" value="{!! $p->formToken !!}">
+          <input type="hidden" name="redirect" value="{{ $p->redirect }}">
           <fieldset>
             <dl>
               <dt><label class="f-child1 f-req" for="id-username">{!! __('Username') !!}</label></dt>
               <dd>
-                <input required class="f-ctrl" id="id-username" type="text" name="username" value="{{ $username }}" maxlength="25" autofocus spellcheck="false" tabindex="1">
+                <input required class="f-ctrl" id="id-username" type="text" name="username" value="{{ $p->username }}" maxlength="25" autofocus spellcheck="false" tabindex="1">
               </dd>
             </dl>
             <dl>
-              <dt><label class="f-child1 f-req" for="id-password">{!! __('Passphrase') !!}<a class="f-forgetlink" href="{!! $forgetLink !!}" tabindex="5">{!! __('Forgotten pass') !!}</a></label></dt>
+              <dt><label class="f-child1 f-req" for="id-password">{!! __('Passphrase') !!}<a class="f-forgetlink" href="{!! $p->forgetLink !!}" tabindex="5">{!! __('Forgotten pass') !!}</a></label></dt>
               <dd>
                 <input required class="f-ctrl" id="id-password" type="password" name="password" tabindex="2">
               </dd>
             </dl>
             <dl>
               <dt></dt>
-@if($save)
+@if($p->save)
               <dd><label class="f-child2"><input type="checkbox" name="save" value="1" tabindex="3" checked>{!! __('Remember me') !!}</label></dd>
 @else
               <dd><label class="f-child2"><input type="checkbox" name="save" value="1" tabindex="3">{!! __('Remember me') !!}</label></dd>
@@ -32,9 +32,9 @@
           </p>
         </form>
       </div>
-@if($regLink)
+@if($p->regLink)
       <div class="f-fdiv f-lrdiv">
-        <p class="f-child3"><a href="{!! $regLink !!}" tabindex="6">{!! __('Not registered') !!}</a></p>
+        <p class="f-child3"><a href="{!! $p->regLink !!}" tabindex="6">{!! __('Not registered') !!}</a></p>
       </div>
 @endif
     </section>

+ 1 - 1
app/templates/maintenance.tpl

@@ -1,5 +1,5 @@
 @extends('layouts/main')
     <section class="f-main f-maintenance">
       <h2>{{ __('Maintenance') }}</h2>
-      <p>{!! $maintenanceMessage !!}</p>
+      <p>{!! $p->maintenanceMessage !!}</p>
     </section>

+ 2 - 2
app/templates/message.tpl

@@ -1,8 +1,8 @@
 @extends('layouts/main')
     <section class="f-main f-message">
       <h2>{!! __('Info') !!}</h2>
-      <p>{!! $message !!}</p>
-@if($back)
+      <p>{!! $p->message !!}</p>
+@if($p->back)
       <p><a href="javascript: history.go(-1)">{!! __('Go back') !!}</a></p>
 @endif
     </section>

部分文件因为文件数量过多而无法显示