Browse Source

intermediate achievements

Visman 8 years ago
parent
commit
75288400c6
84 changed files with 6898 additions and 188 deletions
  1. 12 0
      .htaccess
  2. 53 0
      app/Controllers/Primary.php
  3. 118 0
      app/Controllers/Routing.php
  4. 0 7
      app/Core/Blank.php
  5. 109 29
      app/Core/Install.php
  6. 260 0
      app/Core/Lang.php
  7. 133 0
      app/Core/Model.php
  8. 330 0
      app/Core/Router.php
  9. 73 0
      app/Core/View.php
  10. 55 7
      app/Models/Actions/CacheGenerator.php
  11. 33 18
      app/Models/Actions/CacheLoader.php
  12. 98 0
      app/Models/Actions/CheckBans.php
  13. 62 0
      app/Models/Csrf.php
  14. 242 0
      app/Models/Online.php
  15. 92 0
      app/Models/Pages/Admin/Admin.php
  16. 33 0
      app/Models/Pages/Admin/Index.php
  17. 112 0
      app/Models/Pages/Admin/Statistics.php
  18. 116 0
      app/Models/Pages/Auth.php
  19. 38 0
      app/Models/Pages/Ban.php
  20. 61 0
      app/Models/Pages/Debug.php
  21. 252 0
      app/Models/Pages/Index.php
  22. 70 0
      app/Models/Pages/Maintenance.php
  23. 33 0
      app/Models/Pages/Message.php
  24. 389 0
      app/Models/Pages/Page.php
  25. 98 0
      app/Models/Pages/Redirect.php
  26. 33 0
      app/Models/Pages/Rules.php
  27. 383 0
      app/Models/User.php
  28. 100 0
      app/Models/UserMapper.php
  29. 27 3
      app/bootstrap.php
  30. 2 1
      app/config/install.php
  31. 112 0
      app/lang/English/admin.po
  32. 142 0
      app/lang/English/admin_index.po
  33. 478 0
      app/lang/English/common.po
  34. 55 0
      app/lang/English/index.po
  35. 67 0
      app/lang/English/login.po
  36. 34 0
      app/lang/English/subforums.po
  37. 112 0
      app/lang/Russian/admin.po
  38. 142 0
      app/lang/Russian/admin_index.po
  39. 478 0
      app/lang/Russian/common.po
  40. 55 0
      app/lang/Russian/index.po
  41. 67 0
      app/lang/Russian/login.po
  42. 37 0
      app/lang/Russian/subforums.po
  43. 29 0
      app/templates/admin/index.tpl
  44. 26 0
      app/templates/admin/statistics.tpl
  45. 25 0
      app/templates/auth.tpl
  46. 13 0
      app/templates/ban.tpl
  47. 131 0
      app/templates/index.tpl
  48. 24 0
      app/templates/layouts/admin.tpl
  49. 22 0
      app/templates/layouts/debug.tpl
  50. 50 0
      app/templates/layouts/iswev.tpl
  51. 55 0
      app/templates/layouts/main.tpl
  52. 5 0
      app/templates/maintenance.tpl
  53. 8 0
      app/templates/message.tpl
  54. 23 0
      app/templates/redirect.tpl
  55. 5 0
      app/templates/rules.tpl
  56. 2 1
      composer.json
  57. 54 7
      composer.lock
  58. 71 1
      db_update.php
  59. 3 3
      footer.php
  60. 8 8
      include/cache.php
  61. 9 23
      include/common.php
  62. 58 71
      include/functions.php
  63. BIN
      public/style/ForkBB/font/flow-400-normal.ttf
  64. BIN
      public/style/ForkBB/font/flow-400-normal.woff
  65. BIN
      public/style/ForkBB/font/flow-400-normal.woff2
  66. BIN
      public/style/ForkBB/font/flow-700-normal.ttf
  67. BIN
      public/style/ForkBB/font/flow-700-normal.woff
  68. BIN
      public/style/ForkBB/font/flow-700-normal.woff2
  69. 12 0
      public/style/ForkBB/font/flow.css
  70. 83 0
      public/style/ForkBB/style.css
  71. 2 2
      register.php
  72. 1 3
      vendor/artoodetoo/container/src/Container.php
  73. 1 0
      vendor/artoodetoo/dirk/.gitignore
  74. 22 0
      vendor/artoodetoo/dirk/LICENSE
  75. 117 0
      vendor/artoodetoo/dirk/README.md
  76. 28 0
      vendor/artoodetoo/dirk/composer.json
  77. 17 0
      vendor/artoodetoo/dirk/phpunit.xml.dist
  78. 418 0
      vendor/artoodetoo/dirk/src/Dirk.php
  79. 156 0
      vendor/artoodetoo/dirk/src/PhpEngine.php
  80. 89 0
      vendor/artoodetoo/dirk/tests/DirkTest.php
  81. 77 0
      vendor/artoodetoo/dirk/tests/PhpEngineTest.php
  82. 1 0
      vendor/composer/autoload_psr4.php
  83. 5 0
      vendor/composer/autoload_static.php
  84. 52 4
      vendor/composer/installed.json

+ 12 - 0
.htaccess

@@ -0,0 +1,12 @@
+AddDefaultCharset UTF-8
+
+
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  #RewriteBase /
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteCond %{REQUEST_URI} !/public/.*\.\w+$
+  RewriteCond %{REQUEST_URI} !favicon\.ico
+  RewriteRule . index.php [L]
+</IfModule>

+ 53 - 0
app/Controllers/Primary.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace ForkBB\Controllers;
+
+use R2\DependencyInjection\ContainerInterface;
+
+class Primary
+{
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    protected $c;
+
+    /**
+     * Конструктор
+     * @param array $config
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        $this->c = $container;
+    }
+
+    /**
+     * Проверка на обслуживание
+     * Проверка на обновление
+     * Проверка на бан
+     * @return Page|null
+     */
+    public function check()
+    {
+        $config = $this->c->get('config');
+
+        // Проверяем режим обслуживания форума
+        if ($config['o_maintenance'] && ! defined('PUN_TURN_OFF_MAINT')) { //????
+           if (! in_array($this->c->get('UserCookie')->id(), $this->c->get('admins'))
+               || ! in_array($this->c->get('user')['id'], $this->c->get('admins'))
+           ) {
+               return $this->c->get('Maintenance');
+           }
+        }
+
+        // Обновляем форум, если нужно
+        if (empty($config['i_fork_revision']) || $config['i_fork_revision'] < FORK_REVISION) {
+            header('Location: db_update.php'); //????
+            exit;
+        }
+
+        if (($banned = $this->c->get('CheckBans')->check($this->c->get('user'))) !== null) {
+            return $this->c->get('Ban')->ban($banned);
+        }
+    }
+}

+ 118 - 0
app/Controllers/Routing.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace ForkBB\Controllers;
+
+use R2\DependencyInjection\ContainerInterface;
+
+class Routing
+{
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    protected $c;
+
+    /**
+     * Конструктор
+     * @param array $config
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        $this->c = $container;
+    }
+
+    /**
+     * Маршрутиризация
+     * @return Page
+     */
+    public function routing()
+    {
+        $user = $this->c->get('user');
+        $config = $this->c->get('config');
+        $r = $this->c->get('Router');
+
+        // регистрация/вход/выход
+        if ($user['is_guest']) {
+            // вход
+            $r->add('GET', '/login', 'Auth:login', 'Login');
+            $r->add('POST', '/login', 'Auth:loginPost');
+            $r->add('GET', '/login/forget', 'Auth:forget', 'Forget');
+            // регистрация
+            if ($config['o_regs_allow'] == '1') {
+                $r->add('GET', '/registration', 'Registration:reg', 'Registration'); //????
+            }
+        } else {
+            // выход
+            $r->add('GET', '/logout/{token}', 'Auth:logout', 'Logout');
+        }
+        // просмотр разрешен
+        if ($user['g_read_board'] == '1') {
+            // главная
+            $r->add('GET', '/', 'Index:view', 'Index');
+            // правила
+            if ($config['o_rules'] == '1' && (! $user['is_guest'] || $config['o_regs_allow'] == '1')) {
+                $r->add('GET', '/rules', 'Rules:view', 'Rules');
+            }
+            // поиск
+            if ($user['g_search'] == '1') {
+                $r->add('GET', '/search', 'Search:view', 'Search');
+            }
+            // юзеры
+            if ($user['g_view_users'] == '1') {
+                // список пользователей
+                $r->add('GET', '/userlist[/page/{page}]', 'Userlist:view', 'Userlist');
+                // юзеры
+                $r->add('GET', '/user/{id:\d+}[/{name}]', 'Profile:view', 'User'); //????
+            }
+
+            // разделы
+            $r->add('GET', '/forum/{id:\d+}[/{name}][/page/{page:\d+}]', 'Forum:view', 'Forum');
+            // темы
+            $r->add('GET', '/post/{id:\d+}#p{id}', 'Topic:viewpost', 'viewPost');
+
+        }
+        // админ и модератор
+        if ($user['is_admmod']) {
+            $r->add('GET', '/admin/', 'AdminIndex:index', 'Admin');
+            $r->add('GET', '/admin/statistics', 'AdminStatistics:statistics', 'AdminStatistics');
+        }
+        // только админ
+        if ($user['g_id'] == PUN_ADMIN) {
+            $r->add('GET', '/admin/statistics/info', 'AdminStatistics:info', 'AdminInfo');
+        }
+
+        $uri = $_SERVER['REQUEST_URI'];
+        if (($pos = strpos($uri, '?')) !== false) {
+            $uri = substr($uri, 0, $pos);
+        }
+        $uri = rawurldecode($uri);
+
+        $route = $r->route($_SERVER['REQUEST_METHOD'], $uri);
+        $page = null;
+        switch ($route[0]) {
+            case $r::OK:
+                // ... 200 OK
+                list($page, $action) = explode(':', $route[1], 2);
+                $page = $this->c->get($page)->$action($route[2]);
+                break;
+            case $r::NOT_FOUND:
+                // ... 404 Not Found
+                if ($user['g_read_board'] != '1' && $user['is_guest']) {
+                    $page = $this->c->get('Redirect')->setPage('Login');
+                } else {
+//                  $page = $this->c->get('Message')->message('Bad request');
+                }
+                break;
+            case $r::METHOD_NOT_ALLOWED:
+                // ... 405 Method Not Allowed
+                $page = $this->c->get('Message')->message('Bad request', true, 405, ['Allow: ' . implode(',', $route[1])]);
+                break;
+            case $r::NOT_IMPLEMENTED:
+                // ... 501 Not implemented
+                $page = $this->c->get('Message')->message('Bad request', true, 501);
+                break;
+        }
+        return $page;
+    }
+
+}

+ 0 - 7
app/Core/Blank.php

@@ -1,7 +0,0 @@
-<?php
-
-namespace ForkBB\Core;
-
-class Blank
-{
-}

+ 109 - 29
app/Core/Install.php

@@ -3,9 +3,9 @@
 namespace ForkBB\Core;
 
 use R2\DependencyInjection\ContainerInterface;
-use R2\DependencyInjection\ContainerAwareInterface;
+use RuntimeException;
 
-class Install implements ContainerAwareInterface
+class Install
 {
     /**
      * @var Request
@@ -19,28 +19,24 @@ class Install implements ContainerAwareInterface
     protected $c;
 
     /**
-     * Инициализация контейнера
-     *
-     * @param ContainerInterface $container
+     * Конструктор
+     * @param Request $request
      */
-    public function setContainer(ContainerInterface $container)
+    public function __construct($request, ContainerInterface $container)
     {
+        $this->request = $request;
         $this->c = $container;
     }
 
     /**
-     * @param Request $request
+     * @throws \RuntimeException
+     * @return string
      */
-    public function __construct($request)
-    {
-        $this->request = $request;
-    }
-
     protected function generate_config_file($base_url, $db_type, $db_host, $db_name, $db_username, $db_password, $db_prefix, $cookie_prefix)
     {
-        $config = file_get_contents($this->c->getParameter('DIR_CONFIG') . 'main.dist.php');
+        $config = file_get_contents($this->c->getParameter('DIR_CONFIG') . '/main.dist.php');
         if (false === $config) {
-            exit('Error open main.dist.php'); //????
+            throw new RuntimeException('No access to main.dist.php.');
         }
         $config = str_replace('_BASE_URL_', addslashes($base_url), $config);
         $config = str_replace('_DB_TYPE_', addslashes($db_type), $config);
@@ -55,7 +51,10 @@ class Install implements ContainerAwareInterface
         return $config;
     }
 
-    public function start()
+    /**
+     * @throws \RuntimeException
+     */
+    public function install()
     {
 
         /**
@@ -83,11 +82,12 @@ class Install implements ContainerAwareInterface
         require PUN_ROOT . 'lang/' . $install_lang . '/install.php';
 
         // Make sure we are running at least MIN_PHP_VERSION
-        if (! function_exists('version_compare') || version_compare(PHP_VERSION, MIN_PHP_VERSION, '<'))
-            exit(sprintf($lang_install['You are running error'], 'PHP', PHP_VERSION, FORK_VERSION, MIN_PHP_VERSION));
+        if (! function_exists('version_compare') || version_compare(PHP_VERSION, MIN_PHP_VERSION, '<')) {
+            throw new RuntimeException(sprintf($lang_install['You are running error'], 'PHP', PHP_VERSION, FORK_VERSION, MIN_PHP_VERSION));
+        }
 
 
-        if (null !== $this->request->postBool('generate_config'))
+        if ($this->request->isPost('generate_config'))
         {
             header('Content-Type: text/x-delimtext; name="main.php"');
             header('Content-disposition: attachment; filename=main.php');
@@ -102,11 +102,11 @@ class Install implements ContainerAwareInterface
             $cookie_prefix = $this->request->postStr('cookie_prefix', '');
 
             echo $this->generate_config_file($base_url, $db_type, $db_host, $db_name, $db_username, $db_password, $db_prefix, $cookie_prefix);
-            exit;
+            exit; //????
         }
 
 
-        if (null === $this->request->postBool('form_sent'))
+        if (! $this->request->isPost('form_sent'))
         {
             // Make an educated guess regarding base_url
             $base_url  = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';    // protocol
@@ -191,7 +191,7 @@ class Install implements ContainerAwareInterface
         if (! forum_is_writable(PUN_ROOT . 'img/avatars/'))
             $alerts[] = sprintf($lang_install['Alert avatar'], PUN_ROOT . 'img/avatars/');
 
-        if (null === $this->request->postBool('form_sent') || ! empty($alerts))
+        if (! $this->request->isPost('form_sent') || ! empty($alerts))
         {
             // Determine available database extensions
             $db_extensions = array();
@@ -943,18 +943,29 @@ foreach ($styles as $temp)
                         'datatype'        => 'INT(10) UNSIGNED',
                         'allow_null'    => true
                     ),
-                    'witt_data'            => array(
+                    'witt_data'            => array(              //????
                         'datatype'        => 'VARCHAR(255)',
                         'allow_null'    => false,
                         'default'        => '\'\''
-                    )
+                    ),
+                    'o_position' => [
+                        'datatype'   => 'VARCHAR(100)',
+                        'allow_null' => false,
+                        'default'    => '\'\''
+                    ],
+                    'o_name' => [
+                        'datatype'   => 'VARCHAR(200)',
+                        'allow_null' => false,
+                        'default'    => '\'\''
+                    ],
                 ),
                 'UNIQUE KEYS'    => array(
                     'user_id_ident_idx'    => array('user_id', 'ident')
                 ),
                 'INDEXES'        => array(
-                    'ident_idx'        => array('ident'),
-                    'logged_idx'    => array('logged')
+                    'ident_idx'      => array('ident'),
+                    'logged_idx'     => array('logged'),
+                    'o_position_idx' => array('o_position'),
                 )
             );
 
@@ -1718,7 +1729,11 @@ foreach ($styles as $temp)
                         'datatype'        => 'TINYINT(4) UNSIGNED',
                         'allow_null'    => false,
                         'default'        => '0'
-                    )
+                    ),
+                    'u_mark_all_read'   => array(
+                        'datatype'      => 'INT(10) UNSIGNED',
+                        'allow_null'    => true
+                    ),
                 ),
                 'PRIMARY KEY'    => array('id'),
                 'UNIQUE KEYS'    => array(
@@ -1876,6 +1891,73 @@ foreach ($styles as $temp)
 
             $db->create_table('poll_voted', $schema) or error('Unable to create table poll_voted', __FILE__, __LINE__, $db->error());
 
+            $schema = [
+                'FIELDS'  => [
+                    'uid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'fid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mf_upper' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mf_lower' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                ],
+                'UNIQUE KEYS' => [
+                    'uid_fid_idx'    => ['uid', 'fid'],
+                ],
+                'INDEXES' => [
+                    'mf_upper_idx'   => ['mf_upper'],
+                    'mf_lower_idx'   => ['mf_lower'],
+                ]
+            ];
+
+            $db->create_table('mark_of_forum', $schema) or error('Unable to create mark_of_forum table', __FILE__, __LINE__, $db->error());
+
+            $schema = [
+                'FIELDS'  => [
+                    'uid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'fid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'tid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mt_upper' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mt_lower' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                ],
+                'UNIQUE KEYS' => [
+                    'uid_fid_tid_idx' => ['uid', 'fid', 'tid'],
+                ],
+                'INDEXES' => [
+                    'mt_upper_idx'   => ['mt_upper'],
+                    'mt_lower_idx'   => ['mt_lower'],
+                ]
+            ];
+
+            $db->create_table('mark_of_topic', $schema) or error('Unable to create mark_of_topic table', __FILE__, __LINE__, $db->error());
+
+
+
+
 
             $now = time();
 
@@ -2047,7 +2129,7 @@ foreach ($styles as $temp)
             $written = false;
             if (forum_is_writable($this->c->getParameter('DIR_CONFIG')))
             {
-                $fh = @fopen($this->c->getParameter('DIR_CONFIG') . 'main.php', 'wb');
+                $fh = @fopen($this->c->getParameter('DIR_CONFIG') . '/main.php', 'wb');
                 if ($fh)
                 {
                     fwrite($fh, $config);
@@ -2157,7 +2239,5 @@ else
 <?php
 
         }
-
-        exit;
     }
 }

+ 260 - 0
app/Core/Lang.php

@@ -0,0 +1,260 @@
+<?php
+
+namespace ForkBB\Core;
+
+use R2\DependencyInjection\ContainerInterface;
+use RuntimeException;
+
+class Lang
+{
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    protected $c;
+
+    /**
+     * Массив переводов
+     * @var array
+     */
+    protected $tr = [];
+
+    /**
+     * Загруженные переводы
+     * @var array
+     */
+    protected $loaded = [];
+
+    /**
+     * Конструктор
+     * @param ContainerInterface $container
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        $this->c = $container;
+        __($this);
+    }
+
+    /**
+     * Ищет сообщение в загруженных переводах
+     * @param string $message
+     * @param string $lang
+     * @return string|array
+     */
+    public function get($message, $lang = null)
+    {
+        if ($lang && isset($this->tr[$lang][$message])) {
+            return $this->tr[$lang][$message];
+        }
+
+        foreach ($this->tr as $lang) {
+            if (isset($lang[$message])) {
+                return $lang[$message];
+            }
+        }
+
+        return $message;
+    }
+
+    /**
+     * Загрузка языкового файла
+     * @param string $name
+     * @param string $lang
+     * @param string $path
+     */
+    public function load($name, $lang = null, $path = null)
+    {
+        if ($lang) {
+            if (isset($this->loaded[$name][$lang])) {
+                return;
+            }
+        } elseif (isset($this->loaded[$name])) {
+            return;
+        }
+        $lang = $lang ?: $this->c->get('user')['language'];
+        $path = $path ?: $this->c->getParameter('DIR_TRANSL');
+        do {
+            $flag = true;
+            $fullPath = $path . '/'. $lang . '/' . $name . '.po';
+            if (file_exists($fullPath)) {
+                $file = file_get_contents($fullPath);
+                if (isset($this->tr[$lang])) {
+                    $this->tr[$lang] += $this->arrayFromStr($file);
+                } else {
+                    $this->tr[$lang] = $this->arrayFromStr($file);
+                }
+                $flag = false;
+            } elseif ($lang === 'English') {
+                $flag = false;
+            }
+            $lang = 'English';
+        } while ($flag);
+
+        $this->loaded[$name][$lang] = true;
+    }
+
+    /**
+     * Получение массива перевода из строки (.po файла)
+     * @param string $str
+     * @return array
+     * @throws RuntimeException
+     */
+    protected function arrayFromStr($str)
+    {
+        $lines = explode("\n", $str);
+        $count = count($lines);
+        $result = [];
+        $cur = [];
+        $curComm = null;
+        $curVal = '';
+        $nplurals = 2;
+        $plural = '($n != 1);';
+
+        for ($i = 0; $i < $count; ++$i) {
+            $line = trim($lines[$i]);
+
+            // пустая строка
+            if (! isset($line{0})) {
+                // промежуточные данные
+                if (isset($curComm)) {
+                    $cur[$curComm] = $curVal;
+                }
+
+                // ошибка формата
+                if (! isset($cur['msgid'])) {
+                    throw new RuntimeException('File format error');
+                }
+
+                // заголовки
+                if (! isset($cur['msgid']{0})) {
+                    if (preg_match('%Plural\-Forms:\s+nplurals=(\d+);\s*plural=([^;\n\r]+;)%i', $cur[0], $v)) {
+                        $nplurals = (int) $v[1];
+                        $plural = str_replace('n', '$n', trim($v[2]));
+                        $plural = str_replace(':', ': (', $plural, $curVal);
+                        $plural = str_replace(';', str_repeat(')', $curVal). ';', $plural);
+                    }
+
+                // перевод
+                } else {
+                    // множественный
+                    if (isset($cur['msgid_plural']{0}) || isset($cur[1]{0})) {
+                        if (! isset($cur[1]{0})) {
+                            $cur[1] = $cur['msgid_plural'];
+                        }
+
+                        if (! isset($cur[0]{0})) {
+                            $cur[0] = $cur['msgid'];
+                        }
+
+                        $curVal = [];
+                        for ($v = 0; $v < $nplurals; ++$v) {
+                            if (! isset($cur[$v]{0})) {
+                                $curVal = null;
+                                break;
+                            }
+                            $curVal[$v] = $cur[$v];
+                        }
+
+                        if (isset($curVal)) {
+                            $curVal['plural'] = $plural;
+                            $result[$cur['msgid']] = $curVal;
+                        }
+
+                    // одиночный
+                    } elseif (isset($cur[0]{0})) {
+                        $result[$cur['msgid']] = $cur[0];
+                    }
+                }
+
+                $curComm = null;
+                $curVal = '';
+                $cur = [];
+                continue;
+
+            // комментарий
+            } elseif ($line{0} == '#') {
+                continue;
+
+            // многострочное содержимое
+            } elseif ($line{0} == '"') {
+                if (isset($curComm)) {
+                    $curVal .= $this->originalLine($line);
+                }
+                continue;
+
+            // промежуточные данные
+            } elseif (isset($curComm)) {
+                $cur[$curComm] = $curVal;
+            }
+
+            // выделение команды
+            $v = explode(' ', $line, 2);
+            $command = $v[0];
+            $v = isset($v[1]) ? $this->originalLine(trim($v[1])) : '';
+
+            switch ($command) {
+                case 'msgctxt':
+                case 'msgid':
+                case 'msgid_plural':
+                    $curComm = $command;
+                    $curVal = $v;
+                    break;
+
+                case 'msgstr':
+                case 'msgstr[0]':
+                    $curComm = 0;
+                    $curVal = $v;
+                    break;
+
+                case 'msgstr[1]':
+                    $curComm = 1;
+                    $curVal = $v;
+                    break;
+
+                case 'msgstr[2]':
+                    $curComm = 2;
+                    $curVal = $v;
+                    break;
+
+                case 'msgstr[3]':
+                    $curComm = 3;
+                    $curVal = $v;
+                    break;
+
+                case 'msgstr[4]':
+                    $curComm = 4;
+                    $curVal = $v;
+                    break;
+
+                case 'msgstr[5]':
+                    $curComm = 5;
+                    $curVal = $v;
+                    break;
+
+                default:
+                    throw new RuntimeException('File format error');
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Получение оригинальной строки с удалением кавычек
+     * и преобразованием спецсимволов
+     * @param string $line
+     * @return string
+     */
+    protected function originalLine($line)
+    {
+        if (isset($line[1]) && $line{0} == '"' && $line{strlen($line) - 1} == '"') {
+            $line = substr($line, 1, -1);
+        }
+        return str_replace(
+            ['\\n', '\\t', '\\"', '\\\\'],
+            ["\n",  "\t",  '"',  '\\'],
+            $line
+        );
+    }
+
+}

+ 133 - 0
app/Core/Model.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace ForkBB\Core;
+
+use \ArrayObject;
+use \ArrayIterator;
+use \InvalidArgumentException;
+
+class Model extends ArrayObject
+{
+    /**
+     * @var array
+     */
+    protected $master;
+
+    /**
+     * @var array
+     */
+    protected $current;
+
+    /**
+     * @param array $data
+     */
+    public function __construct(array $data = [])
+    {
+        $this->master = $data;
+        $this->current = $data;
+    }
+
+    /**
+     * @param int|string $key
+     *
+     * @return mixed
+     */
+    public function offsetGet($key)
+    {
+        $this->verifyKey($key);
+        if (isset($this->current[$key])) {
+            return $this->current[$key];
+        }
+    }
+
+    /**
+     * @param int|string $key
+     * @param mixed @value
+     */
+    public function offsetSet($key, $value)
+    {
+        $this->verifyKey($key, true);
+        if (null === $key) {
+            $this->current[] = $value;
+        } else {
+            $this->current[$key] = $value;
+        }
+    }
+
+    /**
+     * @param int|string $key
+     */
+    public function offsetUnset($key)
+    {
+        $this->verifyKey($key);
+        unset($this->current[$key]);
+    }
+
+    /**
+     * @param int|string $key
+     *
+     * @return bool
+     */
+    public function offsetExists($key)
+    {
+        $this->verifyKey($key);
+        return isset($this->current[$key]) || array_key_exists($key, $this->current);
+    }
+
+    /**
+     * @return ArrayIterator
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->current);
+    }
+
+    /**
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->current);
+    }
+
+    /**
+     * @param mixed $value
+     */
+    public function append($value)
+    {
+        $this->current[] = $value;
+    }
+
+    /**
+     * @return array
+     */
+    public function export()
+    {
+        return $this->current;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isModify()
+    {
+        return $this->master !== $this->current;
+    }
+
+    /**
+     * @param mixed $key
+     * @param bool $allowedNull
+     *
+     * @throw InvalidArgumentException
+     */
+    protected function verifyKey($key, $allowedNull = false)
+    {
+        if (is_string($key)
+            || is_int($key)
+            || ($allowedNull && null === $key)
+        ) {
+            return;
+        }
+        throw new InvalidArgumentException('Key should be string or integer');
+    }
+}

+ 330 - 0
app/Core/Router.php

@@ -0,0 +1,330 @@
+<?php
+
+namespace ForkBB\Core;
+
+class Router
+{
+    const OK = 200;
+    const NOT_FOUND = 404;
+    const METHOD_NOT_ALLOWED = 405;
+    const NOT_IMPLEMENTED = 501;
+
+    /**
+     * Массив постоянных маршрутов
+     * @var array
+     */
+    protected $statical = [];
+
+    /**
+     * Массив динамических маршрутов
+     * @var array
+     */
+    protected $dynamic = [];
+
+    /**
+     * Список методов доступа
+     * @var array
+     */
+    protected $methods = [];
+
+    /**
+     * Массив для построения реальных ссылок
+     * @var array
+     */
+    protected $links = [];
+
+    /**
+     * Базовый url сайта
+     * @var string
+     */
+    protected $baseUrl;
+
+    /**
+     * Host сайта
+     * @var string
+     */
+    protected $host;
+
+    /**
+     * Префикс uri
+     * @var string
+     */
+    protected $prefix;
+
+    /**
+     * Длина префикса в байтах
+     * @var int
+     */
+    protected $length;
+
+    /**
+     * Конструктор
+     * @param string $prefix
+     */
+    public function __construct($base = '')
+    {
+        $this->baseUrl = $base;
+        $this->host = parse_url($base, PHP_URL_HOST);
+        $this->prefix = parse_url($base, PHP_URL_PATH);
+        $this->length = strlen($this->prefix);
+    }
+
+    /**
+     * Проверка url на принадлежность форуму
+     * @param string $url
+     * @param string $defMarker
+     * @param array $defArgs
+     * @return string
+     */
+    public function validate($url, $defMarker, array $defArgs = [])
+    {
+        if (parse_url($url, PHP_URL_HOST) === $this->host
+            && ($route = $this->route('GET', rawurldecode(parse_url($url, PHP_URL_PATH))))
+            && $route[0] === self::OK
+        ) {
+            return $url;
+        } else {
+            return $this->link($defMarker, $defArgs);
+        }
+    }
+
+    /**
+     * Возвращает реальный url
+     * @param string $marker
+     * @param array $args
+     * @return string
+     */
+    public function link($marker, array $args = [])
+    {
+        $result = $this->baseUrl; //???? http и https
+        if (isset($this->links[$marker])) {
+            $s = $this->links[$marker];
+            foreach ($args as $key => $val) {
+                if ($key == '#') {
+                    $s .= '#' . rawurlencode($val); //????
+                    continue;
+                }
+                $s = preg_replace(
+                    '%\{' . preg_quote($key, '%') . '(?::[^{}]+)?\}%',
+                    rawurlencode($val),
+                    $s
+                );
+            }
+            $s = preg_replace('%\[[^{}\[\]]*\{[^}]+\}[^{}\[\]]*\]%', '', $s);
+            if (strpos($s, '{') === false) {
+                $result .= str_replace(['[', ']'], '', $s);
+            } else {
+                $result .= '/';
+            }
+        } else {
+            $result .= '/';
+        }
+        return $result;
+    }
+
+    /**
+     * Метод определяет маршрут
+     * @param string $method
+     * @param string $uri
+     * @return array
+     */
+    public function route($method, $uri)
+    {
+        $head = $method == 'HEAD';
+
+        if (empty($this->methods[$method]) && (! $head || empty($this->methods['GET']))) {
+            return [self::NOT_IMPLEMENTED];
+        }
+
+        if ($this->length) {
+            if (0 === strpos($uri, $this->prefix)) {
+                $uri = substr($uri, $this->length);
+            } else {
+                return [self::NOT_FOUND];
+            }
+        }
+
+        $allowed = [];
+
+        if (isset($this->statical[$uri])) {
+            if (isset($this->statical[$uri][$method])) {
+                return [self::OK, $this->statical[$uri][$method], []];
+            } elseif ($head && isset($this->statical[$uri]['GET'])) {
+                return [self::OK, $this->statical[$uri]['GET'], []];
+            } else {
+                $allowed = array_keys($this->statical[$uri]);
+            }
+        }
+
+        $pos = strpos(substr($uri, 1), '/');
+        $base = false === $pos ? $uri : substr($uri, 0, ++$pos);
+
+        if (isset($this->dynamic[$base])) {
+            foreach ($this->dynamic[$base] as $pattern => $data) {
+                if (! preg_match($pattern, $uri, $matches)) {
+                    continue;
+                }
+
+                if (isset($data[$method])) {
+                    $data = $data[$method];
+                } elseif ($head && isset($data['GET'])) {
+                    $data = $data['GET'];
+                } else {
+                    $allowed += array_keys($data);
+                    continue;
+                }
+
+                $args = [];
+                foreach ($data[1] as $key) {
+                    if (isset($matches[$key])) {
+                        $args[$key] = $matches[$key];
+                    }
+                }
+                return [self::OK, $data[0], $args];
+            }
+        }
+
+
+        if (empty($allowed)) {
+            return [self::NOT_FOUND];
+        } else {
+            return [self::METHOD_NOT_ALLOWED, $allowed];
+        }
+    }
+
+    /**
+     * Метод добавдяет маршрут
+     * @param string|array $method
+     * @param string $route
+     * @param string $handler
+     * @param string $marker
+     */
+    public function add($method, $route, $handler, $marker = null)
+    {
+        if (is_array($method)) {
+            foreach ($method as $m) {
+                $this->methods[$m] = 1;
+            }
+        } else {
+            $this->methods[$method] = 1;
+        }
+
+        $link = $route;
+        if (($pos = strpos($route, '#')) !== false) {
+            $route = substr($route, 0, $pos);
+        }
+
+        if (false === strpbrk($route, '{}[]')) {
+            if (is_array($method)) {
+                foreach ($method as $m) {
+                    $this->statical[$route][$m] = $handler;
+                }
+            } else {
+                $this->statical[$route][$method] = $handler;
+            }
+        } else {
+            $data = $this->parse($route);
+            if (false === $data) {
+                throw new \Exception('Route is incorrect');
+            }
+            if (is_array($method)) {
+                foreach ($method as $m) {
+                    $this->dynamic[$data[0]][$data[1]][$m] = [$handler, $data[2]];
+                }
+            } else {
+                $this->dynamic[$data[0]][$data[1]][$method] = [$handler, $data[2]];
+            }
+        }
+
+        if ($marker) {
+            $this->links[$marker] = $link;
+        }
+    }
+
+    /**
+     * Метод разбирает динамический маршрут
+     * @param string $route
+     * @return array|false
+     */
+    protected function parse($route)
+    {
+        $parts = preg_split('%([\[\]{}/])%', $route, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+
+        $s = 1;
+        $base = $parts[0];
+        if ($parts[0] === '/') {
+            $s = 2;
+            $base .= $parts[1];
+        }
+        if (isset($parts[$s]) && $parts[$s] !== '/' && $parts[$s] !== '[') {
+            $base = '/';
+        }
+
+        $pattern = '%^';
+        $var = false;
+        $first = false;
+        $buffer = '';
+        $args = [];
+        $s = 0;
+
+        foreach ($parts as $part) {
+            if ($var) {
+                switch ($part) {
+                    case '{':
+                        return false;
+                    case '}':
+                        $data = explode(':', $buffer, 2);
+                        if (! isset($data[1])) {
+                            $data[1] = '[^/\x00-\x1f]+';
+                        }
+                        if ($data[0] === '' || $data[1] === '' || is_numeric($data[0]{0})) {
+                            return false;
+                        }
+                        $pattern .= '(?P<' . $data[0] . '>' . $data[1] . ')';
+                        $args[] = $data[0];
+                        $var = false;
+                        $buffer = '';
+                        break;
+                    default:
+                        $buffer .= $part;
+                }
+            } elseif ($first) {
+                switch ($part) {
+                    case '/':
+                        $first = false;
+                        $pattern .= preg_quote($part, '%');
+                        break;
+                    default:
+                        return false;
+                }
+            } else {
+                switch ($part) {
+                    case '[':
+                        ++$s;
+                        $pattern .= '(?:';
+                        $first = true;
+                        break;
+                    case ']':
+                        --$s;
+                        if ($s < 0) {
+                            return false;
+                        }
+                        $pattern .= ')?';
+                        break;
+                    case '{':
+                        $var = true;
+                        break;
+                    case '}':
+                        return false;
+                    default:
+                        $pattern .= preg_quote($part, '%');
+                }
+            }
+        }
+        if ($var || $s) {
+            return false;
+        }
+        $pattern .= '$%D';
+        return [$base, $pattern, $args];
+    }
+}

+ 73 - 0
app/Core/View.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace ForkBB\Core;
+
+use R2\Templating\Dirk;
+use ForkBB\Models\Pages\Page;
+use RuntimeException;
+
+class View extends Dirk
+{
+    protected $page;
+
+    public function __construct ($cache, $views)
+    {
+        $config = [
+            'views'     => $views,
+            'cache'     => $cache,
+            'ext'       => '.tpl',
+            'echo'      => 'htmlspecialchars(%s, ENT_HTML5 | ENT_QUOTES | ENT_SUBSTITUTE, \'UTF-8\')',
+            'separator' => '/',
+        ];
+        $this->compilers[] = 'Transformations';
+
+        parent::__construct($config);
+    }
+
+    /**
+     * Трансформация скомпилированного шаблона
+     * @param string $value
+     * @return string
+     */
+    protected function compileTransformations($value)
+    {
+        if (strpos($value, '<!--inline-->') === false) {
+            return $value;
+        }
+        return preg_replace_callback(
+            '%<!--inline-->([^<]*(?:<(?!!--endinline-->)[^<]*)*+)(?:<!--endinline-->)?%',
+            function ($matches) {
+                return preg_replace('%\h*\R\s*%', '', $matches[1]);
+            },
+            $value);
+    }
+
+    public function setPage(Page $page)
+    {
+        if (true !== $page->isReady()) {
+            throw new RuntimeException('The page model does not contain data ready');
+        }
+        $this->page = $page;
+        return $this;
+    }
+
+    public function outputPage()
+    {
+        if (empty($this->page)) {
+            throw new RuntimeException('The page model isn\'t set');
+        }
+
+        $headers = $this->page->getHeaders();
+        foreach ($headers as $header) {
+            header($header);
+        }
+
+        $tpl = $this->page->getNameTpl();
+        // переадресация
+        if (null === $tpl) {
+            return null;
+        }
+
+        return $this->fetch($tpl, $this->page->getData());
+    }
+}

+ 55 - 7
app/Models/Actions/CacheGenerator.php

@@ -3,6 +3,7 @@
 namespace ForkBB\Models\Actions;
 
 //use ForkBB\Core\DB;
+use ForkBB\Models\User;
 
 class CacheGenerator
 {
@@ -13,7 +14,6 @@ class CacheGenerator
 
     /**
      * Конструктор
-     *
      * @param ForkBB\Core\DB $db
      */
     public function __construct($db)
@@ -23,7 +23,6 @@ class CacheGenerator
 
     /**
      * Возвращает массив конфигурации форума
-     *
      * @return array
      */
     public function config()
@@ -40,7 +39,6 @@ class CacheGenerator
 
     /**
      * Возвращает массив банов
-     *
      * @return array
      */
     public function bans()
@@ -57,7 +55,6 @@ class CacheGenerator
 
     /**
      * Возвращает массив слов попадающий под цензуру
-     *
      * @return array
      */
     public function censoring()
@@ -81,7 +78,6 @@ class CacheGenerator
     /**
      * Возвращает информацию о последнем зарегистрированном пользователе и
      * общем числе пользователей
-     *
      * @return array
      */
     public function usersInfo()
@@ -99,7 +95,6 @@ class CacheGenerator
 
     /**
      * Возвращает спимок id админов
-     *
      * @return array
      */
     public function admins()
@@ -117,7 +112,6 @@ class CacheGenerator
 
     /**
      * Возвращает массив с описанием смайлов
-     *
      * @return array
      */
     public function smilies()
@@ -131,4 +125,58 @@ class CacheGenerator
 
         return $arr;
     }
+
+    /**
+     * Возвращает массив с описанием форумов для текущего пользователя
+     * @return array
+     */
+    public function forums(User $user)
+    {
+        $groupId = $user['g_id'];
+		$result = $this->db->query('SELECT g_read_board FROM '.$this->db->prefix.'groups WHERE g_id='.$groupId) or error('Unable to fetch user group read permission', __FILE__, __LINE__, $this->db->error());
+		$read = $this->db->result($result);
+
+        $tree = $desc = $asc = [];
+
+        if ($read) {
+            $result = $this->db->query('SELECT c.id AS cid, c.cat_name, f.id AS fid, f.forum_name, f.redirect_url, f.parent_forum_id, f.disp_position FROM '.$this->db->prefix.'categories AS c INNER JOIN '.$this->db->prefix.'forums AS f ON c.id=f.cat_id LEFT JOIN '.$this->db->prefix.'forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id='.$groupId.') WHERE fp.read_forum IS NULL OR fp.read_forum=1 ORDER BY c.disp_position, c.id, f.disp_position', true) or error('Unable to fetch category/forum list', __FILE__, __LINE__, $this->db->error());
+
+            while ($f = $this->db->fetch_assoc($result)) {
+                $tree[$f['parent_forum_id']][$f['fid']] = $f;
+            }
+            $this->forumsDesc($desc, $tree);
+            $this->forumsAsc($asc, $tree);
+        }
+
+        return [$tree, $desc, $asc];
+    }
+
+    protected function forumsDesc(&$list, $tree, $node = 0)
+    {
+        if (empty($tree[$node])) {
+            return;
+        }
+        foreach ($tree[$node] as $id => $forum) {
+            $list[$id] = $node ? array_merge([$node], $list[$node]) : []; //????
+            $list[$id]['forum_name'] = $forum['forum_name'];
+            $this->forumsDesc($list, $tree, $id);
+        }
+    }
+
+    protected function forumsAsc(&$list, $tree, $nodes = [0])
+    {
+        $list[$nodes[0]][] = $nodes[0];
+
+        if (empty($tree[$nodes[0]])) {
+            return;
+        }
+        foreach ($tree[$nodes[0]] as $id => $forum) {
+            $temp = [$id];
+            foreach ($nodes as $i) {
+                $list[$i][] = $id;
+                $temp[] = $i;
+            }
+            $this->forumsAsc($list, $tree, $temp);
+        }
+    }
 }

+ 33 - 18
app/Models/Actions/CacheLoader.php

@@ -4,10 +4,9 @@ namespace ForkBB\Models\Actions;
 
 use ForkBB\Core\Cache;
 use R2\DependencyInjection\ContainerInterface;
-use R2\DependencyInjection\ContainerAwareInterface;
 use InvalidArgumentException;
 
-class CacheLoader implements ContainerAwareInterface
+class CacheLoader
 {
     /**
      * Контейнер
@@ -15,16 +14,6 @@ class CacheLoader implements ContainerAwareInterface
      */
     protected $c;
 
-    /**
-     * Инициализация контейнера
-     *
-     * @param ContainerInterface $container
-     */
-    public function setContainer(ContainerInterface $container)
-    {
-        $this->c = $container;
-    }
-
     /**
      * @var ForkBB\Core\Cache
      */
@@ -32,22 +21,21 @@ class CacheLoader implements ContainerAwareInterface
 
     /**
      * Конструктор
-     *
-     * @param ForkBB\Core\Cache $cache
+     * @param Cache $cache
+     * @param ContainerInterface $container
      */
-    public function __construct(Cache $cache)
+    public function __construct(Cache $cache, ContainerInterface $container)
     {
         $this->cache = $cache;
+        $this->c = $container;
     }
 
     /**
      * Загрузка данных из кэша (генерация кэша при отсутствии или по требованию)
-     *
      * @param string $key
      * @param bool $update
-     *
-     * @throws \InvalidArgumentException
      * @return mixed
+     * @throws \InvalidArgumentException
      */
     public function load($key, $update = false)
     {
@@ -65,4 +53,31 @@ class CacheLoader implements ContainerAwareInterface
             return $value;
         }
     }
+
+    /**
+     * Загрузка данных по разделам из кэша (генерация кэша при условии)
+     * @return array
+     */
+    public function loadForums()
+    {
+        $mark = $this->cache->get('forums_mark');
+        $key = 'forums_' . $this->c->get('user')['g_id'];
+
+        if (empty($mark)) {
+            $this->cache->set('forums_mark', time());
+            $value = $this->c->get('get forums');
+            $this->cache->set($key, [time(), $value]);
+            return $value;
+        }
+
+        $result = $this->cache->get($key);
+
+        if (empty($result) || $result[0] < $mark) {
+            $value = $this->c->get('get forums');
+            $this->cache->set($key, [time(), $value]);
+            return $value;
+        }
+
+        return $result[1];
+    }
 }

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

@@ -0,0 +1,98 @@
+<?php
+
+namespace ForkBB\Models\Actions;
+
+use ForkBB\Models\User;
+use R2\DependencyInjection\ContainerInterface;
+
+class CheckBans
+{
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    protected $c;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->c = $container;
+    }
+
+    /**
+     * Возвращает массив с описанием бана или null
+     * @param User $user
+     *
+     * @return null|array
+     */
+    public function check(User $user) //????
+    {
+        $bans = $this->c->get('bans');
+
+        // Для админов и при отсутствии банов прекращаем проверку
+        if ($user['g_id'] == PUN_ADMIN || empty($bans)) {
+           return null;
+        }
+
+        // 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
+        $user_ip = get_remote_address();
+        $add = strpos($user_ip, '.') !== false ? '.' : ':';
+        $user_ip .= $add;
+
+        $username = mb_strtolower($user['username']);
+
+        $banned = false;
+        $remove = [];
+
+        foreach ($bans as $cur)
+        {
+            // Has this ban expired?
+            if ($cur['expire'] != '' && $cur['expire'] <= time())
+            {
+                $remove[] = $cur['id'];
+                continue;
+            } elseif ($banned) {
+                continue;
+            }
+
+            if (! $user['is_guest']) {
+                if ($cur['username'] != '' && $username == mb_strtolower($cur['username'])) {
+                    $banned = $cur;
+                    continue;
+                } elseif ($cur['email'] != '' && $user['email'] == $cur['email']) {
+                    $banned = $cur;
+                    continue;
+                }
+            }
+
+            if ($cur['ip'] != '')
+            {
+                $ips = explode(' ', $cur['ip']);
+                foreach ($ips as $ip) {
+                    $ip .= $add;
+                    if (substr($user_ip, 0, strlen($ip)) == $ip) {
+                        $banned = $cur;
+                        break;
+                    }
+                }
+            }
+        }
+
+        // If we removed any expired bans during our run-through, we need to regenerate the bans cache
+        if (! empty($remove))
+        {
+            $db = $this->c->get('DB');
+            $db->query('DELETE FROM '.$db->prefix.'bans WHERE id IN (' . implode(',', $remove) . ')') or error('Unable to delete expired ban', __FILE__, __LINE__, $db->error());
+            $this->c->get('bans update');
+        }
+
+        if ($banned)
+        {
+            //???? а зачем это надо?
+            $this->c->get('Online')->delete($user);
+            return $banned;
+        }
+
+        return null;
+    }
+}

+ 62 - 0
app/Models/Csrf.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Core\Secury;
+use ForkBB\Models\User;
+
+class Csrf
+{
+    /**
+     * @var Secury
+     */
+    protected $secury;
+
+    /**
+     * @var string
+     */
+    protected $key;
+
+    /**
+     * Конструктор
+     *
+     * @param Secury $secury
+     * @param User $user
+     */
+    public function __construct(Secury $secury, User $user)
+    {
+        $this->secury = $secury;
+        $this->key = sha1($user['password'] . $user['ip'] . $user['id']);
+    }
+
+    /**
+     * Возвращает csrf токен
+     * @param string $marker
+     * @param array $args
+     * @param string|int $time
+     * @return string
+     */
+    public function create($marker, array $args = [], $time = null)
+    {
+         unset($args['token'], $args['#']);
+         $data = $marker . '|' . json_encode($args);
+         $time = $time ?: time();
+         return $this->secury->hmac($data, $time . $this->key) . 'f' . $time;
+    }
+
+    /**
+     * Проверка токена
+     * @param string $token
+     * @param string $marker
+     * @param array $args
+     * @return bool
+     */
+    public function check($token, $marker, array $args = [])
+    {
+        return preg_match('%f(\d+)$%D', $token, $matches)
+            && $matches[1] < time()
+            && $matches[1] + 1800 > time()
+            && hash_equals($this->create($marker, $args, $matches[1]), $token);
+    }
+
+}

+ 242 - 0
app/Models/Online.php

@@ -0,0 +1,242 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\User;
+use ForkBB\Models\Pages\Page;
+use R2\DependencyInjection\ContainerInterface;
+use RuntimeException;
+
+class Online
+{
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    protected $c;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var DB
+     */
+    protected $db;
+
+    /**
+     * @var User
+     */
+    protected $user;
+
+    /**
+     * Флаг выполнения
+     * @var bool
+     */
+    protected $flag = false;
+
+    /**
+     * Контролер
+     * @param array $config
+     * @param DB $db
+     * @param User $user
+     * @param ContainerInterface $container
+     */
+    public function __construct(array $config, $db, User $user, ContainerInterface $container)
+    {
+        $this->config = $config;
+        $this->db = $db;
+        $this->user = $user;
+        $this->c = $container;
+    }
+
+    /**
+     * Обработка данных пользователей онлайн
+     * Обновление данных текущего пользователя
+     * Возврат данных по пользователям онлайн
+     * @param Page $page
+     * @return array
+     */
+    public function handle(Page $page)
+    {
+        if ($this->flag) {
+            throw new RuntimeException('Repeated execution of actions over online data');
+        }
+        $this->flag = true;
+
+        //   string     bool   bool
+        list($position, $type, $filter) = $page->getDataForOnline();  //???? возможно стоит возвращать полное имя страницы для отображение
+
+        $this->updateUser($position);
+
+        $all = 0;
+        $now = time();
+        $tOnline = $now - $this->config['o_timeout_online'];
+        $tVisit = $now - $this->config['o_timeout_visit'];
+        $users = $guests = $bots = [];
+        $deleteG = false;
+        $deleteU = false;
+        $setIdle = false;
+
+        if ($this->config['o_users_online'] == '1' && $type) {
+            $result = $this->db->query('SELECT user_id, ident, logged, idle, o_position, o_name FROM '.$this->db->prefix.'online ORDER BY logged') or error('Unable to fetch users from online list', __FILE__, __LINE__, $this->db->error());
+        } elseif ($type) {
+            $result = $this->db->query('SELECT user_id, ident, logged, idle FROM '.$this->db->prefix.'online ORDER BY logged') or error('Unable to fetch users from online list', __FILE__, __LINE__, $this->db->error());
+        } else {
+            $result = $this->db->query('SELECT user_id, ident, logged, idle FROM '.$this->db->prefix.'online WHERE logged<'.$tOnline) or error('Unable to fetch users from online list', __FILE__, __LINE__, $this->db->error());
+        }
+        while ($cur = $this->db->fetch_assoc($result)) {
+
+            // посетитель уже не онлайн (или почти не онлайн)
+            if ($cur['logged'] < $tOnline) {
+                // пользователь
+                if ($cur['user_id'] > 1) {
+                    if ($cur['logged'] < $tVisit) {
+                        $deleteU = true;
+                        $this->db->query('UPDATE '.$this->db->prefix.'users SET last_visit='.$cur['logged'].' WHERE id='.$cur['user_id']) or error('Unable to update user visit data', __FILE__, __LINE__, $this->db->error());
+                    } elseif ($cur['idle'] == '0') {
+                        $setIdle = true;
+                    }
+                // гость
+                } else {
+                    $deleteG = true;
+                }
+
+            // обработка посетителя для вывода статистики
+            } elseif ($type) {
+                ++$all;
+
+                // включен фильтр и проверка не пройдена
+                if ($filter && $cur['o_position'] !== $position) {
+                    continue;
+                }
+
+                // пользователь
+                if ($cur['user_id'] > 1) {
+                    $users[$cur['user_id']] = [
+                        'name' => $cur['ident'],
+                        'logged' => $cur['logged'],
+                    ];
+                // гость
+                } elseif ($cur['o_name'] == '') {
+                    $guests[] = [
+                        'name' => $cur['ident'],
+                        'logged' => $cur['logged'],
+                    ];
+                // бот
+                } else {
+                    $bots[$cur['o_name']][] = [
+                        'name' => $cur['ident'],
+                        'logged' => $cur['logged'],
+                    ];
+                }
+
+            // просто +1 к общему числу посетителей
+            } else {
+                ++$all;
+            }
+        }
+        $this->db->free_result($result);
+
+        // удаление просроченных пользователей
+        if ($deleteU) {
+            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE logged<'.$tVisit) or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+        }
+
+        // удаление просроченных гостей
+        if ($deleteG) {
+            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE user_id=1 AND logged<'.$tOnline) or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+        }
+
+        // обновление idle
+        if ($setIdle) {
+            $this->db->query('UPDATE '.$this->db->prefix.'online SET idle=1 WHERE logged<'.$tOnline) or error('Unable to update into online list', __FILE__, __LINE__, $this->db->error());
+        }
+
+        // обновление максимального значение пользоватеелй онлайн
+        if ($this->config['st_max_users'] < $all) {
+            $this->db->query('UPDATE '.$this->db->prefix.'config SET conf_value=\''.$all.'\' WHERE conf_name=\'st_max_users\'') or error('Unable to update config value \'st_max_users\'', __FILE__, __LINE__, $this->db->error());
+            $this->db->query('UPDATE '.$this->db->prefix.'config SET conf_value=\''.$now.'\' WHERE conf_name=\'st_max_users_time\'') or error('Unable to update config value \'st_max_users_time\'', __FILE__, __LINE__, $this->db->error());
+
+            $this->c->get('config update');
+        }
+/*
+@set_time_limit(0);
+for ($i=0;$i<100;++$i) {
+    $this->db->query('REPLACE INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) VALUES(1, \''.$this->db->escape($i).'\', '.time().', \''.$this->db->escape($position).'\', \'Super Puper '.$this->db->escape($i).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
+}
+*/
+        return [$users, $guests, $bots];
+    }
+
+    /**
+     * Обновление данных текущего пользователя
+     * @param string $position
+     */
+    protected function updateUser($position)
+    {
+        $now = time();
+        // гость
+        if ($this->user['is_guest']) {
+            $oname = (string) $this->user['is_bot'];
+
+            if ($this->user['is_logged']) {
+                $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) SELECT 1, \''.$this->db->escape($this->user['ip']).'\', '.$now.', \''.$this->db->escape($position).'\', \''.$this->db->escape($oname).'\' FROM '.$this->db->prefix.'groups WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id=1 AND ident=\''.$this->db->escape($this->user['ip']).'\') LIMIT 1') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
+
+                // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table
+/*                switch ($this->c->getParameter('DB_TYPE')) {
+                    case 'mysql':
+                    case 'mysqli':
+                    case 'mysql_innodb':
+                    case 'mysqli_innodb':
+                    case 'sqlite':
+                        $this->db->query('REPLACE INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) VALUES(1, \''.$this->db->escape($this->user['ip']).'\', '.$now.', \''.$this->db->escape($position).'\', \''.$this->db->escape($oname).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
+                        break;
+
+                    default:
+                        $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position, o_name) SELECT 1, \''.$this->db->escape($this->user['ip']).'\', '.$now.', \''.$this->db->escape($position).'\', \''.$this->db->escape($oname).'\' WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id=1 AND ident=\''.$this->db->escape($this->user['ip']).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
+                        break;
+                }
+*/
+            } else {
+                $this->db->query('UPDATE '.$this->db->prefix.'online SET logged='.$now.', o_position=\''.$this->db->escape($position).'\', o_name=\''.$this->db->escape($oname).'\' WHERE user_id=1 AND ident=\''.$this->db->escape($this->user['ip']).'\'') or error('Unable to update online list', __FILE__, __LINE__, $this->db->error());
+            }
+        } else {
+        // пользователь
+            if ($this->user['is_logged']) {
+                $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position) SELECT '.$this->user['id'].', \''.$this->db->escape($this->user['username']).'\', '.$now.', \''.$this->db->escape($position).'\' FROM '.$this->db->prefix.'groups WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id='.$this->user['id'].') LIMIT 1') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
+                // With MySQL/MySQLi/SQLite, REPLACE INTO avoids a user having two rows in the online table
+/*                switch ($this->c->getParameter('DB_TYPE')) {
+                    case 'mysql':
+                    case 'mysqli':
+                    case 'mysql_innodb':
+                    case 'mysqli_innodb':
+                    case 'sqlite':
+                        $this->db->query('REPLACE INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position) VALUES('.$this->user['id'].', \''.$this->db->escape($this->user['username']).'\', '.$now.', \''.$this->db->escape($position).'\')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
+                        break;
+
+                    default:
+                        $this->db->query('INSERT INTO '.$this->db->prefix.'online (user_id, ident, logged, o_position) SELECT '.$this->user['id'].', \''.$this->db->escape($this->user['username']).'\', '.$now.', \''.$this->db->escape($position).'\' WHERE NOT EXISTS (SELECT 1 FROM '.$this->db->prefix.'online WHERE user_id='.$this->user['id'].')') or error('Unable to insert into online list', __FILE__, __LINE__, $this->db->error());
+                        break;
+                }
+*/
+            } else {
+                $idle_sql = ($this->user['idle'] == '1') ? ', idle=0' : '';
+                $this->db->query('UPDATE '.$this->db->prefix.'online SET logged='.$now.$idle_sql.', o_position=\''.$this->db->escape($position).'\' WHERE user_id='.$this->user['id']) or error('Unable to update online list', __FILE__, __LINE__, $this->db->error());
+            }
+        }
+    }
+
+    /**
+     * Удаление юзера из таблицы online
+     */
+    public function delete(User $user)
+    {
+        if ($user['is_guest']) {
+            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE user_id=1 AND ident=\''.$this->db->escape($user['ip']).'\'') or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+        } else {
+            $this->db->query('DELETE FROM '.$this->db->prefix.'online WHERE user_id='.$user['id']) or error('Unable to delete from online list', __FILE__, __LINE__, $this->db->error());
+        }
+    }
+}

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

@@ -0,0 +1,92 @@
+<?php
+
+namespace ForkBB\Models\Pages\Admin;
+
+use ForkBB\Models\Pages\Page;
+use R2\DependencyInjection\ContainerInterface;
+
+abstract class Admin extends Page
+{
+    /**
+     * Указатель на активный пункт навигации админки
+     * @var string
+     */
+    protected $adminIndex;
+
+    /**
+     * Указатель на активный пункт навигации
+     * @var string
+     */
+    protected $index = 'admin';
+
+    /**
+     * Позиция для таблицы онлайн текущего пользователя
+     * @var string
+     */
+    protected $onlinePos = 'admin';
+
+    /**
+     * Конструктор
+     * @param ContainerInterface $container
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        parent::__construct($container);
+        $container->get('Lang')->load('admin');
+        $this->titles = [__('Admin title')];
+    }
+
+    /**
+     * Возвращает данные для шаблона
+     * @return array
+     */
+    public function getData()
+    {
+        $this->c->get('Online')->handle($this);
+
+        $data = parent::getData();
+        $data['aNavigation'] = $this->aNavigation();
+        $data['aIndex'] = $this->adminIndex;
+        return $data;
+    }
+
+    /**
+     * Возвращает массив ссылок с описанием для построения навигации админки
+     * @return array
+     */
+    protected function aNavigation()
+    {
+        $user = $this->c->get('user');
+        $is_admin = $user['g_id'] == PUN_ADMIN;
+        $r = $this->c->get('Router');
+
+        $nav = [
+            'Moderator menu'  => [
+                'index' => [$r->link('Admin'), __('Admin index')],
+                'users' => ['admin_users.php', __('Users')],
+            ],
+        ];
+        if ($is_admin || $user['g_mod_ban_users'] == '1') {
+            $nav['Moderator menu']['bans'] = ['admin_bans.php', __('Bans')];
+        }
+        if ($is_admin || $this->config['o_report_method'] == '0' || $this->config['o_report_method'] == '2') {
+            $nav['Moderator menu']['reports'] = ['admin_reports.php', __('Reports')];
+        }
+
+        if ($is_admin) {
+            $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' => ['admin_groups.php', __('User groups')],
+                'censoring' => ['admin_censoring.php', __('Censoring')],
+                'maintenance' => ['admin_maintenance.php', __('Maintenance')]
+            ];
+        }
+
+
+        return $nav;
+    }
+
+}

+ 33 - 0
app/Models/Pages/Admin/Index.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace 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->get('Lang')->load('admin_index');
+        $this->data = [
+            'version' => $this->config['s_fork_version'] . '.' . $this->config['i_fork_revision'],
+            'linkStat' => $this->c->get('Router')->link('AdminStatistics'),
+        ];
+        $this->titles[] = __('Admin index');
+        return $this;
+    }
+}

+ 112 - 0
app/Models/Pages/Admin/Statistics.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace 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()
+    {
+        // Is phpinfo() a disabled function?
+        if (strpos(strtolower((string) ini_get('disable_functions')), 'phpinfo') !== false) {
+            $this->c->get('Message')->message('PHPinfo disabled message', true, 200);
+        }
+
+        phpinfo();
+        exit; //????
+    }
+
+    /**
+     * Подготавливает данные для шаблона
+     * @return Page
+     */
+    public function statistics()
+    {
+        $this->c->get('Lang')->load('admin_index');
+        $this->data = [];
+        $this->titles[] = __('Server statistics');
+        $this->data['isAdmin'] = $this->c->get('user')['g_id'] == PUN_ADMIN;
+        $this->data['linkInfo'] = $this->c->get('Router')->link('AdminInfo');
+
+        // Get the server load averages (if possible)
+        if (@file_exists('/proc/loadavg') && is_readable('/proc/loadavg')) {
+            // We use @ just in case
+            $fh = @fopen('/proc/loadavg', 'r');
+            $ave = @fread($fh, 64);
+            @fclose($fh);
+
+            if (($fh = @fopen('/proc/loadavg', 'r'))) {
+                $ave = fread($fh, 64);
+                fclose($fh);
+            } else {
+                $ave = '';
+            }
+
+            $ave = @explode(' ', $ave);
+            $this->data['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];
+        } else {
+            $this->data['serverLoad'] = __('Not available');
+        }
+
+        // Get number of current visitors
+        $db = $this->c->get('DB');
+        $result = $db->query('SELECT COUNT(user_id) FROM '.$db->prefix.'online WHERE idle=0') or error('Unable to fetch online count', __FILE__, __LINE__, $db->error());
+        $this->data['numOnline'] = $db->result($result);
+
+        // Collect some additional info about MySQL
+        if (in_array($this->c->getParameter('DB_TYPE'), ['mysql', 'mysqli', 'mysql_innodb', 'mysqli_innodb'])) {
+            // Calculate total db size/row count
+            $result = $db->query('SHOW TABLE STATUS LIKE \''.$db->prefix.'%\'') or error('Unable to fetch table status', __FILE__, __LINE__, $db->error());
+
+            $tRecords = $tSize = 0;
+            while ($status = $db->fetch_assoc($result)) {
+                $tRecords += $status['Rows'];
+                $tSize += $status['Data_length'] + $status['Index_length'];
+            }
+
+            $this->data['tSize'] = $this->size($tSize);
+            $this->data['tRecords'] = $this->number($tRecords);
+        } else {
+            $this->data['tSize'] = 0;
+            $this->data['tRecords'] = 0;
+        }
+
+        // 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>';
+        } elseif (isset($_PHPA)) {
+            $this->data['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>';
+        } elseif (ini_get('zend_optimizer.optimization_level')) {
+            $this->data['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>';
+        } elseif (ini_get('xcache.cacher')) {
+            $this->data['accelerator'] = '<a href="http://' . __('XCache link') . '">' . __('XCache') . '</a>';
+        } else {
+            $this->data['accelerator'] = __('NA');
+        }
+
+        $this->data['dbVersion'] = implode(' ', $db->get_version());
+
+        return $this;
+    }
+}

+ 116 - 0
app/Models/Pages/Auth.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+use R2\DependencyInjection\ContainerInterface;
+
+class Auth extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'auth';
+
+    /**
+     * Указатель на активный пункт навигации
+     * @var string
+     */
+    protected $index = 'login';
+
+    /**
+     * Выход пользователя
+     * @param array $args
+     * @retrun Page
+     */
+    public function logout($args)
+    {
+        $user = $this->c->get('user');
+
+        $this->c->get('Lang')->load('login');
+
+        if ($this->c->get('Csrf')->check($args['token'], 'Logout', $args)) {
+            $user->logout();
+            return $this->c->get('Redirect')->setPage('Index')->setMessage(__('Logout redirect'));
+        }
+
+        return $this->c->get('Redirect')->setPage('Index')->setMessage(__('Bad request'));
+    }
+
+    /**
+     * Подготовка данных для страницы входа на форум
+     * @param array $args
+     * @return Page
+     */
+    public function login($args)
+    {
+        $this->c->get('Lang')->load('login');
+
+        if (! isset($args['_name'])) {
+            $args['_name'] = '';
+        }
+        if (! isset($args['_redirect'])) {
+            $args['_redirect'] = empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER'];
+            $args['_redirect'] = $this->c->get('Router')->validate($args['_redirect'], 'Index');
+        }
+
+        $this->titles = [
+            __('Login'),
+        ];
+        $this->data = [
+            'name' => $args['_name'],
+            'formAction' => $this->c->get('Router')->link('Login'),
+            'formToken' => $this->c->get('Csrf')->create('Login'),
+            'forgetLink' => $this->c->get('Router')->link('Forget'),
+            'regLink' => $this->config['o_regs_allow'] == '1'
+                ? $this->c->get('Router')->link('Registration')
+                : null,
+            'formRedirect' => $args['_redirect'],
+            'formSave' => ! empty($args['_save'])
+        ];
+
+        return $this;
+    }
+
+    /**
+     * Вход на форум
+     * @return Page
+     */
+    public function loginPost()
+    {
+        $this->c->get('Lang')->load('login');
+
+        $name = $this->c->get('Request')->postStr('name', '');
+        $password = $this->c->get('Request')->postStr('password', '');
+        $token = $this->c->get('Request')->postStr('token');
+        $save = $this->c->get('Request')->postStr('save');
+
+        $redirect = $this->c->get('Request')->postStr('redirect', '');
+        $redirect = $this->c->get('Router')->validate($redirect, 'Index');
+
+        $args = [
+            '_name' => $name,
+            '_redirect' => $redirect,
+            '_save' => $save,
+        ];
+
+        if (empty($token) || ! $this->c->get('Csrf')->check($token, 'Login')) {
+            $this->iswev['e'][] = __('Bad token');
+            return $this->login($args);
+        }
+
+        if (empty($name) || empty($password)) {
+            $this->iswev['v'][] = __('Wrong user/pass');
+            return $this->login($args);
+        }
+
+        $result = $this->c->get('user')->login($name, $password, ! empty($save));
+
+        if (false === $result) {
+            $this->iswev['v'][] = __('Wrong user/pass');
+            return $this->login($args);
+        }
+
+        return $this->c->get('Redirect')->setUrl($redirect)->setMessage(__('Login redirect'));
+    }
+}

+ 38 - 0
app/Models/Pages/Ban.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+class Ban extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'ban';
+
+    /**
+     * HTTP статус ответа для данной страницы
+     * @var int
+     */
+    protected $httpStatus = 403;
+
+    /**
+     * Подготавливает данные для шаблона
+     * @param array $banned
+     * @return Page
+     */
+    public function ban(array $banned)
+    {
+        $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;
+    }
+}

+ 61 - 0
app/Models/Pages/Debug.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+class Debug extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'layouts/debug';
+
+    /**
+     * Подготавливает данные для шаблона
+     * @return Page
+     */
+    public function debug()
+    {
+        $this->data = [
+            'time' => $this->number(microtime(true) - (empty($_SERVER['REQUEST_TIME_FLOAT']) ? $this->c->getParameter('START') : $_SERVER['REQUEST_TIME_FLOAT']), 3),
+            'numQueries' => $this->c->get('DB')->get_num_queries(),
+            'memory' => $this->size(memory_get_usage()),
+            'peak' => $this->size(memory_get_peak_usage()),
+        ];
+
+        if (defined('PUN_SHOW_QUERIES')) {
+            $this->data['queries'] = $this->c->get('DB')->get_saved_queries();
+        } else {
+            $this->data['queries'] = null;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Возвращает массив ссылок с описанием для построения навигации
+     * @return array
+     */
+    protected function fNavigation()
+    {
+        return [];
+    }
+
+    /**
+     * Возвращает HTTP заголовки страницы
+     * @return array
+     */
+    public function getHeaders()
+    {
+        return [];
+    }
+
+    /**
+     * Возвращает данные для шаблона
+     * @return array
+     */
+    public function getData()
+    {
+        return $this->data;
+    }
+}

+ 252 - 0
app/Models/Pages/Index.php

@@ -0,0 +1,252 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+class Index extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'index';
+
+    /**
+     * Позиция для таблицы онлайн текущего пользователя
+     * @var 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()
+    {
+        $this->c->get('Lang')->load('index');
+        $this->c->get('Lang')->load('subforums');
+
+        $db = $this->c->get('DB');
+        $user = $this->c->get('user');
+        $r = $this->c->get('Router');
+
+        $stats = $this->c->get('users_info');
+
+        $result = $db->query('SELECT SUM(num_topics), SUM(num_posts) FROM '.$db->prefix.'forums') or error('Unable to fetch topic/post count', __FILE__, __LINE__, $db->error());
+        list($stats['total_topics'], $stats['total_posts']) = array_map([$this, 'number'], array_map('intval', $db->fetch_row($result)));
+
+        $stats['total_users'] = $this->number($stats['total_users']);
+
+        if ($user['g_view_users'] == '1') {
+            $stats['newest_user'] = [
+                $r->link('User', [
+                    'id' => $stats['last_user']['id'],
+                    'name' => $stats['last_user']['username'],
+                ]),
+                $stats['last_user']['username']
+            ];
+        } else {
+            $stats['newest_user'] = $stats['last_user']['username'];
+        }
+        $this->data['stats'] = $stats;
+
+        // вывод информации об онлайн посетителях
+        if ($this->config['o_users_online'] == '1') {
+            $this->data['online'] = [];
+            $this->data['online']['max'] = $this->number($this->config['st_max_users']);
+            $this->data['online']['max_time'] = $this->time($this->config['st_max_users_time']);
+
+            // данные онлайн посетителей
+            list($users, $guests, $bots) = $this->c->get('Online')->handle($this);
+            $list = [];
+
+            if ($user['g_view_users'] == '1') {
+                foreach ($users as $id => $cur) {
+                    $list[] = [
+                        $r->link('User', [
+                            'id' => $id,
+                            'name' => $cur['name'],
+                        ]),
+                        $cur['name'],
+                    ];
+                }
+            } else {
+                foreach ($users as $cur) {
+                    $list[] = $cur['name'];
+                }
+            }
+            $this->data['online']['number_of_users'] = $this->number(count($users));
+
+            $s = 0;
+            foreach ($bots as $name => $cur) {
+                $count = count($cur);
+                $s += $count;
+                if ($count > 1) {
+                    $list[] = '[Bot] ' . $name . ' (' . $count . ')';
+                } else {
+                    $list[] = '[Bot] ' . $name;
+                }
+            }
+            $s += count($guests);
+            $this->data['online']['number_of_guests'] = $this->number($s);
+            $this->data['online']['list'] = $list;
+        } else {
+            $this->onlineType = false;
+            $this->c->get('Online')->handle($this);
+            $this->data['online'] = null;
+        }
+        $this->data['forums'] = $this->getForumsData();
+        return $this;
+    }
+
+    /**
+     * Получение данных по разделам
+     * @param int $root
+     * @return array
+     */
+    protected function getForumsData($root = 0)
+    {
+        list($fTree, $fDesc, $fAsc) = $this->c->get('forums');
+
+        // раздел $root не имеет подразделов для вывода или они не доступны
+        if (empty($fTree[$root])) {
+            return [];
+        }
+
+        $db = $this->c->get('DB');
+        $user = $this->c->get('user');
+
+        // текущие данные по подразделам
+        $forums = array_slice($fAsc[$root], 1);
+        if ($user['is_guest']) {
+            $result = $db->query('SELECT id, forum_desc, moderators, num_topics, num_posts, last_post, last_post_id, last_poster, last_topic FROM '.$db->prefix.'forums WHERE id IN ('.implode(',', $forums).')', true) or error('Unable to fetch forum list', __FILE__, __LINE__, $db->error());
+        } else {
+            $result = $db->query('SELECT f.id, f.forum_desc, f.moderators, f.num_topics, f.num_posts, f.last_post, f.last_post_id, f.last_poster, f.last_topic, mof.mf_upper FROM '.$db->prefix.'forums AS f LEFT JOIN '.$db->prefix.'mark_of_forum AS mof ON (mof.uid='.$user['id'].' AND f.id=mof.fid) WHERE f.id IN ('.implode(',', $forums).')', true) or error('Unable to fetch forum list', __FILE__, __LINE__, $db->error());
+        }
+
+        $forums = [];
+        while ($cur = $db->fetch_assoc($result)) {
+            $forums[$cur['id']] = $cur;
+        }
+        $db->free_result($result);
+
+        // поиск новых
+        $new = [];
+        if (! $user['is_guest']) {
+            // предварительная проверка разделов
+            $max = max((int) $user['last_visit'], (int) $user['u_mark_all_read']);
+            foreach ($forums as $id => $cur) {
+                $t = max($max, (int) $cur['mf_upper']);
+                if ($cur['last_post'] > $t) {
+                    $new[$id] = $t;
+                }
+            }
+            // проверка по темам
+            if (! empty($new)) {
+                $result = $db->query('SELECT t.forum_id, t.id, t.last_post FROM '.$db->prefix.'topics AS t LEFT JOIN '.$db->prefix.'mark_of_topic AS mot ON (mot.uid='.$user['id'].' AND mot.tid=t.id) WHERE t.forum_id IN('.implode(',', array_keys($new)).') AND t.last_post>'.$max.' AND t.moved_to IS NULL AND (mot.mt_upper IS NULL OR t.last_post>mot.mt_upper)') or error('Unable to fetch new topics', __FILE__, __LINE__, $db->error());
+                $tmp = [];
+                while ($cur = $db->fetch_assoc($result)) {
+                    if ($cur['last_post']>$new[$cur['forum_id']]) {
+                        $tmp[$cur['forum_id']] = true;
+                    }
+                }
+                $new = $tmp;
+                $db->free_result($result);
+            }
+        }
+
+        $r = $this->c->get('Router');
+
+        // формированием таблицы разделов
+        $result = [];
+        foreach ($fTree[$root] 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']
+                    ];
+                }
+            }
+            // модераторы
+            $moderators = [];
+            if (!empty($forums[$fId]['moderators'])) {
+                $mods = unserialize($forums[$fId]['moderators']);
+                foreach ($mods as $name => $id) {
+                    if ($user['g_view_users'] == '1') {
+                        $moderators[] = [
+                            $r->link('User', [
+                                'id' => $id,
+                                'name' => $name,
+                            ]),
+                            $name
+                        ];
+                    } else {
+                        $moderators[] = $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'];
+                }
+            }
+
+            $result[$cur['cid']]['name'] = $cur['cat_name'];
+            $result[$cur['cid']]['forums'][] = [
+                'fid'          => $fId,
+                'forum_name'   => $cur['forum_name'],
+                'forum_desc'   => $forums[$fId]['forum_desc'],
+                'forum_link'   => $r->link('Forum', [
+                    'id' => $fId,
+                    'name' => $cur['forum_name']
+                ]),
+                'redirect_url' => $cur['redirect_url'],
+                'subforums'    => $subForums,
+                'moderators'   => $moderators,
+                'num_topics'   => $numT,
+                'num_posts'    => $numP,
+                'topics'       => $this->number($numT),
+                'posts'        => $this->number($numP),
+                'last_post'    => $this->time($time),
+                'last_post_id' => $postId > 0 ? $r->link('viewPost', ['id' => $postId]) : null,
+                'last_poster'  => $poster,
+                'last_topic'   => $topic,
+                'new'          => $fnew,
+            ];
+        }
+        return $result;
+    }
+}

+ 70 - 0
app/Models/Pages/Maintenance.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+use R2\DependencyInjection\ContainerInterface;
+
+class Maintenance extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'maintenance';
+
+    /**
+     * HTTP статус ответа для данной страницы
+     * @var int
+     */
+    protected $httpStatus = 503;
+
+    /**
+     * Подготовленные данные для шаблона
+     * @var array
+     */
+    protected $data = [];
+
+    /**
+     * Конструктор
+     * @param ContainerInterface $container
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        $this->c = $container;
+        $this->config = $container->get('config');
+        $container->get('Lang')->load('common', $this->config['o_default_lang']);
+    }
+
+    /**
+     * Возвращает данные для шаблона
+     * @return array
+     */
+    public function getData()
+    {
+        $this->titles = [
+            __('Maintenance'),
+        ];
+        $this->data = [
+            'MaintenanceMessage' => $this->config['o_maintenance_message'],
+        ];
+        return parent::getData();
+    }
+
+    /**
+     * Возвращает массив ссылок с описанием для построения навигации
+     * @return array
+     */
+    protected function fNavigation()
+    {
+        return [];
+    }
+
+    /**
+     * Возврат предупреждений форума
+     * @return array
+     */
+    protected function fWarning()
+    {
+        return [];
+    }
+}

+ 33 - 0
app/Models/Pages/Message.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+class Message extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'message';
+
+    /**
+     * Подготавливает данные для шаблона
+     * @param string $message
+     * @param bool $back
+     * @param int $status
+     * @return Page
+     */
+    public function message($message, $back = true, $status = 404, array $headers = [])
+    {
+        $this->httpStatus = $status;
+        $this->httpHeaders = $headers;
+        $this->titles = [
+            __('Info'),
+        ];
+        $this->data = [
+            'Message' => __($message),
+            'Back' => $back,
+        ];
+        return $this;
+    }
+}

+ 389 - 0
app/Models/Pages/Page.php

@@ -0,0 +1,389 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+use R2\DependencyInjection\ContainerInterface;
+use RuntimeException;
+
+abstract class Page
+{
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    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;
+
+    /**
+     * Позиция для таблицы онлайн текущего пользователя
+     * @var string
+     */
+    protected $onlinePos = '';
+
+    /**
+     * Массив info, success, warning, error, validation информации
+     * @var array
+     */
+    protected $iswev = [];
+
+    /**
+     * Тип обработки пользователей онлайн
+     * Если false, то идет обновление данных
+     * Если true, то идет возврат данных (смотрите $onlineFilter)
+     * @var bool
+     */
+    protected $onlineType = false;
+
+    /**
+     * Тип возврата данных при onlineType === true
+     * Если true, то из online должны вернутся только пользователи находящиеся на этой же странице
+     * Если false, то все пользователи online
+     * @var bool
+     */
+    protected $onlineFilter = true;
+
+    /**
+     * Конструктор
+     * @param ContainerInterface $container
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        $this->c = $container;
+        $this->config = $container->get('config');
+        $container->get('Lang')->load('common');
+    }
+
+    /**
+     * Возвращает HTTP заголовки страницы
+     * @return array
+     */
+    public function getHeaders()
+    {
+        $headers = $this->httpHeaders;
+        if (! empty($status = $this->getStatus())) {
+            $headers[] = $status;
+        }
+        return $headers;
+    }
+
+
+    /**
+     * Возвращает HTTP статус страницы или null
+     * @return null|string
+     */
+    protected function getStatus()
+    {
+        $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 is_array($this->data);
+    }
+
+    /**
+     * Возвращает имя шаблона
+     * @return string
+     */
+    public function getNameTpl()
+    {
+        return $this->nameTpl;
+    }
+
+    /**
+     * Возвращает данные для шаблона
+     * @return array
+     */
+    public function getData()
+    {
+        if (empty($this->data)) {
+            $this->data = [];
+        }
+        return $this->data + [
+            'pageTitle' => $this->pageTitle(),
+            'pageHeads' => $this->pageHeads(),
+            'fTitle' => $this->config['o_board_title'],
+            'fDescription' => $this->config['o_board_desc'],
+            'fNavigation' => $this->fNavigation(),
+            'fIndex' => $this->index,
+            'fAnnounce' => $this->fAnnounce(),
+            'fRootLink' => $this->c->get('Router')->link('Index'),
+            'fIswev' => $this->getIswev(),
+        ];
+    }
+
+    /**
+     * Возврат info, success, warning, error, validation информации
+     * @return array
+     */
+    protected function getIswev()
+    {
+        if ($this->config['o_maintenance'] == '1') {
+            $user = $this->c->get('user');
+            if ($user['is_admmod']) {
+                $this->iswev['w'][] = '<a href="' . $this->c->get('Router')->link('AdminOptions', ['#' => 'maintenance']). '">' . __('Maintenance mode enabled') . '</a>';
+            }
+        }
+        return $this->iswev;
+    }
+
+    /**
+     * Формирует title страницы
+     * @return string
+     */
+    protected function pageTitle()
+    {
+        $arr = empty($this->titles) ? [] : array_reverse($this->titles);
+        $arr[] = $this->config['o_board_title'];
+        return implode(__('Title separator'), $arr);
+    }
+
+    /**
+     * Генерация массива заголовков страницы
+     * @return array
+     */
+    protected function pageHeads()
+    {
+        return [];
+    }
+
+    /**
+     * Возвращает текст объявления или null
+     * @return null|string
+     */
+    protected function fAnnounce()
+    {
+        return empty($this->config['o_announcement']) ? null : $this->config['o_announcement_message'];
+    }
+
+    /**
+     * Возвращает массив ссылок с описанием для построения навигации
+     * @return array
+     */
+    protected function fNavigation()
+    {
+        $user = $this->c->get('user');
+        $r = $this->c->get('Router');
+
+        $nav = [
+            'index' => [$r->link('Index'), __('Index')]
+        ];
+
+        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['is_guest'] || $user['g_read_board'] == '1' || $this->config['o_regs_allow'] == '1')) {
+            $nav['rules'] = [$r->link('Rules'), __('Rules')];
+        }
+
+        if ($user['g_read_board'] == '1' && $user['g_search'] == '1') {
+            $nav['search'] = [$r->link('Search'), __('Search')];
+        }
+
+        if ($user['is_guest']) {
+            $nav['register'] = ['register.php', __('Register')];
+            $nav['login'] = [$r->link('Login'), __('Login')];
+        } else {
+            $nav['profile'] = [$r->link('User', [
+                'id' => $user['id'],
+                'name' => $user['username']
+            ]), __('Profile')];
+            // New PMS
+            if ($this->config['o_pms_enabled'] == '1' && ($user['g_pm'] == 1 || $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->config['o_pms_flasher']) || PUN_ACTIVE_PAGE == 'pms_new') ? '' : ' class="remflasher"' ).'>'.$user['messages_new'].'</span>)' : '').'</a></li>';
+            }
+            // New PMS
+
+            if ($user['is_admmod']) {
+                $nav['admin'] = [$r->link('Admin'), __('Admin')];
+            }
+
+            $nav['logout'] = [$r->link('Logout', [
+                'token' => $this->c->get('Csrf')->create('Logout'),
+            ]), __('Logout')];
+        }
+
+        if ($user['g_read_board'] == '1' && $this->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)) {
+               $k = count($matches[0]);
+               for ($i = 0; $i < $k; ++$i) {
+                   if (empty($matches[4][$i])) {
+                       $matches[4][$i] = 'extra' . $i;
+                   }
+                   if (isset($nav[$matches[4][$i]])) {
+                       $nav[$matches[4][$i]] = [$matches[3][$i], $matches[2][$i]];
+                   } else {
+                       $nav = array_merge(
+                           array_slice($nav, 0, $matches[1][$i]),
+                           [$matches[4][$i] => [$matches[3][$i], $matches[2][$i]]],
+                           array_slice($nav, $matches[1][$i])
+                       );
+                   }
+               }
+            }
+        }
+        return $nav;
+    }
+
+    /**
+     * Заглушка
+     * @param string $name
+     * @param array $arguments
+     * @return Page
+     */
+    public function __call($name, array $arguments)
+    {
+        return $this;
+    }
+
+    /**
+     * Возвращает размер в байтах, Кбайтах, ...
+     * @param int $size
+     * @return string
+     */
+    protected function size($size)
+    {
+        $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB');
+
+        for ($i = 0; $size > 1024; $i++) {
+            $size /= 1024;
+        }
+
+        return __('Size unit '.$units[$i], round($size, 2));
+    }
+
+    /**
+     * Возращает данные для управления обработкой пользователей онлайн
+     * @return array
+     */
+    public function getDataForOnline()
+    {
+        return [$this->onlinePos, $this->onlineType, $this->onlineFilter];
+    }
+
+    /**
+     * Возвращает число в формате языка текущего пользователя
+     * @param mixed $number
+     * @param int $decimals
+     * @return string
+     */
+    protected function number($number, $decimals = 0)
+    {
+        return is_numeric($number) ? number_format($number, $decimals, __('lang_decimal_point'), __('lang_thousands_sep')) : '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)
+    {
+        if (empty($timestamp)) {
+            return __('Never');
+        }
+
+        $user = $this->c->get('user');
+
+        $diff = ($user['timezone'] + $user['dst']) * 3600;
+        $timestamp += $diff;
+
+        if (null === $dateFormat) {
+            $dateFormat = $this->c->getParameter('date_formats')[$user['date_format']];
+        }
+        if(null === $timeFormat) {
+            $timeFormat = $this->c->getParameter('time_formats')[$user['time_format']];
+        }
+
+        $date = gmdate($dateFormat, $timestamp);
+
+        if(! $noText) {
+            $now = time() + $diff;
+
+            if ($date == gmdate($dateFormat, $now)) {
+                $date = __('Today');
+            } elseif ($date == gmdate($dateFormat, $now - 86400)) {
+                $date = __('Yesterday');
+            }
+        }
+
+        if ($dateOnly) {
+            return $date;
+        } elseif ($timeOnly) {
+            return gmdate($timeFormat, $timestamp);
+        } else {
+            return $date . ' ' . gmdate($timeFormat, $timestamp);
+        }
+    }
+}

+ 98 - 0
app/Models/Pages/Redirect.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+class Redirect extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = null;
+
+    /**
+     * Адрес перехода
+     * @var string
+     */
+    protected $link;
+
+    /**
+     * Возвращает флаг готовности данных
+     * @return bool
+     */
+    public function isReady()
+    {
+        return ! empty($this->link);
+    }
+
+    /**
+     * Задает адрес перехода
+     * @param string $marker
+     * @param array $args
+     * @return Page
+     */
+    public function setPage($marker, array $args = [])
+    {
+        $this->link = $this->c->get('Router')->link($marker, $args);
+        return $this;
+    }
+
+    /**
+     * Задает ссылку для перехода
+     * @param string $url
+     * @return Page
+     */
+    public function setUrl($url)
+    {
+        $this->link = $url;
+        return $this;
+    }
+
+    /**
+     * Задает сообщение
+     * @param string $message
+     * @return Page
+     */
+    public function setMessage($message)
+    {
+        // переадресация без вывода сообщения
+        if ($this->config['o_redirect_delay'] == '0') {
+            return $this;
+        }
+
+        $this->nameTpl = 'redirect';
+        $this->titles = [
+            __('Redirecting'),
+        ];
+        $this->data = [
+            'Message' => $message,
+            'Timeout' => (int) $this->config['o_redirect_delay'],  //???? перенести в заголовки?
+        ];
+        return $this;
+    }
+
+    /**
+     * Возвращает HTTP заголовки страницы
+     * @return array
+     */
+    public function getHeaders()
+    {
+        // переадресация без вывода сообщения
+        if (empty($this->data)) {
+            $this->httpHeaders = [
+                'Location: ' . $this->link, //????
+            ];
+        }
+        return parent::getHeaders();
+    }
+
+    /**
+     * Возвращает данные для шаблона
+     * @return array
+     */
+    public function getData()
+    {
+        $this->data['Link'] = $this->link;
+        return parent::getData();
+    }
+}

+ 33 - 0
app/Models/Pages/Rules.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+class Rules extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'rules';
+
+    /**
+     * Указатель на активный пункт навигации
+     * @var string
+     */
+    protected $index = 'rules';
+
+    /**
+     * Подготавливает данные для шаблона
+     * @return Page
+     */
+    public function view()
+    {
+        $this->titles = [
+            __('Forum rules'),
+        ];
+        $this->data = [
+            'Rules' => $this->config['o_rules_message'],
+        ];
+        return $this;
+    }
+}

+ 383 - 0
app/Models/User.php

@@ -0,0 +1,383 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Core\Model; //????
+use R2\DependencyInjection\ContainerInterface;
+use RuntimeException;
+
+class User extends Model
+{
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    protected $c;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var UserCookie
+     */
+    protected $userCookie;
+
+    /**
+     * @var DB
+     */
+    protected $db;
+
+    /**
+     * Адрес пользователя
+     * @var string
+     */
+    protected $ip;
+
+    /**
+     * Конструктор
+     */
+    public function __construct(array $config, $cookie, $db, ContainerInterface $container)
+    {
+        $this->config = $config;
+        $this->userCookie = $cookie;
+        $this->db = $db;
+        $this->c = $container;
+        $this->ip = $this->getIpAddress();
+    }
+
+    /**
+     * @return User
+     */
+    public function init()
+    {
+        if (($userId = $this->userCookie->id()) === false) {
+            return $this->initGuest();
+        }
+
+        $result = $this->db->query('SELECT u.*, g.*, o.logged, o.idle FROM '.$this->db->prefix.'users AS u INNER JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$this->db->prefix.'online AS o ON o.user_id=u.id WHERE u.id='.$userId) or error('Unable to fetch user information', __FILE__, __LINE__, $this->db->error());
+        $user = $this->db->fetch_assoc($result);
+        $this->db->free_result($result);
+
+        if (empty($user['id']) || ! $this->userCookie->verifyHash($user['id'], $user['password'])) {
+            return $this->initGuest();
+        }
+
+        // проверка ip админа и модератора - Visman
+        if ($this->config['o_check_ip'] == '1' && ($user['g_id'] == PUN_ADMIN || $user['g_moderator'] == '1') && $user['registration_ip'] != $this->ip) {
+            return $this->initGuest();
+        }
+
+        $this->userCookie->setUserCookie($user['id'], $user['password']);
+
+        // Set a default language if the user selected language no longer exists
+        if (!file_exists(PUN_ROOT.'lang/'.$user['language'])) {
+            $user['language'] = $this->config['o_default_lang'];
+        }
+
+        // Set a default style if the user selected style no longer exists
+        if (!file_exists(PUN_ROOT.'style/'.$user['style'].'.css')) {
+            $user['style'] = $this->config['o_default_style'];
+        }
+
+        if (!$user['disp_topics']) {
+            $user['disp_topics'] = $this->config['o_disp_topics_default'];
+        }
+        if (!$user['disp_posts']) {
+            $user['disp_posts'] = $this->config['o_disp_posts_default'];
+        }
+
+        $now = time();
+
+        if (! $user['logged']) {
+            $user['logged'] = $now;
+            $user['is_logged'] = true;
+
+            // Reset tracked topics
+            set_tracked_topics(null);
+        } else {
+            $user['is_logged'] = false;
+
+            // Special case: We've timed out, but no other user has browsed the forums since we timed out
+            if ($user['logged'] < ($now - $this->config['o_timeout_visit']))
+            {
+                $this->db->query('UPDATE '.$this->db->prefix.'users SET last_visit='.$user['logged'].' WHERE id='.$user['id']) or error('Unable to update user visit data', __FILE__, __LINE__, $this->db->error());
+                $user['last_visit'] = $user['logged'];
+            }
+            $cookie = $this->c->get('Cookie');
+            $track = $cookie->get('track');
+            // Update tracked topics with the current expire time
+            if (isset($track)) {
+                $cookie->set('track', $track, $now + $this->config['o_timeout_visit']);
+            }
+        }
+
+        $user['is_guest'] = false;
+        $user['is_admmod'] = $user['g_id'] == PUN_ADMIN || $user['g_moderator'] == '1';
+        $user['is_bot'] = false;
+        $user['ip'] = $this->ip;
+
+        $this->current = $user;
+
+        return $this;
+    }
+
+    /**
+     * @throws \RuntimeException
+     * @return User
+     */
+    protected function initGuest()
+    {
+        $result = $this->db->query('SELECT u.*, g.*, o.logged, o.last_post, o.last_search FROM '.$this->db->prefix.'users AS u INNER JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$this->db->prefix.'online AS o ON (o.user_id=1 AND o.ident=\''.$this->db->escape($this->ip).'\') WHERE u.id=1') or error('Unable to fetch guest information', __FILE__, __LINE__, $this->db->error());
+        $user = $this->db->fetch_assoc($result);
+        $this->db->free_result($result);
+
+        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.');
+        }
+
+        $this->userCookie->deleteUserCookie();
+
+        // этого гостя нет в таблице online
+        if (! $user['logged']) {
+            $user['logged'] = time();
+            $user['is_logged'] = true;
+        } else {
+            $user['is_logged'] = false;
+        }
+        $user['disp_topics'] = $this->config['o_disp_topics_default'];
+        $user['disp_posts'] = $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'];
+        $user['is_guest'] = true;
+        $user['is_admmod'] = false;
+        $user['is_bot'] = $this->isBot();
+        $user['ip'] = $this->ip;
+
+        // быстрое переключение языка - Visman
+        $language = $this->c->get('Cookie')->get('glang');
+        if (null !== $language)
+        {
+            $language = preg_replace('%[^\w]%', '', $language);
+            $languages = forum_list_langs();
+            if (in_array($language, $languages))
+                $user['language'] = $language;
+        }
+
+        $this->current = $user;
+        return $this;
+    }
+
+    /**
+     * Возврат адреса пользователя
+     * @return string
+     */
+    protected function getIpAddress()
+    {
+       return filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) ?: 'unknow';
+    }
+
+    /**
+     * Проверка на робота
+     * Если робот, то возврат имени
+     * @return false|string
+     */
+    protected function isBot()
+    {
+        $agent = trim($_SERVER['HTTP_USER_AGENT']);
+        if ($agent == '') {
+            return false;
+        }
+        $agentL = strtolower($agent);
+
+        if (strpos($agentL, 'bot') !== false
+            || strpos($agentL, 'spider') !== false
+            || strpos($agentL, 'crawler') !== false
+            || strpos($agentL, 'http') !== false
+        ) {
+            return $this->nameBot($agent, $agentL);
+        }
+
+        if (strpos($agent, 'Mozilla/') !== false
+            && (strpos($agent, 'Gecko') !== false
+                || (strpos($agent, '(compatible; MSIE ') !== false
+                    && strpos($agent, 'Windows') !== false
+                )
+            )
+        ) {
+            return false;
+        } elseif (strpos($agent, 'Opera/') !== false
+            && strpos($agent, 'Presto/') !== false
+        ) {
+            return false;
+        }
+        return $this->nameBot($agent, $agentL);
+    }
+
+    /**
+     * Выделяет имя робота из юзерагента
+     * @param string $agent
+     * @param string $agentL
+     * @retrun string
+     */
+    protected function nameBot($agent, $agentL)
+    {
+        if (strpos($agentL, 'mozilla') !== false) {
+            $agent = preg_replace('%Mozilla.*?compatible%i', ' ', $agent);
+        }
+        if (strpos($agentL, 'http') !== false || strpos($agentL, 'www.') !== false) {
+            $agent = preg_replace('%(?:https?://|www\.)[^\)]*(\)[^/]+$)?%i', ' ', $agent);
+        }
+        if (strpos($agent, '@') !== false) {
+            $agent = preg_replace('%\b[\w\.-]+@[^\)]+%', ' ', $agent);
+        }
+
+        $agentL = strtolower($agent);
+        if (strpos($agentL, 'bot') !== false
+            || strpos($agentL, 'spider') !== false
+            || strpos($agentL, 'crawler') !== false
+            || strpos($agentL, 'engine') !== false
+        ) {
+            $f = true;
+            $p = '%(?<=[^a-z\d\.-])(?:robot|bot|spider|crawler)\b.*%i';
+        } else {
+            $f = false;
+            $p = '%^$%';
+        }
+
+        if ($f && preg_match('%\b(([a-z\d\.! _-]+)?(?:robot|(?<!ro)bot|spider|crawler|engine)(?(2)[a-z\d\.! _-]*|[a-z\d\.! _-]+))%i', $agent, $matches))
+        {
+            $agent = $matches[1];
+
+            $pat = [
+                $p,
+                '%[^a-z\d\.!-]+%i',
+                '%(?<=^|\s|-)v?\d+\.\d[^\s]*\s*%i',
+                '%(?<=^|\s)\S{1,2}(?:\s|$)%',
+            ];
+            $rep = [
+                '',
+                ' ',
+                '',
+                '',
+            ];
+        } else {
+            $pat = [
+                '%\((?:KHTML|Linux|Mac|Windows|X11)[^\)]*\)?%i',
+                $p,
+                '%\b(?:AppleWebKit|Chrom|compatible|Firefox|Gecko|Mobile(?=[/ ])|Moz|Opera|OPR|Presto|Safari|Version)[^\s]*%i',
+                '%\b(?:InfoP|Intel|Linux|Mac|MRA|MRS|MSIE|SV|Trident|Win|WOW|X11)[^;\)]*%i',
+                '%\.NET[^;\)]*%i',
+                '%/.*%',
+                '%[^a-z\d\.!-]+%i',
+                '%(?<=^|\s|-)v?\d+\.\d[^\s]*\s*%i',
+                '%(?<=^|\s)\S{1,2}(?:\s|$)%',
+            ];
+            $rep = [
+                ' ',
+                '',
+                '',
+                '',
+                '',
+                '',
+                ' ',
+                '',
+                '',
+            ];
+        }
+        $agent = trim(preg_replace($pat, $rep, $agent), ' -');
+
+        if (empty($agent)) {
+            return 'Unknown';
+        }
+
+        $a = explode(' ', $agent);
+        $agent = $a[0];
+        if (strlen($agent) < 20
+            && ! empty($a[1])
+            && strlen($agent . ' ' . $a[1]) < 26
+        ) {
+            $agent .= ' ' . $a[1];
+        } elseif (strlen($agent) > 25) {
+            $agent = 'Unknown';
+        }
+        return $agent;
+    }
+
+    /**
+     * Выход
+     */
+    public function logout()
+    {
+        if ($this->current['is_guest']) {
+            return;
+        }
+
+        $this->userCookie->deleteUserCookie();
+        $this->c->get('Online')->delete($this);
+        // Update last_visit (make sure there's something to update it with)
+        if (isset($this->current['logged'])) {
+            $this->db->query('UPDATE '.$this->db->prefix.'users SET last_visit='.$this->current['logged'].' WHERE id='.$this->current['id']) or error('Unable to update user visit data', __FILE__, __LINE__, $this->db->error());
+        }
+    }
+
+    /**
+     * Вход
+     * @param string $name
+     * @param string $password
+     * @param bool $save
+     * @return mixed
+     */
+    public function login($name, $password, $save)
+    {
+        $result = $this->db->query('SELECT u.id, u.group_id, u.username, u.password, u.registration_ip, g.g_moderator FROM '.$this->db->prefix.'users AS u LEFT JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id WHERE u.username=\''.$this->db->escape($name).'\'') or error('Unable to fetch user info', __FILE__, __LINE__, $this->db->error());
+        $user = $this->db->fetch_assoc($result);
+        $this->db->free_result($result);
+
+        if (empty($user['id'])) {
+            return false;
+        }
+
+        $authorized = false;
+        // For FluxBB by Visman 1.5.10.74 and above
+        if (strlen($user['password']) == 40) {
+            if (hash_equals($user['password'], sha1($password . $this->c->getParameter('SALT1')))) {
+                $authorized = true;
+
+                $user['password'] = password_hash($password, PASSWORD_DEFAULT);
+                $this->db->query('UPDATE '.$this->db->prefix.'users SET password=\''.$this->db->escape($user['password']).'\' WHERE id='.$user['id']) or error('Unable to update user password', __FILE__, __LINE__, $this->db->error());
+            }
+        } else {
+            $authorized = password_verify($password, $user['password']);
+        }
+
+        if (! $authorized) {
+            return false;
+        }
+
+        // Update the status if this is the first time the user logged in
+        if ($user['group_id'] == PUN_UNVERIFIED)
+        {
+            $this->db->query('UPDATE '.$this->db->prefix.'users SET group_id='.$this->config['o_default_user_group'].' WHERE id='.$user['id']) or error('Unable to update user status', __FILE__, __LINE__, $this->db->error());
+
+            $this->c->get('users_info update');
+        }
+
+        // перезаписываем ip админа и модератора - Visman
+        if ($this->config['o_check_ip'] == '1' && $user['registration_ip'] != $this->current['ip'])
+        {
+            if ($user['g_id'] == PUN_ADMIN || $user['g_moderator'] == '1')
+                $this->db->query('UPDATE '.$this->db->prefix.'users SET registration_ip=\''.$this->db->escape($this->current['ip']).'\' WHERE id='.$user['id']) or error('Unable to update user IP', __FILE__, __LINE__, $this->db->error());
+        }
+
+        $this->c->get('Online')->delete($this);
+
+        $this->c->get('UserCookie')->setUserCookie($user['id'], $user['password'], $save);
+
+        return $user['id'];
+    }
+
+}

+ 100 - 0
app/Models/UserMapper.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\User;
+use RuntimeException;
+use InvalidArgumentException;
+
+class UserMapper
+{
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var UserCookie
+     */
+    protected $cookie;
+
+    /**
+     * @var DB
+     */
+    protected $db
+
+    /**
+     * Конструктор
+     * @param array $config
+     * @param UserCookie $cookie
+     * @param DB $db
+     */
+    public function __construct(array $config, $cookie, $db)
+    {
+        $this->config = $config;
+        $this->cookie = $cookie;
+        $this->db = $db;
+    }
+
+    /**
+     * @param int $id
+     *
+     * @throws \InvalidArgumentException
+     * @retrun User
+     */
+    public function load($id = null)
+    {
+        if (null === $id) {
+            $user = $this->loadCurrent();
+            return new User($user, $this->config, $this->cookie);
+        } elseif ($id < 2) {
+            throw new InvalidArgumentException('User id can not be less than 2');
+        }
+
+
+    }
+
+    /**
+     * @retrun array
+     */
+    protected function loadCurrent()
+    {
+        if (($userId = $this->cookie->id()) === false) {
+            return $this->loadGuest();
+        }
+
+        $result = $this->db->query('SELECT u.*, g.*, o.logged, o.idle, o.witt_data FROM '.$this->db->prefix.'users AS u INNER JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$this->db->prefix.'online AS o ON o.user_id=u.id WHERE u.id='.$userId) or error('Unable to fetch user information', __FILE__, __LINE__, $this->db->error());
+        $user = $this->db->fetch_assoc($result);
+        $this->db->free_result($result);
+
+        if (empty($user['id']) || ! $this->cookie->verifyHash($user['id'], $user['password'])) {
+            return $this->loadGuest();
+        }
+
+        // проверка ip админа и модератора - Visman
+        if ($this->config['o_check_ip'] == '1' && ($user['g_id'] == PUN_ADMIN || $user['g_moderator'] == '1') && $user['registration_ip'] != get_remote_address())
+        {
+            return $this->loadGuest();
+        }
+
+        return $user;
+    }
+
+    /**
+     * @throws \RuntimeException
+     * @retrun array
+     */
+    protected function loadGuest()
+    {
+        $remote_addr = get_remote_address();
+        $result = $this->db->query('SELECT u.*, g.*, o.logged, o.last_post, o.last_search, o.witt_data FROM '.$this->db->prefix.'users AS u INNER JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$this->db->prefix.'online AS o ON o.ident=\''.$this->db->escape($remote_addr).'\' WHERE u.id=1') or error('Unable to fetch guest information', __FILE__, __LINE__, $this->db->error());
+        $user = $this->db->fetch_assoc($result);
+        $this->db->free_result($result);
+
+        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.');
+        }
+
+        return $user;
+    }
+}

+ 27 - 3
app/bootstrap.php

@@ -3,6 +3,7 @@
 namespace ForkBB;
 
 use R2\DependencyInjection\Container;
+use ForkBB\Models\Pages\Page;
 use Exception;
 
 if (! defined('PUN_ROOT'))
@@ -46,6 +47,29 @@ if (file_exists(__DIR__ . '/config/main.php')) {
 
 define('PUN', 1);
 
-$container->setParameter('DIR_CONFIG', __DIR__ . '/config/');
-$container->setParameter('DIR_CACHE', __DIR__ . '/cache/');
-$container->get('firstAction');
+$container->setParameter('DIR_CONFIG', __DIR__ . '/config');
+$container->setParameter('DIR_CACHE', __DIR__ . '/cache');
+$container->setParameter('DIR_VIEWS', __DIR__ . '/templates');
+$container->setParameter('DIR_TRANSL', __DIR__ . '/lang');
+$container->setParameter('START', $pun_start);
+
+$config = $container->get('config');
+$container->setParameter('date_formats', [$config['o_date_format'], 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y']);
+$container->setParameter('time_formats', [$config['o_time_format'], 'H:i:s', 'H:i', 'g:i:s a', 'g:i a']);
+
+$page = null;
+$controllers = ['Routing', 'Primary'];
+
+while (! $page instanceof Page && $cur = array_pop($controllers)) {
+    $page = $container->get($cur);
+}
+
+if ($page instanceof Page) { //????
+    $tpl = $container->get('View')->setPage($page)->outputPage();
+    if (defined('PUN_DEBUG')) {
+        $debug = $container->get('Debug')->debug();
+        $debug = $container->get('View')->setPage($debug)->outputPage();
+        $tpl = str_replace('<!-- debuginfo -->', $debug, $tpl);
+    }
+    exit($tpl);
+}

+ 2 - 1
app/config/install.php

@@ -33,12 +33,13 @@ return [
         'Install' => [
             'class' => \ForkBB\Core\Install::class,
             'request' => '@Request',
+            'container' => '%CONTAINER%',
         ],
         'Secury' => [
             'class' => \ForkBB\Core\Secury::class,
             'hmac' => '%HMAC%',
         ],
-        'firstAction' => '@Install:start',
+        'Primary' => '@Install:install',
     ],
     'multiple'  => [],
 ];

+ 112 - 0
app/lang/English/admin.po

@@ -0,0 +1,112 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "Admin menu"
+msgstr "Admin menu"
+
+msgid "Plugins menu"
+msgstr "Plugins menu"
+
+msgid "Moderator menu"
+msgstr "Moderator menu"
+
+msgid "Admin index"
+msgstr "Index"
+
+msgid "Categories"
+msgstr "Categories"
+
+msgid "Forums"
+msgstr "Forums"
+
+msgid "Users"
+msgstr "Users"
+
+msgid "User groups"
+msgstr "User groups"
+
+msgid "Admin options"
+msgstr "Options"
+
+msgid "Permissions"
+msgstr "Permissions"
+
+msgid "Censoring"
+msgstr "Censoring"
+
+msgid "Bans"
+msgstr "Bans"
+
+msgid "Prune"
+msgstr "Prune"
+
+msgid "Maintenance"
+msgstr "Maintenance"
+
+msgid "Reports"
+msgstr "Reports"
+
+msgid "Server statistics"
+msgstr "Server statistics"
+
+msgid "Admin title"
+msgstr "Administration"
+
+msgid "Go back"
+msgstr "Go back"
+
+msgid "Delete"
+msgstr "Delete"
+
+msgid "Update"
+msgstr "Update"
+
+msgid "Add"
+msgstr "Add"
+
+msgid "Edit"
+msgstr "Edit"
+
+msgid "Remove"
+msgstr "Remove"
+
+msgid "Yes"
+msgstr "Yes"
+
+msgid "No"
+msgstr "No"
+
+msgid "Save changes"
+msgstr "Save changes"
+
+msgid "Save"
+msgstr "Save"
+
+msgid "here"
+msgstr "here"
+
+msgid "Action"
+msgstr "Action"
+
+msgid "None"
+msgstr "None"
+
+msgid "Maintenance mode"
+msgstr "maintenance mode"
+
+msgid "No plugin message"
+msgstr "There is no plugin called %s in the plugin directory."
+
+msgid "Plugin failed message"
+msgstr "Loading of the plugin - <strong>%s</strong> - failed."

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

@@ -0,0 +1,142 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "Not available"
+msgstr "Not available"
+
+msgid "Forum admin head"
+msgstr "Forum administration"
+
+msgid "NA"
+msgstr "N/A"
+
+msgid "Welcome to admin"
+msgstr "Welcome to the ForkBB administration control panel. From here you can control vital aspects of the board. Depending on whether you are an administrator or a moderator you can:"
+
+msgid "Welcome 1"
+msgstr "Organize categories and forums."
+
+msgid "Welcome 2"
+msgstr "Set forum-wide options and preferences."
+
+msgid "Welcome 3"
+msgstr "Control permissions for users and guests."
+
+msgid "Welcome 4"
+msgstr "View IP statistics for users."
+
+msgid "Welcome 5"
+msgstr "Ban users."
+
+msgid "Welcome 6"
+msgstr "Censor words."
+
+msgid "Welcome 7"
+msgstr "Set up user groups and promotions."
+
+msgid "Welcome 8"
+msgstr "Prune old posts."
+
+msgid "Welcome 9"
+msgstr "Handle post reports."
+
+msgid "About head"
+msgstr "About ForkBB"
+
+msgid "ForkBB version label"
+msgstr "ForkBB version"
+
+msgid "ForkBB version data"
+msgstr "v %s"
+
+msgid "Server statistics label"
+msgstr "Server statistics"
+
+msgid "View server statistics"
+msgstr "View server statistics"
+
+msgid "Support label"
+msgstr "Support"
+
+msgid "PHPinfo disabled message"
+msgstr "The PHP function phpinfo() has been disabled on this server."
+
+msgid "Server statistics head"
+msgstr "Server statistics"
+
+msgid "Server load label"
+msgstr "Server load"
+
+msgid "Server load data"
+msgstr "%s - %s user(s) online"
+
+msgid "Environment label"
+msgstr "Environment"
+
+msgid "Environment data OS"
+msgstr "Operating system: %s"
+
+msgid "Show info"
+msgstr "Show info"
+
+msgid "Environment data version"
+msgstr "PHP: %s - %s"
+
+msgid "Environment data acc"
+msgstr "Accelerator: %s"
+
+msgid "Turck MMCache"
+msgstr "Turck MMCache"
+
+msgid "Turck MMCache link"
+msgstr "turck-mmcache.sourceforge.net/"
+
+msgid "ionCube PHP Accelerator"
+msgstr "ionCube PHP Accelerator"
+
+msgid "ionCube PHP Accelerator link"
+msgstr "www.php-accelerator.co.uk/"
+
+msgid "Alternative PHP Cache (APC)"
+msgstr "Alternative PHP Cache (APC)"
+
+msgid "Alternative PHP Cache (APC) link"
+msgstr "www.php.net/apc/"
+
+msgid "Zend Optimizer"
+msgstr "Zend Optimizer"
+
+msgid "Zend Optimizer link"
+msgstr "www.zend.com/products/guard/zend-optimizer/"
+
+msgid "eAccelerator"
+msgstr "eAccelerator"
+
+msgid "eAccelerator link"
+msgstr "www.eaccelerator.net/"
+
+msgid "XCache"
+msgstr "XCache"
+
+msgid "XCache link"
+msgstr "xcache.lighttpd.net/"
+
+msgid "Database label"
+msgstr "Database"
+
+msgid "Database data rows"
+msgstr "Rows: %s"
+
+msgid "Database data size"
+msgstr "Size: %s"

+ 478 - 0
app/lang/English/common.po

@@ -0,0 +1,478 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "lang_direction"
+msgstr "ltr"
+
+msgid "lang_identifier"
+msgstr "en"
+
+msgid "lang_decimal_point"
+msgstr "."
+
+msgid "lang_thousands_sep"
+msgstr ","
+
+msgid "Bad token"
+msgstr "Bad token."
+
+msgid "Bad request"
+msgstr "Bad request. The link you followed is incorrect or outdated."
+
+msgid "No view"
+msgstr "You do not have permission to view these forums."
+
+msgid "No permission"
+msgstr "You do not have permission to access this page."
+
+msgid "Bad referrer"
+msgstr "Bad csrf_hash. You were referred to this page from an unauthorized source."
+
+msgid "Bad csrf hash"
+msgstr "Bad CSRF hash. You were referred to this page from an unauthorized source."
+
+msgid "No cookie"
+msgstr "You appear to have logged in successfully, however a cookie has not been set. Please check your settings and if applicable, enable cookies for this website."
+
+msgid "Pun include extension"
+msgstr "Unable to process user include %s from template %s. &quot;%s&quot; files are not allowed"
+
+msgid "Pun include directory"
+msgstr "Unable to process user include %s from template %s. Directory traversal is not allowed"
+
+msgid "Pun include error"
+msgstr "Unable to process user include %s from template %s. There is no such file in neither the template directory nor in the user include directory"
+
+msgid "Hidden text"
+msgstr "Hidden text"
+
+msgid "Show"
+msgstr "Show"
+
+msgid "Hide"
+msgstr "Hide"
+
+msgid "Announcement"
+msgstr "Announcement"
+
+msgid "Options"
+msgstr "Options"
+
+msgid "Submit"
+msgstr "Submit"
+
+msgid "Ban message"
+msgstr "You are banned from this forum."
+
+msgid "Ban message 2"
+msgstr "The ban expires at the end of"
+
+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"
+
+msgid "Never"
+msgstr "Never"
+
+msgid "Today"
+msgstr "Today"
+
+msgid "Yesterday"
+msgstr "Yesterday"
+
+msgid "Info"
+msgstr "Info"
+
+msgid "Go back"
+msgstr "Go back"
+
+msgid "Maintenance"
+msgstr "Maintenance"
+
+msgid "Redirecting"
+msgstr "Redirecting"
+
+msgid "Click redirect"
+msgstr "Click here if you do not want to wait any longer (or if your browser does not automatically forward you)"
+
+msgid "on"
+msgstr "on"
+
+msgid "off"
+msgstr "off"
+
+msgid "Invalid email"
+msgstr "The email address you entered is invalid."
+
+msgid "Required"
+msgstr "(Required)"
+
+msgid "required field"
+msgstr "is a required field in this form."
+
+msgid "Last post"
+msgstr "Last post"
+
+msgid "by"
+msgstr "by"
+
+msgid "New posts"
+msgstr "New posts"
+
+msgid "New posts info"
+msgstr "Go to the first new post in this topic."
+
+msgid "Username"
+msgstr "Username"
+
+msgid "Password"
+msgstr "Password"
+
+msgid "Email"
+msgstr "Email"
+
+msgid "Send email"
+msgstr "Send email"
+
+msgid "Moderated by"
+msgstr "Moderated by"
+
+msgid "Registered"
+msgstr "Registered"
+
+msgid "Subject"
+msgstr "Subject"
+
+msgid "Message"
+msgstr "Message"
+
+msgid "Topic"
+msgstr "Topic"
+
+msgid "Forum"
+msgstr "Forum"
+
+msgid "Stats"
+msgstr "Statistics"
+
+msgid "Posts"
+msgstr "Posts"
+
+msgid "Replies"
+msgstr "Replies"
+
+msgid "Pages"
+msgstr "Pages:"
+
+msgid "Page"
+msgstr "Page %s"
+
+msgid "BBCode"
+msgstr "BBCode:"
+
+msgid "url tag"
+msgstr "[url] tag:"
+
+msgid "img tag"
+msgstr "[img] tag:"
+
+msgid "Smilies"
+msgstr "Smilies:"
+
+msgid "and"
+msgstr "and"
+
+msgid "Image link"
+msgstr "image"
+
+msgid "wrote"
+msgstr "wrote:"
+
+msgid "Mailer"
+msgstr "%s"
+
+msgid "Important information"
+msgstr "Important information"
+
+msgid "Write message legend"
+msgstr "Write your message and submit"
+
+msgid "Previous"
+msgstr "Previous"
+
+msgid "Next"
+msgstr "Next"
+
+msgid "Spacer"
+msgstr "…"
+
+msgid "Title"
+msgstr "Title"
+
+msgid "Member"
+msgstr "Member"
+
+msgid "Moderator"
+msgstr "Moderator"
+
+msgid "Administrator"
+msgstr "Administrator"
+
+msgid "Banned"
+msgstr "Banned"
+
+msgid "Guest"
+msgstr "Guest"
+
+msgid "BBCode error no opening tag"
+msgstr "[/%1$s] was found without a matching [%1$s]"
+
+msgid "BBCode error invalid nesting"
+msgstr "[%1$s] was opened within [%2$s], this is not allowed"
+
+msgid "BBCode error invalid self-nesting"
+msgstr "[%s] was opened within itself, this is not allowed"
+
+msgid "BBCode error no closing tag"
+msgstr "[%1$s] was found without a matching [/%1$s]"
+
+msgid "BBCode error empty attribute"
+msgstr "[%s] tag had an empty attribute section"
+
+msgid "BBCode error tag not allowed"
+msgstr "You are not allowed to use [%s] tags"
+
+msgid "BBCode error tag url not allowed"
+msgstr "You are not allowed to post links"
+
+msgid "BBCode list size error"
+msgstr "Your list was too long to parse, please make it smaller!"
+
+msgid "Index"
+msgstr "Index"
+
+msgid "User list"
+msgstr "User list"
+
+msgid "Rules"
+msgstr "Rules"
+
+msgid "Search"
+msgstr "Search"
+
+msgid "Register"
+msgstr "Register"
+
+msgid "Login"
+msgstr "Login"
+
+msgid "Not logged in"
+msgstr "You are not logged in."
+
+msgid "Profile"
+msgstr "Profile"
+
+msgid "Logout"
+msgstr "Logout"
+
+msgid "Logged in as"
+msgstr "Logged in as"
+
+msgid "Admin"
+msgstr "Administration"
+
+msgid "Last visit"
+msgstr "Last visit: %s"
+
+msgid "Topic searches"
+msgstr "Topics:"
+
+msgid "New posts header"
+msgstr "New"
+
+msgid "Active topics"
+msgstr "Active"
+
+msgid "Unanswered topics"
+msgstr "Unanswered"
+
+msgid "Posted topics"
+msgstr "Posted"
+
+msgid "Show new posts"
+msgstr "Find topics with new posts since your last visit."
+
+msgid "Show active topics"
+msgstr "Find topics with recent posts."
+
+msgid "Show unanswered topics"
+msgstr "Find topics with no replies."
+
+msgid "Show posted topics"
+msgstr "Find topics you have posted to."
+
+msgid "Mark all as read"
+msgstr "Mark all topics as read"
+
+msgid "Mark forum read"
+msgstr "Mark this forum as read"
+
+msgid "Title separator"
+msgstr " - "
+
+msgid "PM"
+msgstr "PM"
+
+msgid "PMsend"
+msgstr "Send private message"
+
+msgid "PMnew"
+msgstr "New private message"
+
+msgid "PMmess"
+msgstr "You have new private messages (%s msgs.)."
+
+msgid "Warn"
+msgstr "Warning"
+
+msgid "WarnMess"
+msgstr "You have a new warning!"
+
+msgid "Board footer"
+msgstr "Board footer"
+
+msgid "Jump to"
+msgstr "Jump to"
+
+msgid "Go"
+msgstr " Go "
+
+msgid "Moderate topic"
+msgstr "Moderate topic"
+
+msgid "All"
+msgstr "All"
+
+msgid "Move topic"
+msgstr "Move topic"
+
+msgid "Open topic"
+msgstr "Open topic"
+
+msgid "Close topic"
+msgstr "Close topic"
+
+msgid "Unstick topic"
+msgstr "Unstick topic"
+
+msgid "Stick topic"
+msgstr "Stick topic"
+
+msgid "Moderate forum"
+msgstr "Moderate forum"
+
+msgid "Powered by"
+msgstr "Powered by <a href=\"https://github.com/forkbb\">ForkBB</a>"
+
+msgid "Debug table"
+msgstr "Debug information"
+
+msgid "Querytime"
+msgstr "Generated in %1$s seconds, %2$s queries executed"
+
+msgid "Memory usage"
+msgstr "Memory usage: %1$s"
+
+msgid "Peak usage"
+msgstr "(Peak: %1$s)"
+
+msgid "Query times"
+msgstr "Time (s)"
+
+msgid "Query"
+msgstr "Query"
+
+msgid "Total query time"
+msgstr "Total query time: %s"
+
+msgid "RSS description"
+msgstr "The most recent topics at %s."
+
+msgid "RSS description topic"
+msgstr "The most recent posts in %s."
+
+msgid "RSS reply"
+msgstr "Re: "
+
+msgid "RSS active topics feed"
+msgstr "RSS active topics feed"
+
+msgid "Atom active topics feed"
+msgstr "Atom active topics feed"
+
+msgid "RSS forum feed"
+msgstr "RSS forum feed"
+
+msgid "Atom forum feed"
+msgstr "Atom forum feed"
+
+msgid "RSS topic feed"
+msgstr "RSS topic feed"
+
+msgid "Atom topic feed"
+msgstr "Atom topic feed"
+
+msgid "After time"
+msgstr "Added later"
+
+msgid "After time s"
+msgstr " s"
+
+msgid "After time i"
+msgstr " min"
+
+msgid "After time H"
+msgstr " h"
+
+msgid "After time d"
+msgstr " d"
+
+msgid "New reports"
+msgstr "There are new reports"
+
+msgid "Maintenance mode enabled"
+msgstr "Maintenance mode is enabled!"
+
+msgid "Size unit B"
+msgstr "%s B"
+
+msgid "Size unit KiB"
+msgstr "%s KiB"
+
+msgid "Size unit MiB"
+msgstr "%s MiB"
+
+msgid "Size unit GiB"
+msgstr "%s GiB"
+
+msgid "Size unit TiB"
+msgstr "%s TiB"
+
+msgid "Size unit PiB"
+msgstr "%s PiB"
+
+msgid "Size unit EiB"
+msgstr "%s EiB"

+ 55 - 0
app/lang/English/index.po

@@ -0,0 +1,55 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "Topics"
+msgstr "Topics"
+
+msgid "Link to"
+msgstr "Link to:"
+
+msgid "Empty board"
+msgstr "Board is empty."
+
+msgid "Newest user"
+msgstr "Newest registered user:"
+
+msgid "Users online"
+msgstr "Registered users online:"
+
+msgid "Guests online"
+msgstr "guests:"
+
+msgid "No of users"
+msgstr "Total number of registered users:"
+
+msgid "No of topics"
+msgstr "Total number of topics:"
+
+msgid "No of posts"
+msgstr "Total number of posts:"
+
+msgid "Online"
+msgstr "Online:"
+
+msgid "Board info"
+msgstr "Board information"
+
+msgid "Board stats"
+msgstr "Board statistics"
+
+msgid "Most online"
+msgstr "Maximum online users (%1$s) was be there %2$s"
+
+msgid "User info"
+msgstr "User information"

+ 67 - 0
app/lang/English/login.po

@@ -0,0 +1,67 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "Login errors"
+msgstr "Login error"
+
+msgid "Login errors info"
+msgstr "The following error needs to be corrected before you can login:"
+
+msgid "Wrong user/pass"
+msgstr "Wrong username and/or password."
+
+msgid "Forgotten pass"
+msgstr "Forgotten your password?"
+
+msgid "Login redirect"
+msgstr "Logged in successfully. Redirecting …"
+
+msgid "Logout redirect"
+msgstr "Logged out. Redirecting …"
+
+msgid "No email match"
+msgstr "There is no user registered with the email address"
+
+msgid "Request pass"
+msgstr "Request password"
+
+msgid "Request pass legend"
+msgstr "Enter the email address with which you registered"
+
+msgid "Request pass info"
+msgstr "A new password together with a link to activate the new password will be sent to that address."
+
+msgid "Not registered"
+msgstr "Not registered yet?"
+
+msgid "Login legend"
+msgstr "Enter your username and password below"
+
+msgid "Remember me"
+msgstr "Remember me"
+
+msgid "Login info"
+msgstr "If you have not registered or have forgotten your password click on the appropriate link below."
+
+msgid "New password errors"
+msgstr "Password request error"
+
+msgid "New passworderrors info"
+msgstr "The following error needs to be corrected before a new password can be sent:"
+
+msgid "Forget mail"
+msgstr "An email has been sent to the specified address with instructions on how to change your password. If it does not arrive you can contact the forum administrator at"
+
+msgid "Email flood"
+msgstr "This account has already requested a password reset in the past hour. Please wait %s minutes before requesting a new password again."

+ 34 - 0
app/lang/English/subforums.po

@@ -0,0 +1,34 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "Sub forum"
+msgid_plural "Sub forums"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Parent forum"
+msgstr "Parent forum"
+
+msgid "No parent forum"
+msgstr "No parent forum"
+
+msgid "%s Topic"
+msgid_plural "%s Topics"
+msgstr[0] "<strong>%s</strong> Topic"
+msgstr[1] "<strong>%s</strong> Topics"
+
+msgid "%s Post"
+msgid_plural "%s Posts"
+msgstr[0] "<strong>%s</strong> Post"
+msgstr[1] "<strong>%s</strong> Posts"

+ 112 - 0
app/lang/Russian/admin.po

@@ -0,0 +1,112 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "Admin menu"
+msgstr "Админка"
+
+msgid "Plugins menu"
+msgstr "Плагины"
+
+msgid "Moderator menu"
+msgstr "Модерирование"
+
+msgid "Admin index"
+msgstr "Главная"
+
+msgid "Categories"
+msgstr "Категории"
+
+msgid "Forums"
+msgstr "Разделы"
+
+msgid "Users"
+msgstr "Пользователи"
+
+msgid "User groups"
+msgstr "Группы"
+
+msgid "Admin options"
+msgstr "Опции"
+
+msgid "Permissions"
+msgstr "Права"
+
+msgid "Censoring"
+msgstr "Цензура"
+
+msgid "Bans"
+msgstr "Баны"
+
+msgid "Prune"
+msgstr "Очистка"
+
+msgid "Maintenance"
+msgstr "Обслуживание"
+
+msgid "Reports"
+msgstr "Сигналы"
+
+msgid "Server statistics"
+msgstr "Статистика сервера"
+
+msgid "Admin title"
+msgstr "Администрирование"
+
+msgid "Go back"
+msgstr "Назад"
+
+msgid "Delete"
+msgstr "Удалить"
+
+msgid "Update"
+msgstr "Изменить"
+
+msgid "Add"
+msgstr "Добавить"
+
+msgid "Edit"
+msgstr "Править"
+
+msgid "Remove"
+msgstr "Убрать"
+
+msgid "Yes"
+msgstr "Да"
+
+msgid "No"
+msgstr "Нет"
+
+msgid "Save changes"
+msgstr "Сохранить изменения"
+
+msgid "Save"
+msgstr "Сохранить"
+
+msgid "here"
+msgstr "здесь"
+
+msgid "Action"
+msgstr "Действие"
+
+msgid "None"
+msgstr "Пусто"
+
+msgid "Maintenance mode"
+msgstr "режим обслуживания"
+
+msgid "No plugin message"
+msgstr "Нет плагина с именем %s в директории плагинов."
+
+msgid "Plugin failed message"
+msgstr "Загрузить плагин <strong>%s</strong> не удалось."

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

@@ -0,0 +1,142 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "Not available"
+msgstr "Not available"
+
+msgid "Forum admin head"
+msgstr "Администрирование форума"
+
+msgid "NA"
+msgstr "N/A"
+
+msgid "Welcome to admin"
+msgstr "Добро пожаловать в панель управления ForkBB! Здесь вы можете настроить ключевые параметры вашего форума. В зависимости от того, Администратор вы или Модератор, вы можете:"
+
+msgid "Welcome 1"
+msgstr "Настроить категории и разделы."
+
+msgid "Welcome 2"
+msgstr "Установить параметры по-умолчанию для форума в целом."
+
+msgid "Welcome 3"
+msgstr "Контролировать права пользователей и гостей."
+
+msgid "Welcome 4"
+msgstr "Смотреть статистику пользователей по IP."
+
+msgid "Welcome 5"
+msgstr "Банить пользователей."
+
+msgid "Welcome 6"
+msgstr "Настроить список цензурируемых слов."
+
+msgid "Welcome 7"
+msgstr "Установить группы пользователей и продвижение по ним."
+
+msgid "Welcome 8"
+msgstr "Очистить старые сообщения."
+
+msgid "Welcome 9"
+msgstr "Реагировать на сигналы пользователей."
+
+msgid "About head"
+msgstr "О ForkBB"
+
+msgid "ForkBB version label"
+msgstr "Версия ForkBB"
+
+msgid "ForkBB version data"
+msgstr "v %s"
+
+msgid "Server statistics label"
+msgstr "Статистика сервера"
+
+msgid "View server statistics"
+msgstr "Смотреть статистику сервера"
+
+msgid "Support label"
+msgstr "Саппорт"
+
+msgid "PHPinfo disabled message"
+msgstr "Функция PHP phpinfo() была отключена на этом сервере."
+
+msgid "Server statistics head"
+msgstr "Статистика сервера"
+
+msgid "Server load label"
+msgstr "Загрузка сервера"
+
+msgid "Server load data"
+msgstr "%s - %s пользователей online"
+
+msgid "Environment label"
+msgstr "Окружение"
+
+msgid "Environment data OS"
+msgstr "Операционная система: %s"
+
+msgid "Show info"
+msgstr "Смотреть подробности"
+
+msgid "Environment data version"
+msgstr "PHP: %s - %s"
+
+msgid "Environment data acc"
+msgstr "Accelerator: %s"
+
+msgid "Turck MMCache"
+msgstr "Turck MMCache"
+
+msgid "Turck MMCache link"
+msgstr "turck-mmcache.sourceforge.net"
+
+msgid "ionCube PHP Accelerator"
+msgstr "ionCube PHP Accelerator"
+
+msgid "ionCube PHP Accelerator link"
+msgstr "www.php-accelerator.co.uk/"
+
+msgid "Alternative PHP Cache (APC)"
+msgstr "Alternative PHP Cache (APC)"
+
+msgid "Alternative PHP Cache (APC) link"
+msgstr "www.php.net/apc/"
+
+msgid "Zend Optimizer"
+msgstr "Zend Optimizer"
+
+msgid "Zend Optimizer link"
+msgstr "www.zend.com/products/guard/zend-optimizer/"
+
+msgid "eAccelerator"
+msgstr "eAccelerator"
+
+msgid "eAccelerator link"
+msgstr "www.eaccelerator.net/"
+
+msgid "XCache"
+msgstr "XCache"
+
+msgid "XCache link"
+msgstr "xcache.lighttpd.net/"
+
+msgid "Database label"
+msgstr "База данных"
+
+msgid "Database data rows"
+msgstr "Строк: %s"
+
+msgid "Database data size"
+msgstr "Размер: %s"

+ 478 - 0
app/lang/Russian/common.po

@@ -0,0 +1,478 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "lang_direction"
+msgstr "ltr"
+
+msgid "lang_identifier"
+msgstr "ru"
+
+msgid "lang_decimal_point"
+msgstr "."
+
+msgid "lang_thousands_sep"
+msgstr ","
+
+msgid "Bad request"
+msgstr "Неверный запрос. Ссылка, по которой вы перешли, является неверной или просроченной."
+
+msgid "No view"
+msgstr "У вас нет прав для просмотра форума."
+
+msgid "No permission"
+msgstr "У вас нет прав на просмотр этой страницы."
+
+msgid "Bad token"
+msgstr "Неверный токен."
+
+msgid "Bad referrer"
+msgstr "Неверный csrf_hash. Вы перешли на эту страницу из неавторизованного источника."
+
+msgid "Bad csrf hash"
+msgstr "Неверный CSRF хэш. Вы перешли на эту страницу из неавторизованного источника."
+
+msgid "No cookie"
+msgstr "Вы вошли, но куки (cookie) не были установлены. Пожалуйста проверьте настройки браузера и, если возможно, разрешите куки для этого сайта."
+
+msgid "Pun include extension"
+msgstr "Невозможно подключить файл %s из шаблона %s. Нет разрешения на подлючение файлов с расширением &quot;%s&quot;"
+
+msgid "Pun include directory"
+msgstr "Невозможно подключить файл %s из шаблона %s. Переключение директорий запрещено."
+
+msgid "Pun include error"
+msgstr "Невозможно подключить файл %s из шаблона %s. Файл отсутствует и в шаблоне, и в директории пользователя."
+
+msgid "Hidden text"
+msgstr "Скрытый текст"
+
+msgid "Show"
+msgstr "Показать"
+
+msgid "Hide"
+msgstr "Скрыть"
+
+msgid "Announcement"
+msgstr "Объявление"
+
+msgid "Options"
+msgstr "Параметры"
+
+msgid "Submit"
+msgstr "Отправить"
+
+msgid "Ban message"
+msgstr "Ваша учетная запись заблокирована."
+
+msgid "Ban message 2"
+msgstr "Блокировка заканчивается"
+
+msgid "Ban message 3"
+msgstr "Причина блокирования:"
+
+msgid "Ban message 4"
+msgstr "Все возникшие вопросы отправляйте администратору форума по адресу"
+
+msgid "Never"
+msgstr "Нет"
+
+msgid "Today"
+msgstr "Сегодня"
+
+msgid "Yesterday"
+msgstr "Вчера"
+
+msgid "Info"
+msgstr "Информация"
+
+msgid "Go back"
+msgstr "Назад"
+
+msgid "Maintenance"
+msgstr "Обслуживание"
+
+msgid "Redirecting"
+msgstr "Перенаправление"
+
+msgid "Click redirect"
+msgstr "Кликните здесь, если вы не желаете ждать (или ваш браузер не поддерживает перенаправление)."
+
+msgid "on"
+msgstr "вкл."
+
+msgid "off"
+msgstr "выкл."
+
+msgid "Invalid email"
+msgstr "Указанный почтовый адрес неверен."
+
+msgid "Required"
+msgstr "(Обязательно)"
+
+msgid "required field"
+msgstr "необходимое поле в этой форме."
+
+msgid "Last post"
+msgstr "Последнее сообщение"
+
+msgid "by"
+msgstr "от"
+
+msgid "New posts"
+msgstr "Новые&nbsp;сообщения"
+
+msgid "New posts info"
+msgstr "Перейти к первому новому сообщению в теме."
+
+msgid "Username"
+msgstr "Имя"
+
+msgid "Password"
+msgstr "Пароль"
+
+msgid "Email"
+msgstr "E-mail"
+
+msgid "Send email"
+msgstr "Отправить письмо"
+
+msgid "Moderated by"
+msgstr "Модератор(ы):"
+
+msgid "Registered"
+msgstr "Дата регистрации"
+
+msgid "Subject"
+msgstr "Заголовок темы"
+
+msgid "Message"
+msgstr "Сообщение"
+
+msgid "Topic"
+msgstr "Тема"
+
+msgid "Forum"
+msgstr "Раздел"
+
+msgid "Stats"
+msgstr "Статистика"
+
+msgid "Posts"
+msgstr "Сообщений"
+
+msgid "Replies"
+msgstr "Ответов"
+
+msgid "Pages"
+msgstr "Страницы"
+
+msgid "Page"
+msgstr "Страница %s"
+
+msgid "BBCode"
+msgstr "BB-коды:"
+
+msgid "url tag"
+msgstr "Тег [url]:"
+
+msgid "img tag"
+msgstr "Тег [img]:"
+
+msgid "Smilies"
+msgstr "Смайлы:"
+
+msgid "and"
+msgstr "и"
+
+msgid "Image link"
+msgstr "картинка"
+
+msgid "wrote"
+msgstr "пишет:"
+
+msgid "Mailer"
+msgstr "%s"
+
+msgid "Important information"
+msgstr "Важная информация"
+
+msgid "Write message legend"
+msgstr "Введите сообщение и нажмите Отправить"
+
+msgid "Previous"
+msgstr "Назад"
+
+msgid "Next"
+msgstr "Вперед"
+
+msgid "Spacer"
+msgstr "&hellip;"
+
+msgid "Title"
+msgstr "Титул"
+
+msgid "Member"
+msgstr "Участник"
+
+msgid "Moderator"
+msgstr "Модератор"
+
+msgid "Administrator"
+msgstr "Администратор"
+
+msgid "Banned"
+msgstr "Забанен"
+
+msgid "Guest"
+msgstr "Гость"
+
+msgid "BBCode error no opening tag"
+msgstr "Обнаружен парный тег [/%1$s] без соответствующего начального тега [%1$s]"
+
+msgid "BBCode error invalid nesting"
+msgstr "Тег [%1$s] открывается внутри [%2$s], это недопустимо"
+
+msgid "BBCode error invalid self-nesting"
+msgstr "Тег [%s] открывается внутри такого же тега, это недопустимо"
+
+msgid "BBCode error no closing tag"
+msgstr "Обнаружен парный тег [%1$s] без соответствующего закрывающего тега [/%1$s]"
+
+msgid "BBCode error empty attribute"
+msgstr "Тег [%s] с пустым атрибутом"
+
+msgid "BBCode error tag not allowed"
+msgstr "Вам нельзя использовать тег [%s]"
+
+msgid "BBCode error tag url not allowed"
+msgstr "Вам нельзя использовать ссылки в сообщениях"
+
+msgid "BBCode list size error"
+msgstr "Ваш список слишком велик, пожалуйста уменьшите его!"
+
+msgid "Index"
+msgstr "Форум"
+
+msgid "User list"
+msgstr "Пользователи"
+
+msgid "Rules"
+msgstr "Правила"
+
+msgid "Search"
+msgstr "Поиск"
+
+msgid "Register"
+msgstr "Регистрация"
+
+msgid "Login"
+msgstr "Вход"
+
+msgid "Not logged in"
+msgstr "Вы не вошли."
+
+msgid "Profile"
+msgstr "Профиль"
+
+msgid "Logout"
+msgstr "Выход"
+
+msgid "Logged in as"
+msgstr "Вы вошли как"
+
+msgid "Admin"
+msgstr "Админка"
+
+msgid "Last visit"
+msgstr "Последний визит: %s"
+
+msgid "Topic searches"
+msgstr "Темы:"
+
+msgid "New posts header"
+msgstr "Новые"
+
+msgid "Active topics"
+msgstr "Активные"
+
+msgid "Unanswered topics"
+msgstr "Без ответа"
+
+msgid "Posted topics"
+msgstr "С вашим участием"
+
+msgid "Show new posts"
+msgstr "Найти темы с новыми сообщениями."
+
+msgid "Show active topics"
+msgstr "Найти темы с недавними сообщениями."
+
+msgid "Show unanswered topics"
+msgstr "Найти темы без ответов."
+
+msgid "Show posted topics"
+msgstr "Найти темы с вашим участием."
+
+msgid "Mark all as read"
+msgstr "Отметить всё как прочтённое"
+
+msgid "Mark forum read"
+msgstr "Отметить раздел как прочтённый"
+
+msgid "Title separator"
+msgstr " - "
+
+msgid "PM"
+msgstr "ЛС"
+
+msgid "PMsend"
+msgstr "Отправить личное сообщение"
+
+msgid "PMnew"
+msgstr "Новое личное сообщение"
+
+msgid "PMmess"
+msgstr "У вас есть новые личные сообщения (%s шт.)."
+
+msgid "Warn"
+msgstr "Предупреждение"
+
+msgid "WarnMess"
+msgstr "Вы получили новое предупреждение!"
+
+msgid "Board footer"
+msgstr "Подвал форума"
+
+msgid "Jump to"
+msgstr "Перейти"
+
+msgid "Go"
+msgstr " Иди "
+
+msgid "Moderate topic"
+msgstr "Модерирование темы"
+
+msgid "All"
+msgstr "All"
+
+msgid "Move topic"
+msgstr "Перенести тему"
+
+msgid "Open topic"
+msgstr "Открыть тему"
+
+msgid "Close topic"
+msgstr "Закрыть тему"
+
+msgid "Unstick topic"
+msgstr "Снять выделение темы"
+
+msgid "Stick topic"
+msgstr "Выделить тему"
+
+msgid "Moderate forum"
+msgstr "Модерирование раздела"
+
+msgid "Powered by"
+msgstr "Под управлением <a href=\"https://github.com/forkbb\">ForkBB</a>"
+
+msgid "Debug table"
+msgstr "Отладочная информация"
+
+msgid "Querytime"
+msgstr "Сгенерировано за %1$s сек, %2$s запросов выполнено"
+
+msgid "Memory usage"
+msgstr "Использовано памяти: %1$s"
+
+msgid "Peak usage"
+msgstr "(Пик: %1$s)"
+
+msgid "Query times"
+msgstr "Время (s)"
+
+msgid "Query"
+msgstr "Запрос"
+
+msgid "Total query time"
+msgstr "Итого: %s"
+
+msgid "RSS description"
+msgstr "Самые свежие темы на %s."
+
+msgid "RSS description topic"
+msgstr "Самые свежие сообщения в %s."
+
+msgid "RSS reply"
+msgstr "Re: "
+
+msgid "RSS active topics feed"
+msgstr "RSS лента активных тем"
+
+msgid "Atom active topics feed"
+msgstr "Atom лента активных тем"
+
+msgid "RSS forum feed"
+msgstr "RSS лента раздела"
+
+msgid "Atom forum feed"
+msgstr "Atom лента раздела"
+
+msgid "RSS topic feed"
+msgstr "RSS лента темы"
+
+msgid "Atom topic feed"
+msgstr "Atom лента темы"
+
+msgid "After time"
+msgstr "Добавлено спустя"
+
+msgid "After time s"
+msgstr " с"
+
+msgid "After time i"
+msgstr " мин"
+
+msgid "After time H"
+msgstr " ч"
+
+msgid "After time d"
+msgstr " дн"
+
+msgid "New reports"
+msgstr "Есть новые сигналы"
+
+msgid "Maintenance mode enabled"
+msgstr "Включен режим обслуживания!"
+
+msgid "Size unit B"
+msgstr "%s байт"
+
+msgid "Size unit KiB"
+msgstr "%s Кбайт"
+
+msgid "Size unit MiB"
+msgstr "%s Мбайт"
+
+msgid "Size unit GiB"
+msgstr "%s Гбайт"
+
+msgid "Size unit TiB"
+msgstr "%s Тбайт"
+
+msgid "Size unit PiB"
+msgstr "%s Пбайт"
+
+msgid "Size unit EiB"
+msgstr "%s Эбайт"

+ 55 - 0
app/lang/Russian/index.po

@@ -0,0 +1,55 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "Topics"
+msgstr "Тем"
+
+msgid "Link to"
+msgstr "Ссылка:"
+
+msgid "Empty board"
+msgstr "Форум пуст."
+
+msgid "Newest user"
+msgstr "Новичок:"
+
+msgid "Users online"
+msgstr "Сейчас пользователей:"
+
+msgid "Guests online"
+msgstr "гостей:"
+
+msgid "No of users"
+msgstr "Всего пользователей:"
+
+msgid "No of topics"
+msgstr "Всего тем:"
+
+msgid "No of posts"
+msgstr "Всего сообщений:"
+
+msgid "Online"
+msgstr "Активны:"
+
+msgid "Board info"
+msgstr "Информация о форуме"
+
+msgid "Board stats"
+msgstr "Статистика форума"
+
+msgid "Most online"
+msgstr "Больше всего онлайн посетителей (%1$s) здесь было %2$s"
+
+msgid "User info"
+msgstr "Информация о пользователях"

+ 67 - 0
app/lang/Russian/login.po

@@ -0,0 +1,67 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "Login errors"
+msgstr "Ошибки входа на форум"
+
+msgid "Login errors info"
+msgstr "Следующие ошибки необходимо исправить, чтобы войти на форум:"
+
+msgid "Wrong user/pass"
+msgstr "Неверное имя и/или пароль. Имя и пароль чувствительны к регистру!"
+
+msgid "Forgotten pass"
+msgstr "Забыли пароль?"
+
+msgid "Login redirect"
+msgstr "Успешный вход. Переадресация &hellip;"
+
+msgid "Logout redirect"
+msgstr "Выход произведён. Переадресация &hellip;"
+
+msgid "No email match"
+msgstr "На форуме нет пользователя с почтовым адресом"
+
+msgid "Request pass"
+msgstr "Восстановление пароля"
+
+msgid "Request pass legend"
+msgstr "Введите почтовый адрес, на который вы регистрировались"
+
+msgid "Request pass info"
+msgstr "Новый пароль и ссылка для активации будут высланы на указанный адрес."
+
+msgid "Not registered"
+msgstr "Ещё не зарегистрированы?"
+
+msgid "Login legend"
+msgstr "Введите имя пользователя и пароль"
+
+msgid "Remember me"
+msgstr "Запомнить меня"
+
+msgid "Login info"
+msgstr "Если вы ещё не зарегистрированы или забыли пароль, кликните на подходящей ссылке ниже."
+
+msgid "New password errors"
+msgstr "Ошибки при запросе пароля"
+
+msgid "New passworderrors info"
+msgstr "Следующие ошибки нужно исправит, чтобы получить новый пароль:"
+
+msgid "Forget mail"
+msgstr "Письмо с инструкцией по изменению пароля было выслано на указанный вами почтовый адрес. Если вы не получите его, свяжитесь с администрацией форума, используя почтовый адрес"
+
+msgid "Email flood"
+msgstr "Для этой учетной записи недавно уже запрашивали новый пароль. Пожалуйста, подождите %s минут, прежде чем повторить попытку."

+ 37 - 0
app/lang/Russian/subforums.po

@@ -0,0 +1,37 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "Sub forum"
+msgid_plural "Sub forums"
+msgstr[0] "Подраздел"
+msgstr[1] "Подразделы"
+msgstr[2] "Подразделы"
+
+msgid "Parent forum"
+msgstr "Родительский раздел"
+
+msgid "No parent forum"
+msgstr "Нет родительского раздела"
+
+msgid "%s Topic"
+msgid_plural "%s Topics"
+msgstr[0] "<strong>%s</strong> Тема"
+msgstr[1] "<strong>%s</strong> Темы"
+msgstr[2] "<strong>%s</strong> Тем"
+
+msgid "%s Post"
+msgid_plural "%s Posts"
+msgstr[0] "<strong>%s</strong> Сообщение"
+msgstr[1] "<strong>%s</strong> Сообщения"
+msgstr[2] "<strong>%s</strong> Сообщений"

+ 29 - 0
app/templates/admin/index.tpl

@@ -0,0 +1,29 @@
+@extends('layouts/admin')
+      <section class="f-admin">
+        <h2>{!! __('Forum admin head') !!}</h2>
+        <div>
+          <p>{!! __('Welcome to admin') !!}</p>
+          <ul>
+            <li><span>{!! __('Welcome 1') !!}</span></li>
+            <li><span>{!! __('Welcome 2') !!}</span></li>
+            <li><span>{!! __('Welcome 3') !!}</span></li>
+            <li><span>{!! __('Welcome 4') !!}</span></li>
+            <li><span>{!! __('Welcome 5') !!}</span></li>
+            <li><span>{!! __('Welcome 6') !!}</span></li>
+            <li><span>{!! __('Welcome 7') !!}</span></li>
+            <li><span>{!! __('Welcome 8') !!}</span></li>
+            <li><span>{!! __('Welcome 9') !!}</span></li>
+          </ul>
+        </div>
+        <h2>{!! __('About head') !!}</h2>
+        <div>
+          <dl>
+            <dt>{!! __('ForkBB version label') !!}</dt>
+            <dd>{!! __('ForkBB version data', $version) !!}</dd>
+            <dt>{!! __('Server statistics label') !!}</dt>
+            <dd><a href="{!! $linkStat !!}">{!! __('View server statistics') !!}</a></dd>
+            <dt>{!! __('Support label') !!}</dt>
+            <dd><a href="https://github.com/forkbb/forkbb">GitHub</a></dd>
+          </dl>
+        </div>
+      </section>

+ 26 - 0
app/templates/admin/statistics.tpl

@@ -0,0 +1,26 @@
+@extends('layouts/admin')
+      <section class="f-admin">
+        <h2>{!! __('Server statistics head') !!}</h2>
+        <div>
+          <dl>
+            <dt>{!! __('Server load label') !!}</dt>
+            <dd>{!! __('Server load data', $serverLoad, $numOnline) !!}</dd>
+@if($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) !!}
+            </dd>
+            <dt>{!! __('Database label') !!}</dt>
+            <dd>
+              {!! $dbVersion !!}
+@if($tRecords && $tSize)
+              <br>{!! __('Database data rows', $tRecords) !!}
+              <br>{!! __('Database data size', $tSize) !!}
+@endif
+            </dd>
+@endif
+          </dl>
+        </div>
+      </section>

+ 25 - 0
app/templates/auth.tpl

@@ -0,0 +1,25 @@
+@extends('layouts/main')
+    <section class="f-main f-login">
+      <div class="f-wrapdiv">
+        <h2>{!! __('Login') !!}</h2>
+        <form id="id-aform" method="post" action="{!! $formAction !!}">
+          <input type="hidden" name="token" value="{!! $formToken !!}">
+          <input type="hidden" name="redirect" value="{{ $formRedirect }}">
+          <label id="id-label1" for="id-fname">{!! __('Username') !!}</label>
+          <input required id="id-fname" type="text" name="name" value="{{ $name }}" maxlength="25" autofocus="autofocus" spellcheck="false" tabindex="1">
+          <label id="id-label2" for="id-fpassword">{!! __('Password') !!}<a class="f-forgetlink" href="{!! $forgetLink !!}" tabindex="5">{!! __('Forgotten pass') !!}</a></label>
+          <input required id="id-fpassword" type="password" name="password" tabindex="2">
+@if($formSave)
+          <label id="id-label3"><input type="checkbox" name="save" value="1" tabindex="3" checked="checked">{!! __('Remember me') !!}</label>
+@else
+          <label id="id-label3"><input type="checkbox" name="save" value="1" tabindex="3">{!! __('Remember me') !!}</label>
+@endif
+          <input class="f-btn" type="submit" name="login" value="{!! __('Login') !!}" tabindex="4">
+        </form>
+      </div>
+@if($regLink)
+      <div class="f-wrapdiv">
+        <p><a href="{!! $regLink !!}" tabindex="6">{!! __('Not registered') !!}</a></p>
+      </div>
+@endif
+    </section>

+ 13 - 0
app/templates/ban.tpl

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

+ 131 - 0
app/templates/index.tpl

@@ -0,0 +1,131 @@
+@extends('layouts/main')
+@if($forums)
+    <section class="f-main">
+      <ol class="f-forumlist">
+@foreach($forums as $id => $cat)
+        <li id="cat-{!! $id !!}" class="f-category">
+          <h2>{{ $cat['name'] }}</h2>
+          <ol class="f-table">
+            <li class="f-row f-thead" value="0">
+              <div class="f-hcell f-cmain">{!! __('Forum') !!}</div>
+              <div class="f-hcell f-cstats">{!! __('Stats') !!}</div>
+              <div class="f-hcell f-clast">{!! __('Last post') !!}</div>
+            </li>
+@foreach($cat['forums'] as $cur)
+@if($cur['redirect_url'])
+            <li id="forum-{!! $cur['fid']!!}" class="f-row f-fredir">
+              <div class="f-cell f-cmain">
+                <div class="f-ficon"></div>
+                <div class="f-finfo">
+                  <h3><span class="f-fredirtext">{!! __('Link to') !!}</span> <a href="{!! $cur['redirect_url'] !!}">{{ $cur['forum_name'] }}</a></h3>
+@if($cur['forum_desc'])
+                  <p class="f-fdesc">{!! $cur['forum_desc'] !!}</p>
+@endif
+                </div>
+              </div>
+            </li>
+@else
+@if($cur['new'])
+            <li id="forum-{!! $cur['fid'] !!}" class="f-row f-fnew">
+@else
+            <li id="forum-{!! $cur['fid'] !!}" class="f-row">
+@endif
+              <div class="f-cell f-cmain">
+                <div class="f-ficon"></div>
+                <div class="f-finfo">
+                  <h3>
+                    <a href="{!! $cur['forum_link'] !!}">{{ $cur['forum_name'] }}</a>
+@if($cur['new'])
+                    <span class="f-newtxt"><a href="">{!! __('New posts') !!}</a></span>
+@endif
+                  </h3>
+@if($cur['subforums'])
+                  <dl class="f-inline f-fsub"><!--inline-->
+                    <dt>{!! __('Sub forum', count($cur['subforums'])) !!}</dt>
+@foreach($cur['subforums'] as $sub)
+                    <dd><a href="{!! $sub[0] !!}">{{ $sub[1] }}</a></dd>
+@endforeach
+                  </dl><!--endinline-->
+@endif
+@if($cur['forum_desc'])
+                  <p class="f-fdesc">{!! $cur['forum_desc'] !!}</p>
+@endif
+@if($cur['moderators'])
+                  <dl class="f-inline f-modlist"><!--inline-->
+                    <dt>{!! __('Moderated by') !!}</dt>
+@foreach($cur['moderators'] as $mod)
+@if(is_string($mod))
+                    <dd>{{ $mod }}</dd>
+@else
+                    <dd><a href="{!! $mod[0] !!}">{{ $mod[1] }}</a></dd>
+@endif
+@endforeach
+                  </dl><!--endinline-->
+@endif
+                </div>
+              </div>
+              <div class="f-cell f-cstats">
+                <ul>
+                  <li>{!! __('%s Topic', $cur['num_topics'], $cur['topics']) !!}</li>
+                  <li>{!! __('%s Post', $cur['num_posts'], $cur['posts'])!!}</li>
+                </ul>
+              </div>
+              <div class="f-cell f-clast">
+                <ul>
+@if($cur['last_post_id'])
+                  <li class="f-cltopic"><a href="{!! $cur['last_post_id'] !!}" title="&quot;{{ $cur['last_topic'] }}&quot; - {!! __('Last post') !!}">{{ $cur['last_topic'] }}</a></li>
+                  <li class="f-clposter">{!! __('by') !!} {{ $cur['last_poster'] }}</li>
+                  <li class="f-cltime">{!! $cur['last_post'] !!}</li>
+@else
+                  <li class="f-cltopic">{!! __('Never') !!}</li>
+@endif
+                </ul>
+              </div>
+            </li>
+@endif
+@endforeach
+          </ol>
+        </li>
+@endforeach
+      </ol>
+    </section>
+@else
+    <section class="f-main f-message">
+      <h2>{!! __('Empty board') !!}</h2>
+    </section>
+@endif
+    <section class="f-stats">
+      <h2>{!! __('Board info') !!}</h2>
+      <div class="clearfix">
+        <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>
+        </dl>
+        <dl class="left">
+          <dt>{!! __('User info') !!}</dt>
+@if(is_string($stats['newest_user']))
+          <dd>{!! __('Newest user')  !!} {{ $stats['newest_user'] }}</dd>
+@else
+          <dd>{!! __('Newest user')  !!} <a href="{!! $stats['newest_user'][0] !!}">{{ $stats['newest_user'][1] }}</a></dd>
+@endif
+@if($online)
+          <dd>{!! __('Users online') !!} <strong>{!! $online['number_of_users'] !!}</strong>, {!! __('Guests online') !!} <strong>{!! $online['number_of_guests'] !!}</strong>.</dd>
+          <dd>{!! __('Most online', $online['max'], $online['max_time']) !!}</dd>
+@endif
+        </dl>
+@if($online && $online['list'])
+        <dl class="f-inline f-onlinelist"><!--inline-->
+          <dt>{!! __('Online') !!}</dt>
+@foreach($online['list'] as $cur)
+@if(is_string($cur))
+          <dd>{{ $cur }}</dd>
+@else
+          <dd><a href="{!! $cur[0] !!}">{{ $cur[1] }}</a></dd>
+@endif
+@endforeach
+        </dl><!--endinline-->
+@endif
+      </div>
+    </section>

+ 24 - 0
app/templates/layouts/admin.tpl

@@ -0,0 +1,24 @@
+@extends('layouts/main')
+    <div class="f-main clearfix">
+      <aside class="f-admin-menu">
+@if(!empty($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)
+          <h2 class="f-menu-items">{!! __($aNameSub) !!}</h2>
+          <ul class="f-menu-items">
+@foreach($aNavigationSub as $key => $val)
+@if($key == $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>
+@endif
+@endforeach
+          </ul>
+@endforeach
+        </nav>
+@endif
+      </aside>
+@yield('content')
+    </div>

+ 22 - 0
app/templates/layouts/debug.tpl

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

+ 50 - 0
app/templates/layouts/iswev.tpl

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

+ 55 - 0
app/templates/layouts/main.tpl

@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>{{ $pageTitle }}</title>
+  <link rel="stylesheet" type="text/css" href="http://forkbb.local/public/style/ForkBB/style.css">
+@foreach($pageHeads as $v)
+  {!! $v !!}
+@endforeach
+</head>
+<body>
+  <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>
+      </div>
+@if(!empty($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)
+          <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>
+@endif
+@endforeach
+        </ul>
+      </nav>
+@endif
+    </header>
+@if($fAnnounce)
+    <section class="f-announce">
+      <h2>{!! __('Announcement') !!}</h2>
+      <p class="f-ancontent">{!! $fAnnounce !!}</p>
+    </section>
+@endif
+@if($fIswev)
+@include('layouts/iswev')
+@endif
+@yield('content')
+    <footer class="f-footer clearfix">
+      <div class="left">
+      </div>
+      <div class="right">
+        <p class="poweredby">{!! __('Powered by') !!}</p>
+      </div>
+    </footer>
+<!-- debuginfo -->
+  </div>
+</body>
+</html>

+ 5 - 0
app/templates/maintenance.tpl

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

+ 8 - 0
app/templates/message.tpl

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

+ 23 - 0
app/templates/redirect.tpl

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<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>
+  <link rel="stylesheet" type="text/css" href="http://forkbb.local/public/style/ForkBB/style.css">
+@foreach($pageHeads as $v)
+  {!! $v !!}
+@endforeach
+</head>
+<body>
+  <div class="f-wrap">
+    <section class="f-main f-redirect">
+      <h2>{!! __('Redirecting') !!}</h2>
+      <p>{!! $Message !!}</p>
+      <p><a href="{{ $Link }}">{!! __('Click redirect') !!}</a></p>
+    </section>
+<!-- debuginfo -->
+  </div>
+</body>
+</html>

+ 5 - 0
app/templates/rules.tpl

@@ -0,0 +1,5 @@
+@extends('layouts/main')
+    <section class="f-main f-rules">
+      <h2>{!! __('Forum rules') !!}</h2>
+      <div>{!! $Rules !!}</div>
+    </section>

+ 2 - 1
composer.json

@@ -19,6 +19,7 @@
     ],
     "require": {
         "php": ">=5.6.0",
-        "artoodetoo/container": "dev-visman"
+        "artoodetoo/container": "dev-visman",
+        "artoodetoo/dirk": "dev-master"
     }
 }

+ 54 - 7
composer.lock

@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "6f4e0ebde4d3b282bfe8097414a0e787",
-    "content-hash": "1356226a18a75fe9ec08328a2f553a59",
+    "hash": "66793e0ac7d0499d9403c353c1a60b7b",
+    "content-hash": "8160828b885585e30e73fe34aaca440c",
     "packages": [
         {
             "name": "artoodetoo/container",
@@ -13,12 +13,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/MioVisman/container.git",
-                "reference": "3ab08b97674dff417f36783c9e054c25540171d3"
+                "reference": "bf147763641e3b6362286d3401b3ee55b2c2760e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/MioVisman/container/zipball/3ab08b97674dff417f36783c9e054c25540171d3",
-                "reference": "3ab08b97674dff417f36783c9e054c25540171d3",
+                "url": "https://api.github.com/repos/MioVisman/container/zipball/bf147763641e3b6362286d3401b3ee55b2c2760e",
+                "reference": "bf147763641e3b6362286d3401b3ee55b2c2760e",
                 "shasum": ""
             },
             "require": {
@@ -46,14 +46,61 @@
             "support": {
                 "source": "https://github.com/MioVisman/container/tree/visman"
             },
-            "time": "2017-01-11 15:15:38"
+            "time": "2017-01-23 11:54:39"
+        },
+        {
+            "name": "artoodetoo/dirk",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/artoodetoo/dirk.git",
+                "reference": "e6c2099435d4d4a13d7e96d0170db6182797b5bd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/artoodetoo/dirk/zipball/e6c2099435d4d4a13d7e96d0170db6182797b5bd",
+                "reference": "e6c2099435d4d4a13d7e96d0170db6182797b5bd",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.0.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "R2\\Templating\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "artoodetoo",
+                    "email": "i.am@artoodetoo.org"
+                }
+            ],
+            "description": "PHP template system",
+            "keywords": [
+                "dirk",
+                "package",
+                "templates",
+                "templating",
+                "views"
+            ],
+            "time": "2017-01-10 21:38:22"
         }
     ],
     "packages-dev": [],
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": {
-        "artoodetoo/container": 20
+        "artoodetoo/container": 20,
+        "artoodetoo/dirk": 20
     },
     "prefer-stable": false,
     "prefer-lowest": false,

+ 71 - 1
db_update.php

@@ -655,7 +655,7 @@ switch ($stage)
         // For FluxBB by Visman 1.5.10.75
         if (! isset($pun_config['i_fork_revision']) || $pun_config['i_fork_revision'] < 1) {
             if (! isset($pun_config['i_fork_revision'])) {
-                $db->query('INSERT INTO '.$db->prefix.'config (conf_name, conf_value) VALUES (\'i_fork_revision\', \'0\')') or error('Unable to insert config value \'i_fork_revision\'', __FILE__, __LINE__, $db->error());
+                $db->query('INSERT INTO '.$db->prefix.'config (conf_name, conf_value) VALUES (\'i_fork_revision\', \'1\')') or error('Unable to insert config value \'i_fork_revision\'', __FILE__, __LINE__, $db->error());
                 $pun_config['i_fork_revision'] = 1;
             }
             if (! isset($pun_config['s_fork_version'])) {
@@ -668,6 +668,76 @@ switch ($stage)
             $db->query('DELETE FROM '.$db->prefix.'config WHERE conf_name=\'o_base_url\'') or error('Unable to delete config value \'o_base_url\'', __FILE__, __LINE__, $db->error());
 
             $db->alter_field('users', 'password', 'VARCHAR(255)', false, '') or error('Unable to alter password field', __FILE__, __LINE__, $db->error());
+            $db->add_field('user', 'u_mark_all_read', 'INT(10) UNSIGNED', true) or error('Unable to add u_mark_all_read field', __FILE__, __LINE__, $db->error());
+
+            $db->add_field('online', 'o_position', 'VARCHAR(100)', false, '') or error('Unable to add o_position field', __FILE__, __LINE__, $db->error());
+            $db->add_index('online', 'o_position_idx', array('o_position')) or error('Unable to add o_position_idx index', __FILE__, __LINE__, $db->error());
+            $db->add_field('online', 'o_name', 'VARCHAR(200)', false, '') or error('Unable to add o_name field', __FILE__, __LINE__, $db->error());
+
+            $schema = [
+                'FIELDS'  => [
+                    'uid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'fid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mf_upper' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mf_lower' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                ],
+                'UNIQUE KEYS' => [
+                    'uid_fid_idx'    => ['uid', 'fid'],
+                ],
+                'INDEXES' => [
+                    'mf_upper_idx'   => ['mf_upper'],
+                    'mf_lower_idx'   => ['mf_lower'],
+                ]
+            ];
+
+            $db->create_table('mark_of_forum', $schema) or error('Unable to create mark_of_forum table', __FILE__, __LINE__, $db->error());
+
+            $schema = [
+                'FIELDS'  => [
+                    'uid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'fid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'tid' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mt_upper' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                    'mt_lower' => [
+                        'datatype'   => 'INT(10) UNSIGNED',
+                        'allow_null' => true,
+                    ],
+                ],
+                'UNIQUE KEYS' => [
+                    'uid_fid_tid_idx' => ['uid', 'fid', 'tid'],
+                ],
+                'INDEXES' => [
+                    'mt_upper_idx'   => ['mt_upper'],
+                    'mt_lower_idx'   => ['mt_lower'],
+                ]
+            ];
+
+            $db->create_table('mark_of_topic', $schema) or error('Unable to create mark_of_topic table', __FILE__, __LINE__, $db->error());
+
 
         }
 		break;

+ 3 - 3
footer.php

@@ -90,8 +90,8 @@ echo "\t\t\t".'<div class="conl">'."\n";
 if ($pun_config['o_quickjump'] == '1')
 {
 	// Load cached quick jump
-	if (file_exists($container->getParameter('DIR_CACHE') . 'cache_quickjump_'.$pun_user['g_id'].'.php'))
-		include $container->getParameter('DIR_CACHE') . 'cache_quickjump_'.$pun_user['g_id'].'.php';
+	if (file_exists($container->getParameter('DIR_CACHE') . '/cache_quickjump_'.$pun_user['g_id'].'.php'))
+		include $container->getParameter('DIR_CACHE') . '/cache_quickjump_'.$pun_user['g_id'].'.php';
 
 	if (!defined('PUN_QJ_LOADED'))
 	{
@@ -99,7 +99,7 @@ if ($pun_config['o_quickjump'] == '1')
 			require PUN_ROOT.'include/cache.php';
 
 		generate_quickjump_cache($pun_user['g_id']);
-		require $container->getParameter('DIR_CACHE') . 'cache_quickjump_'.$pun_user['g_id'].'.php';
+		require $container->getParameter('DIR_CACHE') . '/cache_quickjump_'.$pun_user['g_id'].'.php';
 	}
 	$page_js['c'][] = 'document.getElementById("qjump").getElementsByTagName("div")[0].getElementsByTagName("input")[0].style.display = "none";'; // Visman - скрываем кнопку перехода при включенном js
 }

+ 8 - 8
include/cache.php

@@ -82,12 +82,12 @@ function generate_quickjump_cache($group_id = false)
 		if ($read_board == '1')
 		{
 			// Load cached subforums - Visman
-			if (file_exists($container->getParameter('DIR_CACHE') . 'cache_subforums_'.$group_id.'.php'))
-				include $container->getParameter('DIR_CACHE') . 'cache_subforums_'.$group_id.'.php';
+			if (file_exists($container->getParameter('DIR_CACHE') . '/cache_subforums_'.$group_id.'.php'))
+				include $container->getParameter('DIR_CACHE') . '/cache_subforums_'.$group_id.'.php';
 			else
 			{
 				generate_subforums_cache($group_id);
-				require $container->getParameter('DIR_CACHE') . 'cache_subforums_'.$group_id.'.php';
+				require $container->getParameter('DIR_CACHE') . '/cache_subforums_'.$group_id.'.php';
 			}
 
 			$output .= generate_quickjump_sf_list($sf_array_tree);
@@ -106,7 +106,7 @@ function fluxbb_write_cache_file($file, $content)
 {
     global $container;
 
-	$fh = @fopen($container->getParameter('DIR_CACHE') . $file, 'wb');
+	$fh = @fopen($container->getParameter('DIR_CACHE') . '/' . $file, 'wb');
 	if (!$fh)
 		error('Unable to write cache file '.pun_htmlspecialchars($file).' to cache directory. Please make sure PHP has write access to the directory \''.pun_htmlspecialchars($container->getParameter('DIR_CACHE')).'\'', __FILE__, __LINE__);
 
@@ -118,7 +118,7 @@ function fluxbb_write_cache_file($file, $content)
 	flock($fh, LOCK_UN);
 	fclose($fh);
 
-	fluxbb_invalidate_cached_file($container->getParameter('DIR_CACHE') . $file);
+	fluxbb_invalidate_cached_file($container->getParameter('DIR_CACHE') . '/' . $file);
 }
 
 
@@ -129,13 +129,13 @@ function clear_feed_cache()
 {
     global $container;
 
-	$d = dir($container->getParameter('DIR_CACHE'));
+	$d = dir($container->getParameter('DIR_CACHE') . '/');
 	while (($entry = $d->read()) !== false)
 	{
 		if (substr($entry, 0, 10) == 'cache_feed' && substr($entry, -4) == '.php')
 		{
-			@unlink($container->getParameter('DIR_CACHE') . $entry);
-			fluxbb_invalidate_cached_file($container->getParameter('DIR_CACHE') . $entry);
+			@unlink($container->getParameter('DIR_CACHE') . '/' . $entry);
+			fluxbb_invalidate_cached_file($container->getParameter('DIR_CACHE') . '/' . $entry);
 		}
 	}
 	$d->close();

+ 9 - 23
include/common.php

@@ -49,11 +49,6 @@ require PUN_ROOT.'include/addons.php';
 // Force POSIX locale (to prevent functions such as strtolower() from messing up UTF-8 strings)
 setlocale(LC_CTYPE, 'C');
 
-require PUN_ROOT . 'app/bootstrap.php';
-
-// The addon manager is responsible for storing the hook listeners and communicating with the addons
-$flux_addons = new flux_addon_manager();
-
 // Define a few commonly used constants
 define('PUN_UNVERIFIED', 0);
 define('PUN_ADMIN', 1);
@@ -61,6 +56,11 @@ define('PUN_MOD', 2);
 define('PUN_GUEST', 3);
 define('PUN_MEMBER', 4);
 
+require PUN_ROOT . 'app/bootstrap.php';
+
+// The addon manager is responsible for storing the hook listeners and communicating with the addons
+$flux_addons = new flux_addon_manager();
+
 $db = $container->get('DB');
 
 // Start a transaction
@@ -68,12 +68,6 @@ $db->start_transaction();
 
 $pun_config = $container->get('config');
 
-// Verify that we are running the proper database schema revision
-if (empty($pun_config['i_fork_revision']) || $pun_config['i_fork_revision'] < FORK_REVISION) {
-	header('Location: db_update.php');
-	exit;
-}
-
 // Enable output buffering
 if (!defined('PUN_DISABLE_BUFFERING'))
 {
@@ -89,8 +83,7 @@ $forum_time_formats = array($pun_config['o_time_format'], 'H:i:s', 'H:i', 'g:i:s
 $forum_date_formats = array($pun_config['o_date_format'], 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y');
 
 // Check/update/set cookie and fetch user info
-$pun_user = array();
-check_cookie($pun_user);
+$pun_user = $container->get('user');
 
 // Attempt to load the common language file
 if (file_exists(PUN_ROOT.'lang/'.$pun_user['language'].'/common.php'))
@@ -98,13 +91,6 @@ if (file_exists(PUN_ROOT.'lang/'.$pun_user['language'].'/common.php'))
 else
 	error('There is no valid language pack \''.pun_htmlspecialchars($pun_user['language']).'\' installed. Please reinstall a language of that name');
 
-// Check if we are to display a maintenance message
-if ($pun_config['o_maintenance'] && $pun_user['g_id'] > PUN_ADMIN && !defined('PUN_TURN_OFF_MAINT'))
-	maintenance_message();
-
-// Check if current user is banned
-check_bans();
-
 // Update online list
 $onl_u = $onl_g = $onl_s = array();
 if (!defined('WITT_ENABLE')) // Кто в этой теме - Visman
@@ -127,8 +113,8 @@ if (!defined('FORUM_MAX_COOKIE_SIZE'))
 	define('FORUM_MAX_COOKIE_SIZE', 4048);
 
 // Load cached subforums - Visman
-if (file_exists($container->getParameter('DIR_CACHE') . 'cache_subforums_'.$pun_user['g_id'].'.php'))
-	include $container->getParameter('DIR_CACHE') . 'cache_subforums_'.$pun_user['g_id'].'.php';
+if (file_exists($container->getParameter('DIR_CACHE') . '/cache_subforums_'.$pun_user['g_id'].'.php'))
+	include $container->getParameter('DIR_CACHE') . '/cache_subforums_'.$pun_user['g_id'].'.php';
 
 if (!isset($sf_array_tree))
 {
@@ -136,5 +122,5 @@ if (!isset($sf_array_tree))
 		require PUN_ROOT.'include/cache.php';
 
 	generate_subforums_cache($pun_user['g_id']);
-	require $container->getParameter('DIR_CACHE') . 'cache_subforums_'.$pun_user['g_id'].'.php';
+	require $container->getParameter('DIR_CACHE') . '/cache_subforums_'.$pun_user['g_id'].'.php';
 }

+ 58 - 71
include/functions.php

@@ -287,77 +287,6 @@ function set_default_user()
 }
 
 
-//
-// Check whether the connecting user is banned (and delete any expired bans while we're at it)
-//
-function check_bans()
-{
-	global $container, $pun_config, $lang_common, $pun_user;
-
-    $db = $container->get('DB');
-    $bans = $container->get('bans');
-
-	// Admins and moderators aren't affected
-	if ($pun_user['is_admmod'] || !$bans)
-		return;
-
-	// 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
-	$user_ip = get_remote_address();
-	$user_ip .= (strpos($user_ip, '.') !== false) ? '.' : ':';
-
-	$bans_altered = false;
-	$is_banned = false;
-
-	foreach ($bans as $cur_ban)
-	{
-		// Has this ban expired?
-		if ($cur_ban['expire'] != '' && $cur_ban['expire'] <= time())
-		{
-			$db->query('DELETE FROM '.$db->prefix.'bans WHERE id='.$cur_ban['id']) or error('Unable to delete expired ban', __FILE__, __LINE__, $db->error());
-			$bans_altered = true;
-			continue;
-		}
-
-		if ($cur_ban['username'] != '' && mb_strtolower($pun_user['username']) == mb_strtolower($cur_ban['username']))
-			$is_banned = true;
-
-		if ($cur_ban['ip'] != '')
-		{
-			$cur_ban_ips = explode(' ', $cur_ban['ip']);
-
-			$num_ips = count($cur_ban_ips);
-			for ($i = 0; $i < $num_ips; ++$i)
-			{
-				// Add the proper ending to the ban
-				if (strpos($user_ip, '.') !== false)
-					$cur_ban_ips[$i] = $cur_ban_ips[$i].'.';
-				else
-					$cur_ban_ips[$i] = $cur_ban_ips[$i].':';
-
-				if (substr($user_ip, 0, strlen($cur_ban_ips[$i])) == $cur_ban_ips[$i])
-				{
-					$is_banned = true;
-					break;
-				}
-			}
-		}
-
-		if ($is_banned)
-		{
-			$db->query('DELETE FROM '.$db->prefix.'online WHERE ident=\''.$db->escape($pun_user['username']).'\'') or error('Unable to delete from online list', __FILE__, __LINE__, $db->error());
-			message($lang_common['Ban message'].' '.(($cur_ban['expire'] != '') ? $lang_common['Ban message 2'].' '.strtolower(format_time($cur_ban['expire'], true)).'. ' : '').(($cur_ban['message'] != '') ? $lang_common['Ban message 3'].'<br /><br /><strong>'.pun_htmlspecialchars($cur_ban['message']).'</strong><br /><br />' : '<br /><br />').$lang_common['Ban message 4'].' <a href="mailto:'.pun_htmlspecialchars($pun_config['o_admin_email']).'">'.pun_htmlspecialchars($pun_config['o_admin_email']).'</a>.', true);
-		}
-	}
-
-	// If we removed any expired bans during our run-through, we need to regenerate the bans cache
-	if ($bans_altered)
-	{
-        $container->get('bans update');
-	}
-}
-
-
 //
 // Check username
 //
@@ -2100,3 +2029,61 @@ function sf_crumbs($id)
 
 	return $str;
 }
+
+/******************************************************************************/
+/*
+function __($data, ...$args)
+{
+    static $tr = [];
+    static $loaded = [];
+
+    if ($data instanceof \R2\DependencyInjection\ContainerInterface) {
+        if (isset($loaded[$args[0]])) {
+            return;
+        }
+        $dir = $data->getParameter('DIR_TRANSL');
+        $lang = isset($args[1]) ? $args[1] : $data->get('user')['language'];
+        if (file_exists($dir . $lang . '/' . $args[0] . '.php')) {
+            $tr = (include $dir . $lang . '/' . $args[0] . '.php') + $tr;
+        } elseif (file_exists($dir . 'English/' . $args[0] . '.php')) {
+            $tr = (include $dir . 'English/' . $args[0] . '.php') + $tr;
+        }
+        $loaded[$args[0]] = true;
+    } else {
+        if (! isset($tr[$data])) {
+            return $data;
+        } elseif (empty($args)) {
+            return $tr[$data];
+        } else {
+            return sprintf($tr[$data], ...$args);
+        }
+    }
+}
+*/
+function __($data, ...$args)
+{
+    static $lang;
+
+    if (empty($lang)) {
+        $lang = $data;
+        return;
+    }
+
+    $tr = $lang->get($data);
+
+    if (is_array($tr)) {
+        if (isset($args[0]) && is_numeric($args[0])) {
+            $n = array_shift($args);
+            eval('$n = (int) ' . $tr['plural']);
+            $tr = $tr[$n];
+        } else {
+            $tr = $tr[0];
+        }
+    }
+
+    if (empty($args)) {
+        return $tr;
+    } else {
+        return sprintf($tr, ...$args);
+    }
+}

BIN
public/style/ForkBB/font/flow-400-normal.ttf


BIN
public/style/ForkBB/font/flow-400-normal.woff


BIN
public/style/ForkBB/font/flow-400-normal.woff2


BIN
public/style/ForkBB/font/flow-700-normal.ttf


BIN
public/style/ForkBB/font/flow-700-normal.woff


BIN
public/style/ForkBB/font/flow-700-normal.woff2


+ 12 - 0
public/style/ForkBB/font/flow.css

@@ -0,0 +1,12 @@
+@font-face {
+    font-family: 'Flow Ext';
+    src: local('Flow Ext'), local('FlowExt'), url('flowext.woff2') format('woff2'), url('flowext.woff') format('woff'), url('flowext.ttf') format('truetype');
+    font-weight: 400;
+    font-style: normal;
+}
+@font-face {
+    font-family: 'Flow Ext';
+    src: local('Flow Ext Bold'), local('FlowExt-Bold'), url('flowextbold.woff2') format('woff2'), url('flowextbold.woff') format('woff'), url('flowextbold.ttf') format('truetype');
+    font-weight: 700;
+    font-style: normal;
+}

File diff suppressed because it is too large
+ 83 - 0
public/style/ForkBB/style.css


+ 2 - 2
register.php

@@ -75,12 +75,12 @@ if ($request->isPost('form_sent'))
 		message($lang_register['Registration flood']);
 
 
-	$username = trim($request->posStr('req_user'));
+	$username = trim($request->postStr('req_user'));
 	$email1 = strtolower(trim($request->postStr('req_email1')));
 
 	if ($pun_config['o_regs_verify'] == '1')
 	{
-		$email2 = strtolower(trim($request->posStr('req_email2')));
+		$email2 = strtolower(trim($request->postStr('req_email2')));
 
 		$password1 = $container->get('Secury')->randomPass(12);
 		$password2 = $password1;

+ 1 - 3
vendor/artoodetoo/container/src/Container.php

@@ -20,6 +20,7 @@ class Container implements ContainerInterface
         if (!empty($config)) {
             $this->config($config);
         }
+        $this->config['CONTAINER'] = $this;
     }
 
     public function config(array $config)
@@ -62,9 +63,6 @@ class Container implements ContainerInterface
             $service = $f(...$args);
         } else {
             $service = new $class(...$args);
-            if ($service instanceof ContainerAwareInterface) {
-                $service->setContainer($this);
-            }
         }
         if ($toShare) {
             $this->instances[$id] = $service;

+ 1 - 0
vendor/artoodetoo/dirk/.gitignore

@@ -0,0 +1 @@
+bootstrap.php

+ 22 - 0
vendor/artoodetoo/dirk/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 117 - 0
vendor/artoodetoo/dirk/README.md

@@ -0,0 +1,117 @@
+# Dirk PHP Templates
+
+Tiny and powerfull template engine with syntax almost the same as in laravel/blade.
+
+### Installation
+
+The package can be installed via Composer by requiring the "artoodetoo/dirk" package in your project's composer.json.
+
+```json
+{
+    "require": {
+        "artoodetoo/dirk": "dev-master"
+    }
+}
+```
+
+### Usage
+
+**/views/hello.dirk.html**
+```html
+@extends('layout/main')
+
+<h1>Hello {{{ $name }}}!<h1>
+
+{{ $timestamp or 'Timestamp not defined' }}
+
+@section('sidebar')
+
+  @foreach($list as $l)
+    <p>{{ $l }} @if($l == 3) is equal 3 ! @endif</p>
+  @endforeach
+
+@endsection
+```
+
+**/views/layout/main.dirk.html**
+```html
+<!DOCTYPE html>
+<html>
+<head>
+<title>Example</title>
+</head>
+<body>
+
+<sidebar>
+
+@yield('sidebar', 'Default sidebar text')
+
+</sidebar>
+
+@yield('content')
+
+</body>
+</html>
+```
+
+**/web/index.php**
+```php
+<?php
+
+require 'vendor/autoload.php';
+
+use R2\Templating\Dirk;
+
+$view = new Dirk([
+    'views' => __DIR__.'/views',
+    'cache' => __DIR__.'/cache'
+]);
+
+$name = '<artoodetoo>';
+$list = [1, 2, 3, 4, 5];
+
+$view->render('hello', compact('name', 'list'));
+```
+
+### Feature list
+
+Echoes and comments
+  * *{{ $var }}* - Echo. NOTE: it's escaped by default, like in Laravel 5!
+  * *{!! $var !!}* - Raw echo without escaping
+  * *{{ $var or 'default' }}* - Echo content with a default value
+  * *{{{ $var }}}* - Echo escaped content
+  * *{{-- Comment --}}* - A comment (in code, not in output)
+
+Conditionals
+  * *@if(condition)* - Starts an if block
+  * *@else*
+  * *@elseif(condition)*
+  * *@endif*
+  * *@unless(condition)* - Starts an unless block
+  * *@endunless*
+
+Loops
+  * *@foreach($list as $key => $val)* - Starts a foreach block
+  * *@endforeach*
+  * *@forelse($list as $key => $val)* - Starts a foreach with empty block
+  * *@empty*
+  * *@endforelse*
+  * *@for($i = 0; $i < 10; $i++)* - Starts a for block
+  * *@endfor*
+  * *@while(condition)* - Starts a while block
+  * *@endwhile*
+
+Inheritance and sections
+  * *@include(file)* - Includes another template
+  * *@extends('layout')* - Extends a template with a layout
+  * *@section('name')* - Starts a section
+  * *@endsection* - Ends section
+  * *@yield('section')* - Yields content of a section.
+  * *@show* - Ends section and yields its content
+  * *@stop* - Ends section
+  * *@append* - Ends section and appends it to existing of section of same name
+  * *@overwrite* - Ends section, overwriting previous section of same name
+
+### License
+
+The Dirk is open-source software, licensed under the [MIT license](http://opensource.org/licenses/MIT)

+ 28 - 0
vendor/artoodetoo/dirk/composer.json

@@ -0,0 +1,28 @@
+{
+    "name": "artoodetoo/dirk",
+    "description": "PHP template system",
+    "license": "MIT",
+    "keywords": ["dirk", "package", "templating", "templates", "views"],
+    "authors": [
+        {
+            "name": "artoodetoo",
+            "email": "i.am@artoodetoo.org"
+        }
+    ],
+    "require": {
+        "php": ">=5.4.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "4.0.*"
+    },
+    "autoload": {
+        "psr-4": {
+            "R2\\Templating\\": "src"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "R2\\Templating\\Tests": "tests"
+        }
+    }
+}

+ 17 - 0
vendor/artoodetoo/dirk/phpunit.xml.dist

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false"
+         backupStaticAttributes="false"
+         bootstrap="vendor/autoload.php"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         processIsolation="false"
+         stopOnFailure="false"
+         syntaxCheck="false">
+    <testsuites>
+        <testsuite name="Dirk Test Suite">
+            <directory suffix=".php">./tests/</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>

+ 418 - 0
vendor/artoodetoo/dirk/src/Dirk.php

@@ -0,0 +1,418 @@
+<?php
+
+namespace R2\Templating;
+
+use R2\Templating\PhpEngine;
+
+class Dirk extends PhpEngine
+{
+
+    protected $cache;
+    protected $echoFormat;
+
+    public function __construct(array $config = [])
+    {
+        $config = array_replace_recursive(
+            [
+                'ext'   => '.blade.php',
+                'cache' => '.',
+                'echo'  => 'htmlspecialchars(%s, ENT_QUOTES, \'UTF-8\')',
+            ],
+            $config
+        );
+        $this->cache      = isset($config['cache']) ? $config['cache'] : '.';
+        $this->echoFormat = isset($config['echo'])  ? $config['echo']  : '%s';
+        parent::__construct($config);
+    }
+
+    protected $compilers = array(
+        'Statements',
+        'Comments',
+        'Echos'
+    );
+
+    /**
+     * Prepare file to include
+     * @param  string $name
+     * @return string
+     */
+    protected function prepare($name)
+    {
+        $name = str_replace('.', '/', $name);
+        $tpl = $this->views . '/' . $name . $this->ext;
+        $php = $this->cache . '/' . md5($name) . '.php';
+        if (!file_exists($php) || filemtime($tpl) > filemtime($php)) {
+            $text = file_get_contents($tpl);
+            foreach ($this->compilers as $type) {
+                $text = $this->{'compile' . $type}($text);
+            }
+            file_put_contents($php, $text);
+        }
+        return $php;
+    }
+
+    /**
+     * Compile Statements that start with "@"
+     *
+     * @param  string  $value
+     * @return mixed
+     */
+    protected function compileStatements($value)
+    {
+        return preg_replace_callback(
+            '/\B@(\w+)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x',
+            function($match) {
+                if (method_exists($this, $method = 'compile' . ucfirst($match[1]))) {
+                    $match[0] = $this->$method(isset($match[3]) ? $match[3] : '');
+                }
+                return isset($match[3]) ? $match[0] : $match[0] . $match[2];
+            },
+            $value
+        );
+    }
+
+    /**
+     * Compile comments
+     *
+     * @param  string  $value
+     * @return string
+     */
+    protected function compileComments($value)
+    {
+        $pattern = '/\{\{--((.|\s)*?)--\}\}/';
+
+        return preg_replace($pattern, '<?php /*$1*/ ?>', $value);
+    }
+
+    /**
+     * Compile echos
+     *
+     * @param  string  $value
+     * @return string
+     */
+    protected function compileEchos($value)
+    {
+        // compile escaped echoes
+        $value = preg_replace_callback(
+            '/\{\{\{\s*(.+?)\s*\}\}\}(\r?\n)?/s',
+            function($matches) {
+                $whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
+                return '<?= htmlspecialchars('
+                    .$this->compileEchoDefaults($matches[1])
+                    .', ENT_QUOTES, \'UTF-8\') ?>'
+                    .$whitespace;
+            },
+            $value
+        );
+
+        // compile not escaped echoes
+        $value = preg_replace_callback(
+            '/\{\!!\s*(.+?)\s*!!\}(\r?\n)?/s',
+            function($matches) {
+                $whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
+                return '<?= '.$this->compileEchoDefaults($matches[1]).' ?>'.$whitespace;
+            },
+            $value
+        );
+
+        // compile regular echoes
+        $value = preg_replace_callback(
+            '/(@)?\{\{\s*(.+?)\s*\}\}(\r?\n)?/s',
+            function($matches) {
+                $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
+                return $matches[1]
+                    ? substr($matches[0], 1)
+                    : '<?= '
+                      .sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2]))
+                      .' ?>'.$whitespace;
+            },
+            $value
+        );
+        return $value;
+    }
+
+    /**
+     * Compile the default values for the echo statement.
+     *
+     * @param  string  $value
+     * @return string
+     */
+    public function compileEchoDefaults($value)
+    {
+        return preg_replace('/^(?=\$)(.+?)(?:\s+or\s+)(.+?)$/s', 'isset($1) ? $1 : $2', $value);
+    }
+
+    /**
+     * Compile the if statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileIf($expression)
+    {
+        return "<?php if{$expression}: ?>";
+    }
+
+    /**
+     * Compile the else-if statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileElseif($expression)
+    {
+        return "<?php elseif{$expression}: ?>";
+    }
+
+    /**
+     * Compile the else statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileElse($expression)
+    {
+        return "<?php else: ?>";
+    }
+
+    /**
+     * Compile the end-if statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileEndif($expression)
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * Compile the unless statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileUnless($expression)
+    {
+        return "<?php if(!$expression): ?>";
+    }
+
+    /**
+     * Compile the end unless statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileEndunless($expression)
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * Compile the for statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileFor($expression)
+    {
+        return "<?php for{$expression}: ?>";
+    }
+
+    /**
+     * Compile the end-for statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileEndfor($expression)
+    {
+        return "<?php endfor; ?>";
+    }
+
+    /**
+     * Compile the foreach statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileForeach($expression)
+    {
+        return "<?php foreach{$expression}: ?>";
+    }
+
+    /**
+     * Compile the end-for-each statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileEndforeach($expression)
+    {
+        return "<?php endforeach; ?>";
+    }
+
+    protected $emptyCounter = 0;
+    /**
+     * Compile the forelse statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileForelse($expression)
+    {
+        $this->emptyCounter++;
+        return "<?php \$__empty_{$this->emptyCounter} = true; "
+              ."foreach{$expression}: "
+              ."\$__empty_{$this->emptyCounter} = false;?>";
+    }
+
+    /**
+     * Compile the end-forelse statements
+     *
+     * @return string
+     */
+    protected function compileEmpty()
+    {
+        $s = "<?php endforeach; if (\$__empty_{$this->emptyCounter}): ?>";
+        $this->emptyCounter--;
+        return $s;
+    }
+
+    /**
+     * Compile the end-forelse statements
+     *
+     * @return string
+     */
+    protected function compileEndforelse()
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * Compile the while statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileWhile($expression)
+    {
+        return "<?php while{$expression}: ?>";
+    }
+
+    /**
+     * Compile the end-while statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileEndwhile($expression)
+    {
+        return "<?php endwhile; ?>";
+    }
+
+    /**
+     * Compile the extends statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileExtends($expression)
+    {
+        if (isset($expression{0}) && $expression{0} == '(') {
+            $expression = substr($expression, 1, -1);
+        }
+        return "<?php \$this->extend({$expression}) ?>";
+    }
+
+    /**
+     * Compile the include statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileInclude($expression)
+    {
+        if (isset($expression{0}) && $expression{0} == '(') {
+            $expression = substr($expression, 1, -1);
+        }
+        return "<?php include \$this->prepare({$expression}) ?>";
+    }
+
+    /**
+     * Compile the yield statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileYield($expression)
+    {
+        return "<?= \$this->block{$expression} ?>";
+    }
+
+    /**
+     * Compile the section statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileSection($expression)
+    {
+        return "<?php \$this->beginBlock{$expression} ?>";
+    }
+
+    /**
+     * Compile the end-section statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileEndsection($expression)
+    {
+        return "<?php \$this->endBlock() ?>";
+    }
+
+    /**
+     * Compile the show statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileShow($expression)
+    {
+        return "<?= \$this->block(\$this->endBlock()) ?>";
+    }
+
+    /**
+     * Compile the append statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileAppend($expression)
+    {
+        return "<?php \$this->endBlock() ?>";
+    }
+
+    /**
+     * Compile the stop statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileStop($expression)
+    {
+        return "<?php \$this->endBlock() ?>";
+    }
+
+    /**
+     * Compile the overwrite statements
+     *
+     * @param  string  $expression
+     * @return string
+     */
+    protected function compileOverwrite($expression)
+    {
+        return "<?php \$this->endBlock(true) ?>";
+    }
+}

+ 156 - 0
vendor/artoodetoo/dirk/src/PhpEngine.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace R2\Templating;
+
+class PhpEngine
+{
+    protected $composers;
+    protected $views;
+    protected $ext;
+    protected $separator;
+    protected $blocks;
+    protected $blockStack;
+
+    /**
+     * Constructor
+     * @param array $config
+     */
+    public function __construct(array $config = [], array $composers = [])
+    {
+        $this->views     = isset($config['views'])     ? $config['views']     : '.';
+        $this->ext       = isset($config['ext'])       ? $config['ext']       : '.php';
+        $this->separator = isset($config['separator']) ? $config['separator'] : '/';
+        $this->blocks = [];
+        $this->blockStack = [];
+        $this->composers = [];
+        foreach ($composers as $name => $composer) {
+            $this->composer($name, $composer);
+        }
+    }
+
+    /**
+     * Add view composer
+     * @param mixed $name     template name or array of names
+     * @param mixed $composer data in the same meaning as for fetch() call, or callable returning such data
+     */
+    public function composer($name, $composer)
+    {
+        if (is_array($name)) {
+            foreach ($name as $n) {
+                $this->composer($n, $composer);
+            }
+        } else {
+            $p = '~^'.str_replace('\*', '[^'.$this->separator.']+', preg_quote($name, $this->separator.'~')).'$~';
+            $this->composers[$p][] = $composer;
+        }
+    }
+
+    /**
+     * Prepare file to include
+     * @param  string $name
+     * @return string
+     */
+    protected function prepare($name)
+    {
+        if ($this->separator !== '/') {
+            $name = str_replace($this->separator, '/', $name);
+        }
+        return $this->views.'/'.$name.$this->ext;
+    }
+
+
+    /**
+     * Print result of templating
+     * @param string $name
+     * @param array  $data
+     */
+    public function render($name, array $data = [])
+    {
+        echo $this->fetch($name, $data);
+    }
+
+    /**
+     * Return result of templating
+     * @param  string $name
+     * @param  array  $data
+     * @return string
+     */
+    public function fetch($name, array $data = [])
+    {
+        $this->templates[] = $name;
+        if (!empty($data)) {
+            extract($data);
+        }
+        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');
+    }
+
+    /**
+     * Is template file exists?
+     * @param  string  $name
+     * @return Boolean
+     */
+    public function exists($name)
+    {
+        return file_exists($this->prepare($name));
+    }
+
+    /**
+     * Define parent
+     * @param string $name
+     */
+    protected function extend($name)
+    {
+        $this->templates[] = $name;
+    }
+
+    /**
+     * Return content of block if exists
+     * @param  string $name
+     * @param  string $default
+     * @return string
+     */
+    protected function block($name, $default = '')
+    {
+        return array_key_exists($name, $this->blocks)
+            ? $this->blocks[$name]
+            : $default;
+    }
+
+    /**
+     * Block begins
+     * @param string $name
+     */
+    protected function beginBlock($name)
+    {
+        array_push($this->blockStack, $name);
+        ob_start();
+    }
+
+    /**
+     * Block ends
+     * @param boolean $overwrite
+     * @return string
+     */
+    protected function endBlock($overwrite = false)
+    {
+        $name = array_pop($this->blockStack);
+        if ($overwrite || !array_key_exists($name, $this->blocks)) {
+            $this->blocks[$name] = ob_get_clean();
+        } else {
+            $this->blocks[$name] .= ob_get_clean();
+        }
+        return $name;
+    }
+}

+ 89 - 0
vendor/artoodetoo/dirk/tests/DirkTest.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace R2\Templating\Tests;
+
+use R2\Templating\Dirk;
+
+class DirkTest extends \PHPUnit_Framework_TestCase
+{
+    /** @var PhpEngine */
+    protected $engine;
+    /** @var string */
+    protected $views;
+    /** @var string */
+    protected $cache;
+    /** @var string */
+    protected $ext;
+    /** @var string[] */
+    protected $templates;
+
+    protected function setUp()
+    {
+        $this->views  = sys_get_temp_dir();
+        $this->cache  = $this->views;
+        $this->ext    = '.dirk.html';
+        $this->engine = new Dirk(
+            [
+                'views' => $this->views,
+                'cache' => $this->cache,
+                'ext'   => $this->ext,
+            ]
+        );
+        $this->templates = [];
+    }
+
+    protected function tearDown()
+    {
+        foreach ($this->templates as $name) {
+            $src = $name.$this->ext;
+            $dst = md5($name).'.php';
+            unlink($this->views.'/'.$src);
+            unlink($this->views.'/'.$dst);
+        }
+        unset($this->engine, $this->templates);
+    }
+
+    protected function template($text)
+    {
+        $name = \md5(\uniqid());
+        file_put_contents($this->views.'/'.$name.$this->ext, $text);
+        $this->templates[] = $name;
+        return $name;
+    }
+
+    /**
+     * @covers R2\Templating\Dirk::render
+     */
+    public function testRender1()
+    {
+        $name1 = $this->template('Well done, {{ $grade }}!');
+        $this->engine->render($name1, ['grade' => '<b>captain</b>']);
+        $this->expectOutputString('Well done, &lt;b&gt;captain&lt;/b&gt;!');
+    }
+
+    /**
+     * @covers R2\Templating\Dirk::render
+     */
+    public function testRender2()
+    {
+        $name2 = $this->template('Well done, {!! $grade !!}!');
+        $this->engine->render($name2, ['grade' => '<b>captain</b>']);
+        $this->expectOutputString('Well done, <b>captain</b>!');
+    }
+
+    /**
+     * @covers R2\Templating\Dirk::fetch
+     */
+    public function testFetch()
+    {
+        $parentName = $this->template(
+            'The text is `@yield(\'content\')`. '.
+            '@foreach([1,2,3] as $i)'.
+            '{!! $i !!}'.
+            '@endforeach'
+        );
+        $name = $this->template("@extends('{$parentName}')-xxx-");
+        $result = $this->engine->fetch($name, []);
+        $this->assertEquals("The text is `-xxx-`. 123", $result);
+    }
+}

+ 77 - 0
vendor/artoodetoo/dirk/tests/PhpEngineTest.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace R2\Templating\Tests;
+
+use R2\Templating\PhpEngine;
+
+class PhpEngineTest extends \PHPUnit_Framework_TestCase
+{
+    /** @var PhpEngine */
+    protected $engine;
+    /** @var string */
+    protected $views;
+    /** @var string */
+    protected $ext;
+    /** @var string[] */
+    protected $templates;
+
+    protected function setUp()
+    {
+        $this->views  = sys_get_temp_dir();
+        $this->ext    = '.php';
+        $this->engine = new PhpEngine(
+            [
+                'views' => $this->views,
+                'ext'   => $this->ext,
+            ]
+        );
+        $this->templates = [];
+    }
+
+    protected function tearDown()
+    {
+        foreach ($this->templates as $name) {
+            unlink($this->views.'/'.$name.$this->ext);
+        }
+        unset($this->engine, $this->templates);
+    }
+
+    protected function template($text)
+    {
+        $name = \md5(\uniqid());
+        file_put_contents($this->views.'/'.$name.$this->ext, $text);
+        $this->templates[] = $name;
+        return $name;
+    }
+
+    /**
+     * @covers R2\Templating\PhpEngine::render
+     */
+    public function testRender()
+    {
+        $name = $this->template('Well done, <?= $grade ?>!');
+        $this->engine->render($name, ['grade' => 'captain']);
+        $this->expectOutputString('Well done, captain!');
+    }
+
+    /**
+     * @covers R2\Templating\PhpEngine::fetch
+     */
+    public function testFetch()
+    {
+        $parentName = $this->template('The text is (<?= $this->block(\'content\') ?>).');
+        $name = $this->template('<?php $this->extend(\''.$parentName.'\'); ?>xxx');
+        $result = $this->engine->fetch($name, []);
+        $this->assertEquals('The text is (xxx).', $result);
+    }
+
+    /**
+     * @covers R2\Templating\PhpEngine::exists
+     */
+    public function testExists()
+    {
+        $name = $this->template('dummy');
+        $this->assertTrue($this->engine->exists($name));
+        $this->assertFalse($this->engine->exists('some-weird-name'));
+    }
+}

+ 1 - 0
vendor/composer/autoload_psr4.php

@@ -6,5 +6,6 @@ $vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);
 
 return array(
+    'R2\\Templating\\' => array($vendorDir . '/artoodetoo/dirk/src'),
     'R2\\DependencyInjection\\' => array($vendorDir . '/artoodetoo/container/src'),
 );

+ 5 - 0
vendor/composer/autoload_static.php

@@ -9,11 +9,16 @@ class ComposerStaticInit90ad93c7251d4f60daa9e545879c49e7
     public static $prefixLengthsPsr4 = array (
         'R' => 
         array (
+            'R2\\Templating\\' => 14,
             'R2\\DependencyInjection\\' => 23,
         ),
     );
 
     public static $prefixDirsPsr4 = array (
+        'R2\\Templating\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/artoodetoo/dirk/src',
+        ),
         'R2\\DependencyInjection\\' => 
         array (
             0 => __DIR__ . '/..' . '/artoodetoo/container/src',

+ 52 - 4
vendor/composer/installed.json

@@ -1,4 +1,52 @@
 [
+    {
+        "name": "artoodetoo/dirk",
+        "version": "dev-master",
+        "version_normalized": "9999999-dev",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/artoodetoo/dirk.git",
+            "reference": "e6c2099435d4d4a13d7e96d0170db6182797b5bd"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/artoodetoo/dirk/zipball/e6c2099435d4d4a13d7e96d0170db6182797b5bd",
+            "reference": "e6c2099435d4d4a13d7e96d0170db6182797b5bd",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.4.0"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "4.0.*"
+        },
+        "time": "2017-01-10 21:38:22",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "R2\\Templating\\": "src"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "artoodetoo",
+                "email": "i.am@artoodetoo.org"
+            }
+        ],
+        "description": "PHP template system",
+        "keywords": [
+            "dirk",
+            "package",
+            "templates",
+            "templating",
+            "views"
+        ]
+    },
     {
         "name": "artoodetoo/container",
         "version": "dev-visman",
@@ -6,18 +54,18 @@
         "source": {
             "type": "git",
             "url": "https://github.com/MioVisman/container.git",
-            "reference": "3ab08b97674dff417f36783c9e054c25540171d3"
+            "reference": "bf147763641e3b6362286d3401b3ee55b2c2760e"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/MioVisman/container/zipball/3ab08b97674dff417f36783c9e054c25540171d3",
-            "reference": "3ab08b97674dff417f36783c9e054c25540171d3",
+            "url": "https://api.github.com/repos/MioVisman/container/zipball/bf147763641e3b6362286d3401b3ee55b2c2760e",
+            "reference": "bf147763641e3b6362286d3401b3ee55b2c2760e",
             "shasum": ""
         },
         "require": {
             "php": ">=5.6"
         },
-        "time": "2017-01-11 15:15:38",
+        "time": "2017-01-23 11:54:39",
         "type": "library",
         "installation-source": "dist",
         "autoload": {

Some files were not shown because too many files changed in this diff