Visman 7 lat temu
rodzic
commit
7e91738c70
40 zmienionych plików z 1416 dodań i 1424 usunięć
  1. 1 1
      app/Controllers/Install.php
  2. 163 0
      app/Core/FuncAll.php
  3. 0 148
      app/Models/Actions/CacheGenerator.php
  4. 0 83
      app/Models/Actions/CacheLoader.php
  5. 0 104
      app/Models/Actions/CacheStopwords.php
  6. 28 19
      app/Models/DataModel.php
  7. 177 21
      app/Models/Forum.php
  8. 18 22
      app/Models/ForumList.php
  9. 0 93
      app/Models/ForumList/Load.php
  10. 10 11
      app/Models/ForumList/LoadTree.php
  11. 97 0
      app/Models/ForumList/Refresh.php
  12. 39 9
      app/Models/Model.php
  13. 64 73
      app/Models/Online.php
  14. 56 0
      app/Models/Online/Info.php
  15. 6 10
      app/Models/Page.php
  16. 1 1
      app/Models/Pages/Admin/Statistics.php
  17. 18 68
      app/Models/Pages/CrumbTrait.php
  18. 15 122
      app/Models/Pages/Forum.php
  19. 0 102
      app/Models/Pages/ForumsTrait.php
  20. 19 18
      app/Models/Pages/Index.php
  21. 2 2
      app/Models/Pages/Install.php
  22. 0 62
      app/Models/Pages/OnlineTrait.php
  23. 90 265
      app/Models/Pages/Topic.php
  24. 0 61
      app/Models/Pages/UsersTrait.php
  25. 80 0
      app/Models/Post.php
  26. 2 3
      app/Models/Stats.php
  27. 274 0
      app/Models/Topic.php
  28. 93 0
      app/Models/Topic/Load.php
  29. 50 11
      app/Models/User.php
  30. 4 4
      app/Models/User/LoadUserFromCookie.php
  31. 2 2
      app/bootstrap.php
  32. 1 1
      app/lang/English/admin_index.po
  33. 1 1
      app/lang/Russian/admin_groups.po
  34. 1 1
      app/lang/Russian/admin_index.po
  35. 29 35
      app/templates/forum.tpl
  36. 3 7
      app/templates/index.tpl
  37. 13 11
      app/templates/layouts/stats.tpl
  38. 26 26
      app/templates/layouts/subforums.tpl
  39. 32 26
      app/templates/topic.tpl
  40. 1 1
      composer.json

+ 1 - 1
app/Controllers/Install.php

@@ -41,7 +41,7 @@ class Install
             . substr($uri, 0, (int) strrpos($uri, '/'));
 
         $this->c->Lang->load('common', $this->c->config->o_default_lang);
-        $this->c->user = new User(['id' => 2, 'group_id' => $this->c->GROUP_ADMIN], $this->c);
+        $this->c->user = $this->ModelUser->setAttrs(['id' => 2, 'group_id' => $this->c->GROUP_ADMIN]);
 
         $r = $this->c->Router;
         $r->add('GET', '/install', 'Install:install', 'Install');

+ 163 - 0
app/Core/FuncAll.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace ForkBB\Core;
+
+use ForkBB\Models\Model;
+
+class FuncAll
+{
+    /**
+     * Контейнер
+     * @var Container
+     */
+    protected $c;
+
+    /**
+     * Модель вызова
+     * @var Model
+     */
+    protected $model;
+
+    /**
+     * Метод вызова
+     * @var string
+     */
+    protected $method;
+
+    /**
+     * Аргументы вызова
+     * @var array
+     */
+    protected $args;
+
+    /**
+     * Конструктор
+     *
+     * @param Container $container
+     */
+    public function __construct(Container $container)
+    {
+        $this->c = $container;
+    }
+
+    /**
+     * Настройка вызова
+     * 
+     * @param string $method
+     * @param Model $model
+     * @param array ...$args
+     * 
+     * @return FuncAll
+     */
+    public function setModel($method, Model $model, ...$args) 
+    {
+        $this->model  = $model;
+        $this->method = $method;
+        $this->args   = $args;
+        return $this;
+    }
+
+    /**
+     * Обработака вызова
+     * 
+     * @param string $name
+     * 
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        $data = $this->model->{$name};
+
+        return $this->{$this->method}($data, ...$this->args);
+    }
+
+    /**
+     * Цензура
+     * 
+     * @param string $str
+     * 
+     * @return string
+     */
+    public function cens($str)
+    {
+        return $this->c->censorship->censor($str);
+    }
+
+    /**
+     * Возвращает число в формате языка текущего пользователя
+     *
+     * @param mixed $number
+     * @param int $decimals
+     *
+     * @return string
+     */
+    protected function num($number, $decimals = 0)
+    {
+        return is_numeric($number)
+            ? number_format($number, $decimals, __('lang_decimal_point'), __('lang_thousands_sep'))
+            : 'not a number';
+    }
+
+    /**
+     * Возвращает даты/время в формате текущего пользователя
+     *
+     * @param int $timestamp
+     * @param bool $dateOnly
+     * @param string $dateFormat
+     * @param string $timeFormat
+     * @param bool $timeOnly
+     * @param bool $noText
+     *
+     * @return string
+     */
+    protected function dt($timestamp, $dateOnly = false, $dateFormat = null, $timeFormat = null, $timeOnly = false, $noText = false)
+    {
+        if (empty($timestamp)) {
+            return __('Never');
+        }
+
+        $user = $this->c->user;
+
+        $diff = ($user->timezone + $user->dst) * 3600;
+        $timestamp += $diff;
+
+        if (null === $dateFormat) {
+            $dateFormat = $this->c->DATE_FORMATS[$user->date_format];
+        }
+        if(null === $timeFormat) {
+            $timeFormat = $this->c->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);
+        }
+    }
+
+    /**
+     * Преобразует timestamp в YYYY-MM-DDTHH:mm:ss.sssZ
+     * 
+     * @param int $timestamp
+     * 
+     * @return string
+     */
+    public function utc($timestamp)
+    {
+        return gmdate('Y-m-d\TH:i:s\Z', $timestamp);
+    }
+}

+ 0 - 148
app/Models/Actions/CacheGenerator.php

@@ -1,148 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Actions;
-
-use ForkBB\Core\Container;
-use ForkBB\Models\User;
-
-class CacheGenerator
-{
-    /**
-     * Контейнер
-     * @var Container
-     */
-    protected $c;
-
-    /**
-     * Конструктор
-     * @param Container $container
-     */
-    public function __construct(Container $container)
-    {
-        $this->c = $container;
-    }
-
-    /**
-     * Возвращает массив конфигурации форума
-     * @return array
-     */
-    public function config()
-    {
-        return $this->c->DB->query('SELECT conf_name, conf_value FROM ::config')->fetchAll(\PDO::FETCH_KEY_PAIR);
-    }
-
-    /**
-     * Возвращает массив банов
-     * @return array
-     */
-    public function bans()
-    {
-        return $this->c->DB->query('SELECT id, username, ip, email, message, expire FROM ::bans')->fetchAll();
-    }
-
-    /**
-     * Возвращает массив слов попадающий под цензуру
-     * @return array
-     */
-    public function censoring()
-    {
-        $stmt = $this->c->DB->query('SELECT search_for, replace_with FROM ::censoring');
-        $search = $replace = [];
-        while ($row = $stmt->fetch()) {
-            $replace[] = $row['replace_with'];
-            $search[] = '%(?<![\p{L}\p{N}])('.str_replace('\*', '[\p{L}\p{N}]*?', preg_quote($row['search_for'], '%')).')(?![\p{L}\p{N}])%iu';
-        }
-        return [$search, $replace];
-    }
-
-    /**
-     * Возвращает информацию о последнем зарегистрированном пользователе и
-     * общем числе пользователей
-     * @return array
-     */
-    public function usersInfo()
-    {
-        $stats = [];
-        $stats['total_users'] = $this->c->DB->query('SELECT COUNT(id)-1 FROM ::users WHERE group_id!=?i', [$this->c->GROUP_UNVERIFIED])->fetchColumn();
-        $stats['last_user'] = $this->c->DB->query('SELECT id, username FROM ::users WHERE group_id!=?i ORDER BY registered DESC LIMIT 1', [$this->c->GROUP_UNVERIFIED])->fetch();
-        return $stats;
-    }
-
-    /**
-     * Возвращает спимок id админов
-     * @return array
-     */
-    public function admins()
-    {
-        return $this->c->DB->query('SELECT id FROM ::users WHERE group_id=?i', [$this->c->GROUP_ADMIN])->fetchAll(\PDO::FETCH_COLUMN);
-    }
-
-    /**
-     * Возвращает массив с описанием смайлов
-     * @return array
-     */
-    public function smilies()
-    {
-        return $this->c->DB->query('SELECT text, image FROM ::smilies ORDER BY disp_position')->fetchAll(\PDO::FETCH_KEY_PAIR); //???? text уникальное?
-    }
-
-    /**
-     * Возвращает массив карты базы данных
-     */
-    public function dbMap()
-    {
-        return $this->c->DB->getMap();
-    }
-
-    /**
-     * Возвращает массив с описанием форумов для текущего пользователя
-     * @return array
-     */
-    public function forums(User $user)
-    {
-        $stmt = $this->c->DB->query('SELECT g_read_board FROM ::groups WHERE g_id=?i:id', [':id' => $user->group_id]);
-        $read = $stmt->fetchColumn();
-        $stmt->closeCursor();
-
-        $tree = $desc = $asc = [];
-
-        if ($read) {
-            $stmt = $this->c->DB->query('SELECT c.id AS cid, c.cat_name, f.id AS fid, f.forum_name, f.redirect_url, f.parent_forum_id, f.disp_position, fp.post_topics, fp.post_replies FROM ::categories AS c INNER JOIN ::forums AS f ON c.id=f.cat_id LEFT JOIN ::forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id=?i:gid) WHERE fp.read_forum IS NULL OR fp.read_forum=1 ORDER BY c.disp_position, c.id, f.disp_position', [':gid' => $user->group_id]);
-            while ($f = $stmt->fetch()) {
-                $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);
-        }
-    }
-}

+ 0 - 83
app/Models/Actions/CacheLoader.php

@@ -1,83 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Actions;
-
-use ForkBB\Core\Cache;
-use ForkBB\Core\Container;
-use InvalidArgumentException;
-
-class CacheLoader
-{
-    /**
-     * Контейнер
-     * @var Container
-     */
-    protected $c;
-
-    /**
-     * @var ForkBB\Core\Cache
-     */
-    protected $cache;
-
-    /**
-     * Конструктор
-     * @param Cache $cache
-     * @param Container $container
-     */
-    public function __construct(Cache $cache, Container $container)
-    {
-        $this->cache = $cache;
-        $this->c = $container;
-    }
-
-    /**
-     * Загрузка данных из кэша (генерация кэша при отсутствии или по требованию)
-     * @param string $key
-     * @param bool $update
-     * @return mixed
-     * @throws \InvalidArgumentException
-     */
-    public function load($key, $update = false)
-    {
-        if (preg_match('%\p{Lu}%u', $key)) {
-            throw new InvalidArgumentException('The key must not contain uppercase letters');
-        }
-        if (! $update && $this->cache->has($key)) {
-            return $this->cache->get($key);
-        } else {
-            $value = $this->c->{'get ' . $key};
-            $this->cache->set($key, $value);
-            if ($update) {
-                $this->c->$key = $value;
-            }
-            return $value;
-        }
-    }
-
-    /**
-     * Загрузка данных по разделам из кэша (генерация кэша при условии)
-     * @return array
-     */
-    public function loadForums()
-    {
-        $mark = $this->cache->get('forums_mark');
-        $key = 'forums_' . $this->c->user->group_id;
-
-        if (empty($mark)) {
-            $this->cache->set('forums_mark', time());
-            $value = $this->c->{'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 forums'};
-            $this->cache->set($key, [time(), $value]);
-            return $value;
-        }
-
-        return $result[1];
-    }
-}

+ 0 - 104
app/Models/Actions/CacheStopwords.php

@@ -1,104 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Actions;
-
-use ForkBB\Core\Cache;
-
-class CacheStopwords
-{
-    /**
-     * @var ForkBB\Core\Cache
-     */
-    protected $cache;
-
-    /**
-     * @var array
-     */
-    protected $files;
-
-    /**
-     * @var string
-     */
-    protected $id;
-
-    /**
-     * Конструктор
-     *
-     * @param ForkBB\Core\Cache $cache
-     */
-    public function __construct(Cache $cache)
-    {
-        $this->cache = $cache;
-    }
-
-    /**
-     * Возвращает массив слов, которые не участвуют в поиске
-     *
-     * @return array
-     */
-    public function load()
-    {
-        $arr = $this->cache->get('stopwords');
-        if (isset($arr['id'])
-            && isset($arr['stopwords'])
-            && $arr['id'] === $this->generateId()
-        ) {
-            return $arr['stopwords'];
-        } else {
-            return $this->regeneration();
-        }
-    }
-
-    /**
-     * Генерация id кэша на основе найденных файлов stopwords.txt
-     *
-     * @return string
-     */
-    protected function generateId()
-    {
-        if (! empty($this->id)) {
-            return $this->id;
-        }
-
-        $files = glob(PUN_ROOT . 'lang/*/stopwords.txt');
-        if ($files === false) {
-            return 'cache_id_error';
-        }
-
-        $this->files = $files;
-        $hash = [];
-
-        foreach ($files as $file) {
-            $hash[] = $file;
-            $hash[] = filemtime($file);
-        }
-
-        return $this->id = sha1(implode('|', $hash));
-    }
-
-    /**
-     * Регенерация кэша массива слов с возвращением результата
-     *
-     * @return array
-     */
-    protected function regeneration()
-    {
-        $id = $this->generateId();
-
-        if (! is_array($this->files)) {
-            return [];
-        }
-
-        $stopwords = [];
-        foreach ($this->files as $file) {
-            $stopwords = array_merge($stopwords, file($file));
-        }
-
-        // Tidy up and filter the stopwords
-        $stopwords = array_map('trim', $stopwords);
-        $stopwords = array_filter($stopwords);
-
-        $this->cache->set('stopwords', ['id' => $id, 'stopwords' => $stopwords]);
-        return $stopwords;
-    }
-}

+ 28 - 19
app/Models/DataModel.php

@@ -2,12 +2,11 @@
 
 namespace ForkBB\Models;
 
-use ForkBB\Core\Container;
 use ForkBB\Models\Model;
 use InvalidArgumentException;
 use RuntimeException;
 
-abstract class DataModel extends Model
+class DataModel extends Model
 {
     /**
      * Массив флагов измененных свойств модели
@@ -16,28 +15,40 @@ abstract class DataModel extends Model
     protected $modified = [];
 
     /**
-     * Конструктор
+     * Устанавливает значения для свойств
      *
      * @param array $attrs
-     * @param Container $container
+     *
+     * @return DataModel
      */
-    public function __construct(array $attrs, Container $container)
+    public function setAttrs(array $attrs)
     {
-        parent::__construct($container);
-        $this->a = $attrs;
+        $this->a        = $attrs; //????
+        $this->aCalc    = [];
+        $this->modified = [];
+        return $this;
     }
 
     /**
-     * Устанавливает значения для свойств
-     *
+     * Перезапись свойст модели
+     * 
      * @param array $attrs
-     *
+     * 
      * @return DataModel
      */
-    public function setAttrs(array $attrs)
+    public function replAttrs(array $attrs)
     {
-        $this->a = $attrs; //????
+        foreach ($attrs as $key => $val) {
+            $this->{'__' . $key} = $val; //????
+            unset($this->aCalc['key']);
+        }
+
+        $modified = array_diff(array_keys($this->modified), array_keys($attrs));
         $this->modified = [];
+        foreach ($modified as $key) {
+            $this->modified[$key] = true;
+        }
+
         return $this;
     }
 
@@ -77,15 +88,13 @@ abstract class DataModel extends Model
      */
     public function __set($name, $val)
     {
-        // запись свойства без отслеживания изменений
+        // без отслеживания
         if (strpos($name, '__') === 0) {
-            return parent::__set(substr($name, 2), $val);
-        }
-
-        $old = isset($this->a[$name]) ? $this->a[$name] : null;
-        parent::__set($name, $val);
-        if ($old !== $this->a[$name]) {
+            $name = substr($name, 2);
+        // с отслеживанием
+        } else {
             $this->modified[$name] = true;
         }
+        return parent::__set($name, $val);
     }
 }

+ 177 - 21
app/Models/Forum.php

@@ -4,28 +4,11 @@ namespace ForkBB\Models;
 
 use ForkBB\Models\DataModel;
 use ForkBB\Core\Container;
+use RuntimeException;
 
 class Forum extends DataModel
 {
-    /**
-     * @param array $attrs
-     * 
-     * @return Forum
-     */
-    public function replAtttrs(array $attrs)
-    {
-        foreach ($attrs as $key => $val) {
-            $this->{'__' . $key} = $val; //????
-        }
-        $modified = array_diff(array_keys($this->modified), array_keys($attrs));
-        $this->modified = [];
-        foreach ($modified as $key) {
-            $this->modified[$key] = true;
-        }
-        return $this;
-    }
-
-    protected function getSubforums()
+    protected function getsubforums()
     {
         $sub = [];
         if (! empty($this->a['subforums'])) {
@@ -36,7 +19,7 @@ class Forum extends DataModel
         return $sub;
     }
 
-    protected function getDescendants()
+    protected function getdescendants()
     {
         $all = [];
         if (! empty($this->a['descendants'])) {
@@ -47,8 +30,181 @@ class Forum extends DataModel
         return $all;
     }
 
-    protected function getParent()
+    protected function getparent()
     {
         return $this->c->forums->forum($this->parent_forum_id);
     }
+
+    protected function getlink()
+    {
+        return $this->c->Router->link('Forum', ['id' => $this->id, 'name' => $this->forum_name]);
+    }
+
+    protected function getmoderators()
+    {
+        if (empty($this->a['moderators'])) {
+            return [];
+        }
+
+        $moderators = [];
+        $mods = unserialize($this->a['moderators']);
+        foreach ($mods as $name => $id) {
+            if ($this->c->user->g_view_users == '1') {
+                $moderators[$id] = [
+                    $this->c->Router->link('User', [
+                        'id' => $id,
+                        'name' => $name,
+                    ]),
+                    $name
+                ];
+            } else {
+                $moderators[$id] = $name;
+            }
+        }
+        return $moderators;
+    }
+
+    protected function gettree()
+    {
+        if (empty($this->a['tree'])) {
+            $numT   = (int) $this->num_topics;
+            $numP   = (int) $this->num_posts;
+            $time   = (int) $this->last_post;
+            $postId = (int) $this->last_post_id;
+            $poster = $this->last_poster;
+            $topic  = $this->last_topic;
+            $fnew   = $this->newMessages;
+            foreach ($this->descendants as $chId => $children) {
+                $fnew  = $fnew || $children->newMessages;
+                $numT += $children->num_topics;
+                $numP += $children->num_posts;
+                if ($children->last_post > $time) {
+                    $time   = $children->last_post;
+                    $postId = $children->last_post_id;
+                    $poster = $children->last_poster;
+                    $topic  = $children->last_topic;
+                }
+            }
+            $this->a['tree'] = $this->c->ModelForum->setAttrs([
+                'num_topics'     => $numT,
+                'num_posts'      => $numP,
+                'last_post'      => $time,
+                'last_post_id'   => $postId,
+                'last_poster'    => $poster,
+                'last_topic'     => $topic,
+                'newMessages'    => $fnew,
+                'last_post_link' => empty($postId) ? '' : $this->c->Router->link('ViewPost', ['id' => $postId]),
+            ]);
+        }
+        return $this->a['tree'];
+    }
+
+    /**
+     * @param int $page
+     * 
+     * @return bool
+     */
+    public function hasPage($page)
+    {
+        if (null === $this->num_topics) {
+            throw new RuntimeException('The model does not have the required data');
+        }
+
+        if (empty($this->num_topics)) {
+            if ($page !== 1) {
+                return false;
+            }
+            $this->page   = 1;
+            $this->pages  = 1;
+            $this->offset = 0;
+        } else {
+            $pages = ceil($this->num_topics / $this->c->user->disp_topics);
+            if ($page < 1 || $page > $pages) {
+                return false;
+            }
+            $this->page   = $page;
+            $this->pages  = $pages;
+            $this->offset = ($page - 1) * $this->c->user->disp_topics;
+        }
+        return true;
+    }
+
+    /**
+     * @return array
+     */
+    public function topics()
+    {
+        if (null === $this->page) {
+            throw new RuntimeException('The model does not have the required data');
+        }
+
+        if (empty($this->num_topics)) {
+            return [];
+        }
+
+        switch ($this->sort_by) {
+            case 1:
+                $sortBy = 'posted DESC';
+                break;
+            case 2:
+                $sortBy = 'subject ASC';
+                break;
+            case 0:
+            default:
+                $sortBy = 'last_post DESC';
+                break;
+        }
+
+        $vars = [
+            ':fid'    => $this->id,
+            ':offset' => $this->offset,
+            ':rows'   => $this->c->user->disp_topics,
+        ];
+        $sql = "SELECT id 
+                FROM ::topics 
+                WHERE forum_id=?i:fid 
+                ORDER BY sticky DESC, {$sortBy}, id DESC 
+                LIMIT ?i:offset, ?i:rows";
+
+        $ids = $this->c->DB->query($sql, $vars)->fetchAll(\PDO::FETCH_COLUMN);
+        if (empty($ids)) {
+            return []; //????
+        }
+
+        $vars = [
+            ':uid' => $this->c->user->id,
+            ':ids' => $ids,
+        ];
+
+        if (! $this->c->user->isGuest && $this->c->config->o_show_dot == '1') {
+            $dots = $this->c->DB
+                ->query('SELECT topic_id FROM ::posts WHERE poster_id=?i:uid AND topic_id IN (?ai:ids) GROUP BY topic_id', $vars)
+                ->fetchAll(\PDO::FETCH_COLUMN);
+            $dots = array_flip($dots);
+        } else {
+            $dots = [];
+        }
+
+        if ($this->c->user->isGuest) {
+            $sql = "SELECT t.* 
+                    FROM ::topics AS t 
+                    WHERE t.id IN(?ai:ids) 
+                    ORDER BY t.sticky DESC, t.{$sortBy}, t.id DESC";
+        } else {
+            $sql = "SELECT t.*, mot.mt_last_visit, mot.mt_last_read 
+                    FROM ::topics AS t 
+                    LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid) 
+                    WHERE t.id IN (?ai:ids) 
+                    ORDER BY t.sticky DESC, t.{$sortBy}, t.id DESC";
+        }
+        $topics = $this->c->DB->query($sql, $vars)->fetchAll();
+
+        foreach ($topics as &$cur) {
+            $cur['dot'] = isset($dots[$cur['id']]);
+            $cur = $this->c->ModelTopic->setAttrs($cur);
+        }
+        unset($cur);
+
+        return $topics;
+    }
 }

+ 18 - 22
app/Models/ForumList.php

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

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

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

+ 10 - 11
app/Models/ForumList/LoadTree.php

@@ -7,7 +7,7 @@ use ForkBB\Models\MethodModel;
 class LoadTree extends MethodModel
 {
     /**
-     * Загружает данные в модели разделов для указанного раздела и всех его потомков
+     * Загружает данные в модели для указанного раздела и всех его потомков
      * 
      * @param int $rootId
      * 
@@ -22,25 +22,25 @@ class LoadTree extends MethodModel
 
         $list = [];
         if (! $root->ready) {
-            $list[] = $rootId;
+            $list[$rootId] = $root;
         }
         foreach ($root->descendants as $id => $descendant) {
             if (! $descendant->ready) {
-                $list[] = $id;
+                $list[$id] = $descendant;
             }
         }
 
         $this->loadData($list);
 
         if (! $this->c->user->isGuest) {
-            $this->checkForNew(array_keys($root->descendants)); //????
+            $this->checkForNew($root->descendants);
         }
 
         return $root;
     }
 
     /**
-     * Загружает данные из БД по списку id разделов
+     * Загружает данные из БД по списку разделов
      * 
      * @param array $list
      */
@@ -52,7 +52,7 @@ class LoadTree extends MethodModel
 
         $vars = [
             ':uid'    => $this->c->user->id,
-            ':forums' => $list,
+            ':forums' => array_keys($list),
         ];
 
         if ($this->c->user->isGuest) {
@@ -79,12 +79,12 @@ class LoadTree extends MethodModel
 
         $stmt = $this->c->DB->query($sql, $vars);
         while ($cur = $stmt->fetch()) {
-            $this->model->forum($cur['id'])->replAtttrs($cur)->ready = true;
+            $list[$cur['id']]->replAttrs($cur)->__ready = true;
         }
     }
 
     /**
-     * Проверяет наличие новых сообщений в разделах по списку id
+     * Проверяет наличие новых сообщений в разделах по их списку
      * 
      * @param array $list
      */
@@ -97,8 +97,7 @@ class LoadTree extends MethodModel
         // предварительная проверка разделов
         $time = [];
         $max = max((int) $this->c->user->last_visit, (int) $this->c->user->u_mark_all_read);
-        foreach ($list as $id) {
-            $forum = $this->model->forum($id);
+        foreach ($list as $forum) {
             $t = max($max, (int) $forum->mf_mark_all_read);
             if ($forum->last_post > $t) {
                 $time[$id] = $t;
@@ -125,7 +124,7 @@ class LoadTree extends MethodModel
         $stmt = $this->c->DB->query($sql, $vars);
         while ($cur = $stmt->fetch()) {
             if ($cur['last_post'] > $time[$cur['forum_id']]) {
-                $this->model->forum($cur['forum_id'])->newMessages = true; //????
+                $list[$cur['forum_id']]->__newMessages = true; //????
             }
         }
     }

+ 97 - 0
app/Models/ForumList/Refresh.php

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

+ 39 - 9
app/Models/Model.php

@@ -3,10 +3,9 @@
 namespace ForkBB\Models;
 
 use ForkBB\Core\Container;
-use InvalidArgumentException;
 use RuntimeException;
 
-abstract class Model
+class Model
 {
     /**
      * Контейнер
@@ -20,6 +19,12 @@ abstract class Model
      */
     protected $a = [];
 
+    /**
+     * Вычисленные данные модели
+     * @var array
+     */
+    protected $aCalc = [];
+
     /**
      * Конструктор
      *
@@ -39,7 +44,9 @@ abstract class Model
      */
     public function __isset($name)
     {
-        return isset($this->a[$name]); //???? array_key_exists($name, $this->a)
+        return array_key_exists($name, $this->a) 
+            || array_key_exists($name, $this->aCalc)
+            || method_exists($this, 'get' . $name);
     }
 
     /**
@@ -49,7 +56,8 @@ abstract class Model
      */
     public function __unset($name)
     {
-        unset($this->a[$name]);
+        unset($this->a[$name]);     //????
+        unset($this->aCalc[$name]); //????
     }
 
     /**
@@ -60,8 +68,9 @@ abstract class Model
      */
     public function __set($name, $val)
     {
-        $method = 'set' . ucfirst($name);
-        if (method_exists($this, $method)) {
+        unset($this->aCalc[$name]);
+
+        if (method_exists($this, $method = 'set' . $name)) {
             $this->$method($val);
         } else {
             $this->a[$name] = $val;
@@ -77,9 +86,10 @@ abstract class Model
      */
     public function __get($name)
     {
-        $method = 'get' . ucfirst($name);
-        if (method_exists($this, $method)) {
-            return $this->$method();
+        if (array_key_exists($name, $this->aCalc)) {
+            return $this->aCalc[$name];
+        } elseif (method_exists($this, $method = 'get' . $name)) {
+            return $this->aCalc[$name] = $this->$method();
         } else {
             return isset($this->a[$name]) ? $this->a[$name] : null;
         }
@@ -112,4 +122,24 @@ abstract class Model
             return $factory->$name(...$args);
         }
     }
+
+    public function cens()
+    {
+        return $this->c->FuncAll->setModel('cens', $this);
+    }
+
+    public function num($decimals = 0)
+    {
+        return $this->c->FuncAll->setModel('num', $this, $decimals);
+    }
+
+    public function dt($dateOnly = false, $dateFormat = null, $timeFormat = null, $timeOnly = false, $noText = false)
+    {
+        return $this->c->FuncAll->setModel('dt', $this, $dateOnly, $dateFormat, $timeFormat, $timeOnly, $noText);
+    }
+
+    public function utc()
+    {
+        return $this->c->FuncAll->setModel('utc', $this);
+    }
 }

+ 64 - 73
app/Models/Online.php

@@ -2,46 +2,34 @@
 
 namespace ForkBB\Models;
 
-use ForkBB\Core\Container;
 use ForkBB\Models\Model;
 use ForkBB\Models\User;
 use ForkBB\Models\Page;
 
 class Online extends Model
 {
-    /**
-     * Конструктор
-     *
-     * @param Container $container
-     */
-    public function __construct(Container $container)
-    {
-        parent::__construct($container);
-        $this->users  = [];
-        $this->guests = [];
-        $this->bots   = [];
-    }
-
     /**
      * Обработка данных пользователей онлайн
      * Обновление данных текущего пользователя
      * Возврат данных по пользователям онлайн
      *
      * @param Page $page
+     * 
+     * @return Online
      */
     public function calc(Page $page)
     {
         if ($this->done) {
-            return;
+            return $this;
         }
         $this->done = true;
 
         $position = $page->onlinePos;
         if (null === $position) {
-            return;
+            return $this;
         }
-        $type     = $page->onlineType;
-        $filter   = $page->onlineFilter;
+        $detail = $page->onlineDetail && $this->c->config->o_users_online == '1';
+        $filter = $page->onlineFilter;
 
         $this->updateUser($position);
 
@@ -49,69 +37,67 @@ class Online extends Model
         $now     = time();
         $tOnline = $now - $this->c->config->o_timeout_online;
         $tVisit  = $now - $this->c->config->o_timeout_visit;
+        $online  = [];
         $users   = [];
         $guests  = [];
         $bots    = [];
         $deleteG = false;
         $deleteU = false;
-        $setIdle = false;
 
-        if ($this->c->config->o_users_online == '1' && $type) {
-            $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle, o_position, o_name FROM ::online ORDER BY logged');
-        } elseif ($type) {
-            $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle FROM ::online ORDER BY logged');
+        if ($detail) {
+            $sql = 'SELECT user_id, ident, logged, o_position, o_name FROM ::online ORDER BY logged';
         } else {
-            $stmt = $this->c->DB->query('SELECT user_id, ident, logged, idle FROM ::online WHERE logged<?i:online', [':online' => $tOnline]);
+            $sql = 'SELECT user_id, ident, logged FROM ::online ORDER BY logged';
         }
-        while ($cur = $stmt->fetch()) {
+        $stmt = $this->c->DB->query($sql);
 
+        while ($cur = $stmt->fetch()) {
             // посетитель уже не онлайн (или почти не онлайн)
             if ($cur['logged'] < $tOnline) {
                 // пользователь
                 if ($cur['user_id'] > 1) {
                     if ($cur['logged'] < $tVisit) {
                         $deleteU = true;
-                        $this->c->DB->exec('UPDATE ::users SET last_visit=?i:last WHERE id=?i:id', [':last' => $cur['logged'], ':id' => $cur['user_id']]);
-                    } elseif ($cur['idle'] == '0') {
-                        $setIdle = true;
+                        $this->c->DB->exec('UPDATE ::users SET last_visit=?i:last WHERE id=?i:id', [':last' => $cur['logged'], ':id' => $cur['user_id']]); //????
                     }
                 // гость
                 } else {
                     $deleteG = true;
                 }
+                continue;
+            }
 
-            // обработка посетителя для вывода статистики
-            } elseif ($type) {
-                ++$all;
-
-                // включен фильтр и проверка не пройдена
-                if ($filter && $cur['o_position'] !== $position) {
-                    continue;
-                }
+            // пользователи онлайн и общее количество
+            $online[$cur['user_id']] = true;
+            ++$all;
 
-                // пользователь
-                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'],
-                    ];
-                }
+            if (! $detail) {
+                continue;
+            }
 
-            // просто +1 к общему числу посетителей
+            // включен фильтр и проверка не пройдена
+            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 {
-                ++$all;
+                $bots[$cur['o_name']][] = [
+                    'name'   => $cur['ident'],
+                    'logged' => $cur['logged'],
+                ];
             }
         }
 
@@ -125,20 +111,26 @@ class Online extends Model
             $this->c->DB->exec('DELETE FROM ::online WHERE user_id=1 AND logged<?i:online', [':online' => $tOnline]);
         }
 
-        // обновление idle
-        if ($setIdle) {
-            $this->c->DB->exec('UPDATE ::online SET idle=1 WHERE logged<?i:online', [':online' => $tOnline]);
-        }
-
         // обновление максимального значение пользоватеелй онлайн
         if ($this->c->config->st_max_users < $all) {
             $this->c->config->st_max_users      = $all;
             $this->c->config->st_max_users_time = $now;
             $this->c->config->save();
         }
-        $this->users  = $users;
-        $this->guests = $guests;
-        $this->bots   = $bots;
+
+        $this->all    = $all;
+        $this->detail = $detail;
+
+        unset($online[1]);
+        $this->online = $online;
+
+        if ($detail) {
+            $this->users  = $users;
+            $this->guests = $guests;
+            $this->bots   = $bots;
+        }
+
+        return $this;
     }
 
     /**
@@ -148,14 +140,13 @@ class Online extends Model
      */
     protected function updateUser($position)
     {
-        $now = time();
         // гость
         if ($this->c->user->isGuest) {
             $vars = [
                 ':logged' => time(),
-                ':pos' => $position,
-                ':name' => (string) $this->c->user->isBot,
-                ':ip' => $this->c->user->ip
+                ':pos'    => $position,
+                ':name'   => (string) $this->c->user->isBot,
+                ':ip'     => $this->c->user->ip
             ];
             if ($this->c->user->isLogged) {
                 $this->c->DB->exec('UPDATE ::online SET logged=?i:logged, o_position=?s:pos, o_name=?s:name WHERE user_id=1 AND ident=?s:ip', $vars);
@@ -166,12 +157,12 @@ class Online extends Model
         // пользователь
             $vars = [
                 ':logged' => time(),
-                ':pos' => $position,
-                ':id' => $this->c->user->id,
-                ':name' => $this->c->user->username,
+                ':pos'    => $position,
+                ':id'     => $this->c->user->id,
+                ':name'   => $this->c->user->username,
             ];
             if ($this->c->user->isLogged) {
-                $this->c->DB->exec('UPDATE ::online SET logged=?i:logged, idle=0, o_position=?s:pos WHERE user_id=?i:id', $vars);
+                $this->c->DB->exec('UPDATE ::online SET logged=?i:logged, o_position=?s:pos WHERE user_id=?i:id', $vars);
             } else {
                 $this->c->DB->exec('INSERT INTO ::online (user_id, ident, logged, o_position) SELECT ?i:id, ?s:name, ?i:logged, ?s:pos FROM ::groups WHERE NOT EXISTS (SELECT 1 FROM ::online WHERE user_id=?i:id) LIMIT 1', $vars);
             }

+ 56 - 0
app/Models/Online/Info.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace ForkBB\Models\Online;
+
+use ForkBB\Models\MethodModel;
+
+class Info extends MethodModel
+{
+    /**
+     * Получение информации об онлайн посетителях
+     *
+     * @return null|Online
+     */
+    public function info()
+    {
+        if (! $this->model->detail) {
+            return null;
+        }
+            
+        $this->model->maxNum = $this->c->config->st_max_users;
+        $this->model->maxTime = $this->c->config->st_max_users_time;
+
+        $info   = [];
+        if ($this->c->user->g_view_users == '1') {
+            foreach ($this->model->users as $id => $user) {
+                $info[] = [
+                    $this->c->Router->link('User', [
+                        'id' => $id,
+                        'name' => $user['name'],
+                    ]),
+                    $user['name'],
+                ];
+            }
+        } else {
+            foreach ($this->model->users as $user) {
+                $info[] = $user['name'];
+            }
+        }
+        $this->model->numUsers = count($info);
+
+        $s = 0;
+        foreach ($this->model->bots as $bot => $arr) {
+            $count = count($arr);
+            $s += $count;
+            if ($count > 1) {
+                $info[] = '[Bot] ' . $bot . ' (' . $count . ')';
+            } else {
+                $info[] = '[Bot] ' . $bot;
+            }
+        }
+        $this->model->numGuests = $s + count($this->model->guests);
+        $this->model->info      = $info;
+
+        return $this->model;
+    }
+}

+ 6 - 10
app/Models/Page.php

@@ -26,12 +26,8 @@ abstract class Page extends Model
 #       $this->titles       = [];      # array       Массив титула страницы | setTitles()
         $this->fIswev       = [];      # array       Массив info, success, warning, error, validation информации
 #       $this->onlinePos    = '';      # null|string Позиция для таблицы онлайн текущего пользователя
-        $this->onlineType   = false;   # bool        Тип обработки пользователей онлайн
-                                       #             Если false, то идет обновление данных
-                                       #             Если true, то идет возврат данных (смотрите onlineFilter)
-        $this->onlineFilter = true;    # bool        Тип возврата данных при onlineType === true
-                                       #             Если true, то из online должны вернутся только пользователи находящиеся на этой же странице
-                                       #             Если false, то все пользователи online
+        $this->onlineDetail = false;   # bool        Формировать данные по посетителям online или нет
+        $this->onlineFilter = true;    # bool        Посетители только по текущей странице или по всем
 #       $this->robots       = '';      # string      Переменная для meta name="robots"
 #       $this->canonical    = '';      # string      Переменная для link rel="canonical"
 
@@ -140,7 +136,7 @@ abstract class Page extends Model
      *
      * @return string
      */
-    protected function getPageTitle(array $titles = [])
+    protected function getpageTitle(array $titles = [])
     {
         if (empty($titles)) {
             $titles = $this->titles;
@@ -155,7 +151,7 @@ abstract class Page extends Model
      *
      * @return array
      */
-    protected function getPageHeaders()
+    protected function getpageHeaders()
     {
         $headers = ['link rel="stylesheet" type="text/css" href="' . $this->c->PUBLIC_URL . '/style/' . $this->c->user->style . '/style.css' . '"'];
         if ($this->robots) {
@@ -173,7 +169,7 @@ abstract class Page extends Model
      *
      * @return array
      */
-    protected function getHttpHeaders()
+    protected function gethttpHeaders()
     {
         $headers = $this->a['httpHeaders'];
         if (! empty($status = $this->httpStatus())) {
@@ -216,7 +212,7 @@ abstract class Page extends Model
      *
      * @param string @val
      */
-    public function setTitles($val)
+    public function settitles($val)
     {
         if (empty($this->a['titles'])) {
             $this->a['titles'] = [$val];

+ 1 - 1
app/Models/Pages/Admin/Statistics.php

@@ -59,7 +59,7 @@ class Statistics extends Admin
         }
 
         // Get number of current visitors
-        $this->numOnline = $this->c->DB->query('SELECT COUNT(user_id) FROM ::online WHERE idle=0')->fetchColumn();
+        $this->numOnline = $this->c->Online->calc($this)->all;
 
         $stat = $this->c->DB->statistics();
         $this->dbVersion = $stat['db'];

+ 18 - 68
app/Models/Pages/CrumbTrait.php

@@ -2,10 +2,13 @@
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Models\Model;
+
 trait CrumbTrait 
 {
     /**
      * Возвращает массив хлебных крошек
+     * Заполняет массив титула страницы
      * 
      * @param mixed $args
      * 
@@ -17,84 +20,31 @@ trait CrumbTrait
         $active = true;
 
         foreach ($args as $arg) {
-            if (isset($arg->forum_name)) {
-                while ($arg->id > 0) {
-                    $this->titles = $arg->forum_name;
-                    $crumbs[] = [
-                        $this->c->Router->link('Forum', ['id' => $arg->id, 'name' => $arg->forum_name]),
-                        $arg->forum_name,
-                        $active,
-                    ];
-                    $active = null;
-                    $arg = $arg->parent;
-                }
-            } else {
-                $this->titles = (string) $arg;
-                $crumbs[] = [
-                    null,
-                    (string) $arg,
-                    $active,
-                ];
-            }
-/*
-            if (is_array($arg)) {
-                $cur = array_shift($arg);
-                // массив разделов
-                if (is_array($cur)) {
-                    $id = $arg[0];
-                    while (true) {
-                        $this->titles = $cur[$id]['forum_name'];
-                        $crumbs[] = [
-                            $this->c->Router->link('Forum', ['id' => $id, 'name' => $cur[$id]['forum_name']]),
-                            $cur[$id]['forum_name'], 
-                            $active,
-                        ];
-                        $active = null;
-                        if (! isset($cur[$id][0])) {
-                            break;
-                        }
-                        $id = $cur[$id][0];
-                    }
-                // отдельная страница
-                } else {
-                    // определение названия
-                    if (isset($arg[1])) {
-                        $vars = $arg[0];
-                        $name = $arg[1];
-                    } elseif (is_string($arg[0])) {
-                        $vars = [];
-                        $name = $arg[0];
-                    } elseif (isset($arg[0]['name'])) {
-                        $vars = $arg[0];
-                        $name = $arg[0]['name'];
+            // Раздел или топик
+            if ($arg instanceof Model) {
+                while (null !== $arg->parent && $arg->link) {
+                    if (isset($arg->forum_name)) {
+                        $name = $arg->forum_name;
+                    } elseif (isset($arg->subject)) {
+                        $name = $arg->cens()->subject;
                     } else {
-                        continue;
+                        $name = 'no name';
                     }
+
                     $this->titles = $name;
-                    $crumbs[] = [
-                        $this->c->Router->link($cur, $vars),
-                        $name, 
-                        $active,
-                    ];
+                    $crumbs[] = [$arg->link, $name, $active];
+                    $active = null;
+                    $arg = $arg->parent;
                 }
-            // предположительно идет только название, без ссылки
+            // Строка
             } else {
                 $this->titles = (string) $arg;
-                $crumbs[] = [
-                    null,
-                    (string) $arg,
-                    $active,
-                ];
+                $crumbs[] = [null, (string) $arg, $active];
             }
-*/
             $active = null;
         }
         // главная страница
-        $crumbs[] = [
-            $this->c->Router->link('Index'),
-            __('Index'),
-            $active,
-        ];
+        $crumbs[] = [$this->c->Router->link('Index'), __('Index'), $active];
 
         return array_reverse($crumbs);
     }

+ 15 - 122
app/Models/Pages/Forum.php

@@ -6,7 +6,6 @@ use ForkBB\Models\Page;
 
 class Forum extends Page
 {
-    use ForumsTrait;
     use CrumbTrait;
 
     /**
@@ -29,143 +28,37 @@ class Forum extends Page
             return $this->c->Redirect->url($forum->redirect_url);
         }
 
-        $user = $this->c->user;
         $page = isset($args['page']) ? (int) $args['page'] : 1;
-        if (empty($forum->num_topics)) {
-            // попытка открыть страницу которой нет
-            if ($page !== 1) {
-                return $this->c->Message->message('Bad request');
-            }
-
-            $pages = 1;
-            $offset = 0;
-            $topics = null;
-        } else {
-            $pages = ceil($forum->num_topics / $user->disp_topics);
-
-            // попытка открыть страницу которой нет
-            if ($page < 1 || $page > $pages) {
-                return $this->c->Message->message('Bad request');
-            }
-
-            $offset = ($page - 1) * $user->disp_topics;
-
-            switch ($forum->sort_by) {
-                case 1:
-                    $sortBy = 'posted DESC';
-                    break;
-                case 2:
-                    $sortBy = 'subject ASC';
-                    break;
-                case 0:
-                default:
-                    $sortBy = 'last_post DESC';
-                    break;
-            }
-
-            $vars = [
-                ':fid'    => $args['id'],
-                ':offset' => $offset,
-                ':rows'   => $user->disp_topics,
-            ];
-            $topics = $this->c->DB
-                ->query("SELECT id FROM ::topics WHERE forum_id=?i:fid ORDER BY sticky DESC, {$sortBy}, id DESC LIMIT ?i:offset, ?i:rows", $vars)
-                ->fetchAll(\PDO::FETCH_COLUMN);
+        if (! $forum->hasPage($page)) {
+            return $this->c->Message->message('Bad request');
         }
 
-        if (! empty($topics)) {
-            $vars = [
-                ':uid'    => $user->id,
-                ':topics' => $topics,
-            ];
-
-            if (! $user->isGuest && $this->c->config->o_show_dot == '1') {
-                $dots = $this->c->DB
-                    ->query('SELECT topic_id FROM ::posts WHERE poster_id=?i:uid AND topic_id IN (?ai:topics) GROUP BY topic_id', $vars)
-                    ->fetchAll(\PDO::FETCH_COLUMN);
-                $dots = array_flip($dots);
-            } else {
-                $dots = [];
-            }
-
-            if (! $user->isGuest) {
-                $lower = max((int) $user->u_mark_all_read, (int) $forum->mf_mark_all_read);
-                $upper = max($lower, (int) $user->last_visit);
-            }
-
-            if ($user->isGuest) {
-                $sql = "SELECT id, poster, subject, posted, last_post, last_post_id, last_poster, num_views, num_replies, closed, sticky, moved_to, poll_type FROM ::topics WHERE id IN(?ai:topics) ORDER BY sticky DESC, {$sortBy}, id DESC";
-            } else {
-                $sql = "SELECT t.id, t.poster, t.subject, t.posted, t.last_post, t.last_post_id, t.last_poster, t.num_views, t.num_replies, t.closed, t.sticky, t.moved_to, t.poll_type, mot.mt_last_visit, mot.mt_last_read FROM ::topics AS t LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid) WHERE t.id IN (?ai:topics) ORDER BY t.sticky DESC, t.{$sortBy}, t.id DESC";
-            }
-            $topics = $this->c->DB->query($sql, $vars)->fetchAll();
-
-            foreach ($topics as &$cur) {
-                $cur['subject'] = $this->c->censorship->censor($cur['subject']);
-                // перенос темы
-                if ($cur['moved_to']) {
-                    $cur['link'] = $this->c->Router->link('Topic', ['id' => $cur['moved_to'], 'name' => $cur['subject']]);
-                    $cur['link_last'] = null;
-                    $cur['link_new'] = null;
-                    $cur['link_unread'] = null;
-                    $cur['dot'] = false;
-                    continue;
-                }
-                // страницы темы
-                $tPages = ceil(($cur['num_replies'] + 1) / $user->disp_posts);
-                if ($tPages > 1) {
-                    $cur['pages'] = $this->c->Func->paginate($tPages, -1, 'Topic', ['id' => $cur['id'], 'name' => $cur['subject']]);
-                } else {
-                    $cur['pages'] = null;
-                }
+        $topics = $forum->topics();
+        $user = $this->c->user;
 
-                $cur['link'] = $this->c->Router->link('Topic', ['id' => $cur['id'], 'name' => $cur['subject']]);
-                $cur['link_last'] = $this->c->Router->link('ViewPost', ['id' => $cur['last_post_id']]);
-                $cur['views'] = $this->c->config->o_topic_views == '1' ? $this->number($cur['num_views']) : null;
-                $cur['replies'] = $this->number($cur['num_replies']);
-                $time = $cur['last_post'];
-                $cur['last_post'] = $this->time($cur['last_post']);
-                // для гостя пусто
-                if ($user->isGuest) {
-                    $cur['link_new'] = null;
-                    $cur['link_unread'] = null;
-                    $cur['dot'] = false;
-                    continue;
-                }
-                // новые сообщения
-                if ($time > max($upper, (int) $cur['mt_last_visit'])) {
-                    $cur['link_new'] = $this->c->Router->link('TopicViewNew', ['id' => $cur['id']]);
-                } else {
-                    $cur['link_new'] = null;
-                }
-                // не прочитанные сообщения
-                if ($time > max($lower, (int) $cur['mt_last_read'])) {
-                    $cur['link_unread'] = $this->c->Router->link('TopicViewUnread', ['id' => $cur['id']]);
-                } else {
-                    $cur['link_unread'] = null;
-                }
-                // активность пользователя в теме
-                $cur['dot'] = isset($dots[$cur['id']]);
-            }
-            unset($cur);
+        if (! $user->isGuest) {
+            $lower = max((int) $user->u_mark_all_read, (int) $forum->mf_mark_all_read);
+            $upper = max($lower, (int) $user->last_visit);
         }
 
-        $moders = empty($forum->moderators) ? [] : array_flip(unserialize($forum->moderators));
+        if (empty($topics)) {
+            $this->a['fIswev']['i'][] = __('Empty forum');
+        }
         $newOn = $forum->post_topics == 1
             || (null === $forum->post_topics && $user->g_post_topics == 1)
             || $user->isAdmin
-            || ($user->isAdmMod && isset($moders[$user->id]));
+            || ($user->isAdmMod && isset($forum->moderators[$user->id]));
 
         $this->fIndex     = 'index';
         $this->nameTpl    = 'forum';
         $this->onlinePos  = 'forum-' . $args['id'];
-        $this->canonical  = $this->c->Router->link('Forum', ['id' => $args['id'], 'name' => $forum->forum_name, 'page' => $page]);
-        $this->forums     = $this->forumsData($args['id']);
+        $this->canonical  = $this->c->Router->link('Forum', ['id' => $args['id'], 'name' => $forum->forum_name, 'page' => $forum->page]);
+        $this->forum      = $forum;
+        $this->forums     = $forum->subforums;
         $this->topics     = $topics;
         $this->crumbs     = $this->crumbs($forum);
-        $this->forumName  = $forum->forum_name;
         $this->newTopic   = $newOn ? $this->c->Router->link('NewTopic', ['id' => $args['id']]) : null;
-        $this->pages      = $this->c->Func->paginate($pages, $page, 'Forum', ['id' => $args['id'], 'name' => $forum->forum_name]);
+        $this->pages      = $this->c->Func->paginate($forum->pages, $forum->page, 'Forum', ['id' => $args['id'], 'name' => $forum->forum_name]);
 
         return $this;
     }

+ 0 - 102
app/Models/Pages/ForumsTrait.php

@@ -1,102 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Pages;
-
-trait ForumsTrait 
-{
-    /**
-     * Получение данных по разделам
-     * 
-     * @param int $rootId
-     * 
-     * @return array
-     */
-    protected function forumsData($rootId = 0)
-    {
-        $root = $this->c->forums->loadTree($rootId);
-        if (empty($root)) {
-            return [];
-        }
-
-        $r = $this->c->Router;
-
-        // формированием таблицы разделов
-        $result = [];
-        foreach ($root->subforums as $forumId => $forum) {
-            // модераторы
-            $moderators = [];
-            if (! empty($forum->moderators)) {
-                $mods = unserialize($forum->moderators);
-                foreach ($mods as $name => $id) {
-                    if ($this->c->user->g_view_users == '1') {
-                        $moderators[] = [
-                            $r->link('User', [
-                                'id' => $id,
-                                'name' => $name,
-                            ]),
-                            $name
-                        ];
-                    } else {
-                        $moderators[] = $name;
-                    }
-                }
-            }
-
-            // список подразделов
-            $subForums = [];
-            foreach ($forum->subforums as $subId => $subforum) {
-                $subForums[] = [
-                    $r->link('Forum', [
-                        'id'   => $subId,
-                        'name' => $subforum->forum_name,
-                    ]),
-                    $subforum->forum_name,
-                ];
-            }
-
-            // статистика по разделам
-            $numT   = (int) $forum->num_topics;
-            $numP   = (int) $forum->num_posts;
-            $time   = (int) $forum->last_post;
-            $postId = (int) $forum->last_post_id;
-            $poster = $forum->last_poster;
-            $topic  = $forum->last_topic;
-            $fnew   = $forum->newMessages;
-            foreach ($forum->descendants as $chId => $children) {
-                $fnew  = $fnew || $children->newMessages;
-                $numT += $children->num_topics;
-                $numP += $children->num_posts;
-                if ($children->last_post > $time) {
-                    $time   = $children->last_post;
-                    $postId = $children->last_post_id;
-                    $poster = $children->last_poster;
-                    $topic  = $children->last_topic;
-                }
-            }
-
-            $result[$forum->cid]['name'] = $forum->cat_name;
-            $result[$forum->cid]['forums'][] = [
-                'fid'          => $forumId,
-                'forum_name'   => $forum->forum_name,
-                'forum_desc'   => $forum->forum_desc,
-                'forum_link'   => $r->link('Forum', [
-                    'id'   => $forumId,
-                    'name' => $forum->forum_name,
-                ]),
-                'redirect_url' => $forum->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'   => $this->c->censorship->censor($topic),
-                'new'          => $fnew,
-            ];
-        }
-        return $result;
-    }
-}

+ 19 - 18
app/Models/Pages/Index.php

@@ -6,9 +6,6 @@ use ForkBB\Models\Page;
 
 class Index extends Page
 {
-    use ForumsTrait;
-    use OnlineTrait;
-
     /**
      * Подготовка данных для шаблона
      * 
@@ -19,31 +16,35 @@ class Index extends Page
         $this->c->Lang->load('index');
         $this->c->Lang->load('subforums');
 
-        $stats = [];
-        $stats['total_users']  = $this->number($this->c->stats->userTotal);
-        $stats['total_posts']  = $this->number($this->c->stats->postTotal);
-        $stats['total_topics'] = $this->number($this->c->stats->topicTotal);
-
-        if ($this->c->user->g_view_users == '1') {
-            $stats['newest_user'] = [
-                $this->c->Router->link('User', [
+        // крайний пользователь // ???? может в stats переместить?
+        $this->c->stats->userLast = $this->c->user->g_view_users == '1' 
+            ? [ $this->c->Router->link('User', [
                     'id'   => $this->c->stats->userLast['id'],
                     'name' => $this->c->stats->userLast['username'],
                 ]),
-                $this->c->stats->userLast['username']
-            ];
+                $this->c->stats->userLast['username'],
+            ] : $this->c->stats->userLast['username'];
+
+        // для таблицы разделов
+        $root   = $this->c->forums->loadTree(0);
+        $forums = empty($root) ? [] : $root->subforums;
+        $ctgs   = [];
+        if (empty($forums)) {
+            $this->a['fIswev']['i'][] = __('Empty board');
         } else {
-            $stats['newest_user'] = $this->c->stats->userLast['username'];
+            foreach($forums as $forum) {
+                $ctgs[$forum->cat_id][] = $forum;
+            }
         }
 
         $this->nameTpl      = 'index';
         $this->onlinePos    = 'index';
-        $this->onlineType   = true;
+        $this->onlineDetail = true;
         $this->onlineFilter = false;
         $this->canonical    = $this->c->Router->link('Index');
-        $this->stats        = $stats;
-        $this->online       = $this->usersOnlineInfo();
-        $this->forums       = $this->forumsData();
+        $this->stats        = $this->c->stats;
+        $this->online       = $this->c->Online->calc($this)->info();
+        $this->categoryes   = $ctgs;
 
         return $this;
     }

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

@@ -483,7 +483,7 @@ class Install extends Page
                 'user_id'     => ['INT(10) UNSIGNED', false, 1],
                 'ident'       => ['VARCHAR(200)', false, ''],
                 'logged'      => ['INT(10) UNSIGNED', false, 0],
-                'idle'        => ['TINYINT(1)', false, 0],
+                'idle'        => ['TINYINT(1)', false, 0], //????
                 'last_post'   => ['INT(10) UNSIGNED', true],
                 'last_search' => ['INT(10) UNSIGNED', true],
                 'witt_data'   => ['VARCHAR(255)', false, ''],  //????
@@ -496,7 +496,7 @@ class Install extends Page
             'INDEXES' => [
                 'ident_idx'      => ['ident'],
                 'logged_idx'     => ['logged'],
-                'o_position_idx' => ['o_position'],
+                'o_position_idx' => ['o_position'], //????
             ],
             'ENGINE' => $this->DBEngine,
         ];

+ 0 - 62
app/Models/Pages/OnlineTrait.php

@@ -1,62 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Pages;
-
-trait OnlineTrait 
-{
-    /**
-     * Получение информации об онлайн посетителях
-     * @return null|array
-     */
-    protected function usersOnlineInfo() 
-    {
-        if ($this->c->config->o_users_online == '1') {
-            // данные онлайн посетителей
-            $this->c->Online->calc($this);
-            $users  = $this->c->Online->users; //????
-            $guests = $this->c->Online->guests;
-            $bots   = $this->c->Online->bots;
-
-            $list   = [];
-            $data = [
-                'max'      => $this->number($this->c->config->st_max_users),
-                'max_time' => $this->time($this->c->config->st_max_users_time),
-            ];
-
-            if ($this->c->user->g_view_users == '1') {
-                foreach ($users as $id => $cur) {
-                    $list[] = [
-                        $this->c->Router->link('User', [
-                            'id' => $id,
-                            'name' => $cur['name'],
-                        ]),
-                        $cur['name'],
-                    ];
-                }
-            } else {
-                foreach ($users as $cur) {
-                    $list[] = $cur['name'];
-                }
-            }
-            $data['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);
-            $data['number_of_guests'] = $this->number($s);
-            $data['list'] = $list;
-            return $data;
-        } else {
-            $this->onlineType = false;
-            return null;
-        }
-    }
-}

+ 90 - 265
app/Models/Pages/Topic.php

@@ -6,16 +6,8 @@ use ForkBB\Models\Page;
 
 class Topic extends Page
 {
-    use UsersTrait;
-    use OnlineTrait;
     use CrumbTrait;
 
-    /**
-     * Данные по текущей теме
-     * @var array
-     */
-    protected $curTopic;
-
     /**
      * Переход к первому новому сообщению темы (или в конец)
      * 
@@ -25,35 +17,7 @@ class Topic extends Page
      */
     public function viewNew(array $args)
     {
-        $topic = $this->curTopic($args['id']); 
-        if (false === $topic) {
-            return $this->c->Message->message('Bad request');
-        }
-
-        if (! $this->c->user->isGuest) {
-            $upper = max(
-                (int) $this->c->user->last_visit,
-                (int) $this->c->user->u_mark_all_read,
-                (int) $topic['mf_mark_all_read'],
-                (int) $topic['mt_last_visit']
-            );
-
-            if ($upper < $topic['last_post']) {
-                $vars = [
-                    ':tid' => $args['id'],
-                    ':visit' => $upper,
-                ];
-                $sql = 'SELECT MIN(id) FROM ::posts WHERE topic_id=?i:tid AND posted>?i:visit';
-
-                $pid = $this->c->DB->query($sql, $vars)->fetchColumn();
-
-                if (! empty($pid)) {
-                    return $this->c->Redirect->page('ViewPost', ['id' => $pid]);
-                }
-            }
-        }
-
-        return $this->viewLast(['id' => $topic['id']]);
+        return $this->view('new', $args);
     }
 
     /**
@@ -65,34 +29,7 @@ class Topic extends Page
      */
     public function viewUnread(array $args)
     {
-        $topic = $this->curTopic($args['id']); 
-        if (false === $topic) {
-            return $this->c->Message->message('Bad request');
-        }
-
-        if (! $this->c->user->isGuest) {
-            $lower = max(
-                (int) $this->c->user->u_mark_all_read,
-                (int) $topic['mf_mark_all_read'],
-                (int) $topic['mt_last_read']
-            );
-
-            if ($lower < $topic['last_post']) {
-                $vars = [
-                    ':tid' => $args['id'],
-                    ':visit' => $lower,
-                ];
-                $sql = 'SELECT MIN(id) FROM ::posts WHERE topic_id=?i:tid AND posted>?i:visit';
-
-                $pid = $this->c->DB->query($sql, $vars)->fetchColumn();
-
-                if (! empty($pid)) {
-                    return $this->c->Redirect->page('ViewPost', ['id' => $pid]);
-                }
-            }
-        }
-
-        return $this->viewLast(['id' => $topic['id']]);
+        return $this->view('unread', $args);
     }
 
     /**
@@ -104,100 +41,9 @@ class Topic extends Page
      */
     public function viewLast(array $args)
     {
-        $topic = $this->curTopic($args['id']); 
-        if (false === $topic) {
-            return $this->c->Message->message('Bad request');
-        }
-
-        $vars = [
-            ':tid' => $args['id'],
-        ];
-        $sql = 'SELECT MAX(id) FROM ::posts WHERE topic_id=?i:tid';
-
-        $pid = $this->c->DB->query($sql, $vars)->fetchColumn();
-        // нет ни одного сообщения в теме
-        if (empty($pid)) {
-            return $this->c->Message->message('Bad request');
-        }
-
-        return $this->c->Redirect->page('ViewPost', ['id' => $pid]);
-     }
-
-    /**
-     * Получение данных по текущей теме
-     * 
-     * @param mixed $id
-     * @param mixed $pid
-     * 
-     * @return bool|array
-     */
-    protected function curTopic($id, $pid = null)
-    {
-        if ($this->curTopic) {
-            return $this->curTopic;
-        }
-
-        if (isset($pid)) {
-            $vars = [
-                ':pid' => $pid,
-                ':uid' => $this->c->user->id,
-            ];
-            if ($this->c->user->isGuest) {
-                $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed, 0 AS mf_mark_all_read, 0 AS mt_last_visit, 0 AS mt_last_read
-                        FROM ::topics AS t
-                        INNER JOIN ::forums AS f ON f.id=t.forum_id
-                        INNER JOIN ::posts AS p ON t.id=p.topic_id
-                        WHERE p.id=?i:pid AND t.moved_to IS NULL';
-
-            } else {
-                $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
-                        FROM ::topics AS t
-                        INNER JOIN ::forums AS f ON f.id=t.forum_id
-                        INNER JOIN ::posts AS p ON t.id=p.topic_id
-                        LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
-                        LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
-                        LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
-                        WHERE p.id=?i:pid AND t.moved_to IS NULL';
-            }
-        } else {
-            $vars = [
-                ':tid' => $id,
-                ':uid' => $this->c->user->id,
-            ];
-            if ($this->c->user->isGuest) {
-                $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed, 0 AS mf_mark_all_read, 0 AS mt_last_visit, 0 AS mt_last_read
-                        FROM ::topics AS t
-                        INNER JOIN ::forums AS f ON f.id=t.forum_id
-                        WHERE t.id=?i:tid AND t.moved_to IS NULL';
-
-            } else {
-                $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
-                        FROM ::topics AS t
-                        INNER JOIN ::forums AS f ON f.id=t.forum_id
-                        LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
-                        LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
-                        LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
-                        WHERE t.id=?i:tid AND t.moved_to IS NULL';
-            }
-        }
-
-        $topic = $this->c->DB->query($sql, $vars)->fetch();
-
-        // тема отсутствует или недоступна
-        if (empty($topic)) {
-            return false;
-        }
-
-        list($fTree, $fDesc, $fAsc) = $this->c->forums;
-
-        // раздел отсутствует в доступных
-        if (empty($fDesc[$topic['forum_id']])) {
-            return false;
-        }
+        return $this->view('last', $args);
+    }
 
-        $this->curTopic = $topic;
-        return $topic;
-     }
 
     /**
      * Просмотр темы по номеру сообщения
@@ -208,11 +54,7 @@ class Topic extends Page
      */
     public function viewPost(array $args)
     {
-        $topic = $this->curTopic(null, $args['id']);
-        if (false === $topic) {
-            return $this->c->Message->message('Bad request');
-        }
-        return $this->view($topic, $args['id']);
+        return $this->view('post', $args);
     }
 
     /**
@@ -224,65 +66,83 @@ class Topic extends Page
      */
     public function viewTopic(array $args)
     {
-        $topic = $this->curTopic($args['id']);
-        if (false === $topic) {
-            return $this->c->Message->message('Bad request');
+        return $this->view('topic', $args);
+    }
+
+    /**
+     * @param string $type
+     * @param Models\Topic $topic
+     * 
+     * @param Page
+     */
+    protected function go($type, $topic)
+    {
+        switch ($type) {
+            case 'new':
+                $pid = $topic->post_new;
+                break;
+            case 'unread':
+                $pid = $topic->post_unread;
+                break;
+            case 'last':
+                $pid = $topic->last_post_id;
+                break;
+            default:
+                return $this->c->Message->message('Bad request');
+        }
+        
+        if ($pid) {
+            return $this->c->Redirect->page('ViewPost', ['id' => $pid]);
+        } else {
+            return $this->c->Redirect->page('ViewPost', ['id' => $topic->last_post_id]);
         }
-        $page = isset($args['page']) ? (int) $args['page'] : 1;
-        return $this->view($topic, null, $page);
     }
 
     /**
      * Подготовка данных для шаблона
      * 
-     * @param array $topic
-     * @param int|null $pid
-     * @param int|null $page
+     * @param string $type
+     * @param array $args
      * 
      * @return Page
      */
-     protected function view(array $topic, $pid, $page = null)
-     {
-        $user = $this->c->user;
-
-        if (null === $page) {
-            $vars = [
-                ':tid' => $topic['id'],
-                ':pid' => $pid,
-            ];
-            $sql = 'SELECT COUNT(id) FROM ::posts WHERE topic_id=?i:tid AND id<?i:pid';
-
-            $num = 1 + $this->c->DB->query($sql, $vars)->fetchColumn();
+    protected function view($type, array $args)
+    {
+        $topic = $this->c->ModelTopic->load((int) $args['id'], $type === 'post');
+            
+        if (! $topic->id || ! $topic->last_post_id) {
+            return $this->c->Message->message('Bad request');
+        }
+    
+        if ($topic->moved_to) {
+            return $this->c->Redirect->page('Topic', ['id' => $topic->moved_to]);
+        }
 
-            $page = ceil($num / $user->disp_posts);
+        switch ($type) {
+            case 'topic':
+                $topic->page = isset($args['page']) ? (int) $args['page'] : 1;
+                break;
+            case 'post':
+                $topic->calcPage((int) $args['id']);
+                break;
+            default:
+                return $this->go($type, $topic);
         }
 
-        $pages = ceil(($topic['num_replies'] + 1) / $user->disp_posts);
-        // попытка открыть страницу которой нет
-        if ($page < 1 || $page > $pages) {
+        if (! $topic->hasPage()) {
             return $this->c->Message->message('Bad request');
         }
 
-        $offset = ($page - 1) * $user->disp_posts;
-        $vars = [
-            ':tid'    => $topic['id'],
-            ':offset' => $offset,
-            ':rows'   => $user->disp_posts,
-        ];
-        $sql = 'SELECT id
-                FROM ::posts
-                WHERE topic_id=?i:tid
-                ORDER BY id LIMIT ?i:offset, ?i:rows';
-
-        $ids = $this->c->DB->query($sql, $vars)->fetchAll(\PDO::FETCH_COLUMN);
-
-        // нарушена синхронизация количества сообщений в темах
-        if (empty($ids)) {
-            return $this->viewLast(['id' => $topic['id']]);
+        if (! $posts = $topic->posts()) {
+            return $this->go('last', $topic);
         }
 
         $this->c->Lang->load('topic');
 
+        $user = $this->c->user;
+        
+        
+/*
         list($fTree, $fDesc, $fAsc) = $this->c->forums;
 
         $moders = empty($topic['moderators']) ? [] : array_flip(unserialize($topic['moderators']));
@@ -305,35 +165,6 @@ class Topic extends Page
             $newOn = true;
         }
 
-        // приклейка первого сообщения темы
-        $stickFP = (! empty($topic['stick_fp']) || ! empty($topic['poll_type']));
-        if ($stickFP) {
-            $ids[] = $topic['first_post_id'];
-        }
-
-        $vars = [
-            ':ids' => $ids,
-        ];
-        $sql = 'SELECT id, message, poster, posted
-                FROM ::warnings
-                WHERE id IN (?ai:ids)';
-
-        $warnings = $this->c->DB->query($sql, $vars)->fetchAll(\PDO::FETCH_GROUP);
-
-        $vars = [
-            ':ids' => $ids,
-        ];
-        $sql = 'SELECT u.warning_all, u.gender, u.email, u.title, u.url, u.location, u.signature,
-                       u.email_setting, u.num_posts, u.registered, u.admin_note, u.messages_enable,
-                       p.id, p.poster as username, p.poster_id, p.poster_ip, p.poster_email, p.message,
-                       p.hide_smilies, p.posted, p.edited, p.edited_by, p.edit_post, p.user_agent,
-                       g.g_id, g.g_user_title, g.g_promote_next_group, g.g_pm
-                FROM ::posts AS p
-                INNER JOIN ::users AS u ON u.id=p.poster_id
-                INNER JOIN ::groups AS g ON g.g_id=u.group_id
-                WHERE p.id IN (?ai:ids) ORDER BY p.id';
-
-        $stmt = $this->c->DB->query($sql, $vars);
 
         // парсер и его настройка для сообщений
         $bbcodes = include $this->c->DIR_CONFIG . '/defaultBBCode.php';
@@ -489,14 +320,14 @@ class Topic extends Page
 
         $topic['subject'] = $this->c->censorship->censor($topic['subject']);
 
-
+*/
         // данные для формы быстрого ответа
         $form = null;
-        if ($newOn && $this->c->config->o_quickpost == '1') {
+        if ($topic->post_replies && $this->c->config->o_quickpost == '1') {
             $form = [
-                'action' => $this->c->Router->link('NewReply', ['id' => $topic['id']]),
+                'action' => $this->c->Router->link('NewReply', ['id' => $topic->id]),
                 'hidden' => [
-                    'token' => $this->c->Csrf->create('NewReply', ['id' => $topic['id']]),
+                    'token' => $this->c->Csrf->create('NewReply', ['id' => $topic->id]),
                 ],
                 'sets'   => [],
                 'btns'   => [
@@ -557,54 +388,48 @@ class Topic extends Page
             }
         }
 
-        $this->nameTpl    = 'topic';
-        $this->onlinePos  = 'topic-' . $topic['id'];
-        $this->onlineType = true;
-        $this->canonical  = $this->c->Router->link('Topic', ['id' => $topic['id'], 'name' => $topic['subject'], 'page' => $page]);
-        $this->topic      = $topic;
-        $this->posts      = $posts;
-        $this->signs      = $signs;
-        $this->warnings   = $warnings;
-        $this->crumbs     = $this->crumbs(
-            ['Topic', ['id' => $topic['id'], 'name' => $topic['subject']]],
-            [$fDesc, $topic['forum_id']]
-        );
-        $this->NewReply   = $newOn ? $this->c->Router->link('NewReply', ['id' => $topic['id']]) : $newOn;
-        $this->stickFP    = $stickFP;
-        $this->pages      = $this->c->Func->paginate($pages, $page, 'Topic', ['id' => $topic['id'], 'name' => $topic['subject']]);
-        $this->online     = $this->usersOnlineInfo();
-        $this->stats      = null;
-        $this->form       = $form;
+        $this->nameTpl      = 'topic';
+        $this->onlinePos    = 'topic-' . $topic->id;
+        $this->onlineDetail = true;
+        $this->canonical    = $this->c->Router->link('Topic', ['id' => $topic->id, 'name' => $topic->cens()->subject, 'page' => $topic->page]);
+        $this->topic        = $topic;
+        $this->posts        = $posts;
+        $this->crumbs       = $this->crumbs($topic);
+        $this->NewReply     = $topic->post_replies ? $this->c->Router->link('NewReply', ['id' => $topic->id]) : null;
+        $this->pages        = $topic->pages;
+        $this->online       = $this->c->Online->calc($this)->info();
+        $this->stats        = null;
+        $this->form         = $form;
 
         if ($this->c->config->o_topic_views == '1') {
             $vars = [
-                ':tid' => $topic['id'],
+                ':tid' => $topic->id,
             ];
             $sql = 'UPDATE ::topics SET num_views=num_views+1 WHERE id=?i:tid';
 
             $this->c->DB->query($sql, $vars);
         }
-
+/*
         if (! $user->isGuest) {
             $vars = [
                 ':uid'   => $user->id,
-                ':tid'   => $topic['id'],
-                ':read'  => $topic['mt_last_read'],
-                ':visit' => $topic['mt_last_visit'],
+                ':tid'   => $topic->id,
+                ':read'  => $topic->mt_last_read,
+                ':visit' => $topic->mt_last_visit,
             ];
             $flag = false;
-            $lower = max((int) $user->u_mark_all_read, (int) $topic['mf_mark_all_read'], (int) $topic['mt_last_read']); //????
+            $lower = max((int) $user->u_mark_all_read, (int) $topic->mf_mark_all_read, (int) $topic->mt_last_read); //????
             if ($timeMax > $lower) {
                 $vars[':read'] = $timeMax;
                 $flag = true;
             }
-            $upper = max($lower, (int) $topic['mt_last_visit'], (int) $user->last_visit); //????
-            if ($topic['last_post'] > $upper) {
-                $vars[':visit'] = $topic['last_post'];
+            $upper = max($lower, (int) $topic->mt_last_visit, (int) $user->last_visit); //????
+            if ($topic->last_post > $upper) {
+                $vars[':visit'] = $topic->last_post;
                 $flag = true;
             }
             if ($flag) {
-                if (empty($topic['mt_last_read']) && empty($topic['mt_last_visit'])) {
+                if (empty($topic->mt_last_read) && empty($topic->mt_last_visit)) {
                     $this->c->DB->exec('INSERT INTO ::mark_of_topic (uid, tid, mt_last_visit, mt_last_read)
                                         SELECT ?i:uid, ?i:tid, ?i:visit, ?i:read
                                         FROM ::groups
@@ -619,7 +444,7 @@ class Topic extends Page
                 }
             }
         }
-
+*/
         return $this;
     }
 }

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

@@ -1,61 +0,0 @@
-<?php
-
-namespace ForkBB\Models\Pages;
-
-trait UsersTrait 
-{
-    /**
-     * Имена забаненных пользователей
-     * 
-     * @var array
-     */
-    protected $userBanNames;
-
-    /**
-     * Определение титула для пользователя
-     * 
-     * @param array $data
-     * 
-     * @return string
-     */
-    protected function userGetTitle(array $data) 
-    {
-        if (! isset($this->userBanNames)) {
-            $this->userBanNames = $this->c->bans->userList; //????
-        }
-
-        if (isset($this->userBanNames[mb_strtolower($data['username'])])) { //????
-            return __('Banned');
-        } elseif ($data['title'] != '') {
-            return $data['title'];
-        } elseif ($data['g_user_title'] != '') {
-            return $data['g_user_title'];
-        } elseif ($data['g_id'] == $this->c->GROUP_GUEST) {
-            return __('Guest');
-        } else {
-            return __('Member');
-        }
-    }
-
-    /**
-     * Определение ссылки на аватарку
-     * 
-     * @param int $id
-     * 
-     * @return string|null
-     */
-    protected function userGetAvatarLink($id)
-    {
-        $filetypes = array('jpg', 'gif', 'png');
-    
-        foreach ($filetypes as $type) {
-            $path = $this->c->DIR_PUBLIC . "/{$this->c->config->o_avatars_dir}/{$id}.{$type}";
-
-            if (file_exists($path) && getimagesize($path)) {
-                return $this->c->PUBLIC_URL . "/{$this->c->config->o_avatars_dir}/{$id}.{$type}";
-            }
-        }
-
-        return null;
-    }
-}

+ 80 - 0
app/Models/Post.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\DataModel;
+use ForkBB\Core\Container;
+use RuntimeException;
+
+class Post extends DataModel
+{
+    protected function getlink()
+    {
+        return $this->c->Router->link('ViewPost', ['id' => $this->id]);
+    }
+
+    protected function getuser()
+    {
+        $attrs = $this->a; //????
+        $attrs['id'] = $attrs['poster_id'];
+        return $this->c->ModelUser->setAttrs($attrs);
+    }
+
+    protected function getshowUserLink()
+    {
+        return $this->c->user->g_view_users == '1';
+    }
+
+    protected function getshowUserAvatar()
+    {
+        return $this->c->config->o_avatars == '1' && $this->c->user->show_avatars == '1';
+    }
+
+    protected function getshowUserInfo()
+    {
+        return $this->c->config->o_show_user_info == '1';
+    }
+
+    protected function getshowSignature()
+    {
+        return $this->c->config->o_signatures == '1' && $this->c->user->show_sig == '1';
+    }
+
+    protected function getcontrols()
+    {
+        $user = $this->c->user;
+        $controls = [];
+        $vars = ['id' => $this->id];
+        if (! $user->isAdmin && ! $user->isGuest) {
+            $controls['report'] = [$this->c->Router->link('ReportPost', $vars), 'Report'];
+        }
+        if ($user->isAdmin
+            || ($user->isAdmMod 
+                && ! $this->user->isAdmin
+                && isset($this->parent->parent->moderators[$user->id]) 
+            )
+        ) {
+            $controls['delete'] = [$this->c->Router->link('DeletePost', $vars), 'Delete'];
+            $controls['edit'] = [$this->c->Router->link('EditPost', $vars), 'Edit'];
+        } elseif ($this->parent->closed != '1'
+            && $this->user->id == $user->id
+            && ($user->g_deledit_interval == '0' 
+                || $this->edit_post == '1' 
+                || time() - $this->posted < $user->g_deledit_interval
+            )
+        ) {
+            if (($this->id == $this->parent->first_post_id && $user->g_delete_topics == '1') 
+                || ($this->id != $this->parent->first_post_id && $user->g_delete_posts == '1')
+            ) {
+                $controls['delete'] = [$this->c->Router->link('DeletePost', $vars), 'Delete'];
+            }
+            if ($user->g_edit_posts == '1') {
+                $controls['edit'] = [$this->c->Router->link('EditPost', $vars), 'Edit'];
+            }
+        }
+        if ($this->parent->post_replies) {
+            $controls['quote'] = [$this->c->Router->link('NewReply', ['id' => $this->parent->id, 'quote' => $this->id]), 'Reply'];
+        }
+        return $controls;
+    }
+}

+ 2 - 3
app/Models/Stats.php

@@ -3,6 +3,7 @@
 namespace ForkBB\Models;
 
 use ForkBB\Models\Model;
+use PDO;
 
 class Stats extends Model
 {
@@ -21,9 +22,7 @@ class Stats extends Model
             $this->load();
         }
 
-        list($topics, $posts) = $this->c->DB->query('SELECT SUM(num_topics), SUM(num_posts) FROM ::forums')->fetch(\PDO::FETCH_NUM);
-        $this->postTotal  = $posts;
-        $this->topicTotal = $topics;
+        list($this->topicTotal, $this->postTotal) = $this->c->DB->query('SELECT SUM(num_topics), SUM(num_posts) FROM ::forums')->fetch(PDO::FETCH_NUM);
 
         return $this;
     }

+ 274 - 0
app/Models/Topic.php

@@ -0,0 +1,274 @@
+<?php
+
+namespace ForkBB\Models;
+
+use ForkBB\Models\DataModel;
+use ForkBB\Core\Container;
+use RuntimeException;
+
+class Topic extends DataModel
+{
+    protected function getpost_replies()
+    {
+        if ($this->c->user->isAdmin) {
+            return true;
+        } elseif ($this->closed || $this->c->user->isBot) {
+            return false;
+        } elseif ($this->parent->post_replies == '1'
+            || (null === $this->parent->post_replies && $this->c->user->g_post_replies == '1')
+            || ($this->c->user->isAdmMod && isset($this->parent->moderators[$this->c->user->id]))
+        ) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    protected function getnum_views()
+    {
+        return $this->c->config->o_topic_views == '1' ? $this->a['num_views'] : null;
+    }
+
+    protected function getparent()
+    {
+        return $this->c->forums->forum($this->forum_id);
+    }
+
+    protected function getlink()
+    {
+        if ($this->moved_to) {
+            return $this->c->Router->link('Topic', ['id' => $this->moved_to, 'name' => $this->cens()->subject]);
+        } else {
+            return $this->c->Router->link('Topic', ['id' => $this->id, 'name' => $this->cens()->subject]);
+        }
+    }
+
+    protected function getlink_last()
+    {
+        if ($this->moved_to) {
+            return null;
+        } else {
+            return $this->c->Router->link('ViewPost', ['id' => $this->last_post_id]);
+        }
+    }
+
+    protected function getlink_new()
+    {
+        if ($this->c->user->isGuest || $this->moved_to) {
+            return null;
+        }
+        if ($this->last_post > max(
+            (int) $this->c->user->u_mark_all_read, 
+            (int) $this->parent->mf_mark_all_read,
+            (int) $this->c->user->last_visit, 
+            (int) $this->mt_last_visit)
+        ) {
+            return $this->c->Router->link('TopicViewNew', ['id' => $this->id]);
+        } else {
+            return null;
+        }
+    }
+
+    protected function getpost_new()
+    {
+        if ($this->c->user->isGuest || $this->moved_to) {
+            return null;
+        }
+        $upper = max(
+            (int) $this->c->user->u_mark_all_read, 
+            (int) $this->parent->mf_mark_all_read,
+            (int) $this->c->user->last_visit, 
+            (int) $this->mt_last_visit
+        );
+        if ($this->last_post > $upper) {
+            $vars = [
+                ':tid'   => $this->id,
+                ':visit' => $upper,
+            ];
+            $sql = 'SELECT MIN(id) FROM ::posts WHERE topic_id=?i:tid AND posted>?i:visit';
+
+            $pid = $this->c->DB->query($sql, $vars)->fetchColumn();
+
+            if (! empty($pid)) {
+                return $pid;
+            }
+        }
+
+        return null;
+    }
+
+    protected function getlink_unread()
+    {
+        if ($this->c->user->isGuest || $this->moved_to) {
+            return null;
+        }
+        if ($this->last_post > max(
+            (int) $this->c->user->u_mark_all_read, 
+            (int) $this->parent->mf_mark_all_read,
+            (int) $this->mt_last_read)
+        ) {
+            return $this->c->Router->link('TopicViewUnread', ['id' => $this->id]);
+        } else {
+            return null;
+        }
+    }
+
+    protected function getpost_unread()
+    {
+        if ($this->c->user->isGuest || $this->moved_to) {
+            return null;
+        }
+        $lower = max(
+            (int) $this->c->user->u_mark_all_read, 
+            (int) $this->parent->mf_mark_all_read,
+            (int) $this->mt_last_read
+        );
+        if ($this->last_post > $lower) {
+            $vars = [
+                ':tid'   => $this->id,
+                ':visit' => $lower,
+            ];
+            $sql = 'SELECT MIN(id) FROM ::posts WHERE topic_id=?i:tid AND posted>?i:visit';
+
+            $pid = $this->c->DB->query($sql, $vars)->fetchColumn();
+
+            if (! empty($pid)) {
+                return $pid;
+            }
+        }
+
+        return null;
+    }
+
+    protected function getnum_pages()
+    {
+        if (null === $this->num_replies) {
+            throw new RuntimeException('The model does not have the required data');
+        }
+
+        return (int) ceil(($this->num_replies + 1) / $this->c->user->disp_posts);
+    }
+
+    /**
+     * @returm array
+     */
+    protected function getpages()
+    {
+        $page = (int) $this->page;
+        if ($page < 1 && $this->num_pages === 1) {
+            return [];
+        } else {
+            return $this->c->Func->paginate($this->num_pages, $page, 'Topic', ['id' => $this->id, 'name' => $this->cens()->subject]);
+        }
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasPage()
+    {
+        return $this->page > 0 && $this->page <= $this->num_pages;
+    }
+
+    /**
+     * @param int $pid
+     */
+    public function calcPage($pid)
+    {
+        $vars = [
+            ':tid' => $this->id,
+            ':pid' => $pid,
+        ];
+        $sql = 'SELECT COUNT(p.id) AS num, j.id AS flag
+                FROM ::posts AS p
+                INNER JOIN ::posts AS j ON (j.topic_id=?i:tid AND j.id=?i:pid)
+                WHERE p.topic_id=?i:tid AND p.id<?i:pid';
+
+        $result = $this->c->DB->query($sql, $vars)->fetch();
+
+        $this->page = empty($result['flag']) ? null : (int) ceil(($result['num'] + 1) / $this->c->user->disp_posts);
+    }
+
+    /**
+     * @return array
+     */
+    public function posts()
+    {
+        if (! $this->hasPage()) {
+            throw new RuntimeException('The model does not have the required data');
+        }
+
+        $offset = ($this->page - 1) * $this->c->user->disp_posts;
+        $vars = [
+            ':tid'    => $this->id,
+            ':offset' => $offset,
+            ':rows'   => $this->c->user->disp_posts,
+        ];
+        $sql = 'SELECT id
+                FROM ::posts
+                WHERE topic_id=?i:tid
+                ORDER BY id LIMIT ?i:offset, ?i:rows';
+
+        $ids = $this->c->DB->query($sql, $vars)->fetchAll(\PDO::FETCH_COLUMN);
+        if (empty($ids)) {
+            return [];
+        }
+
+        // приклейка первого сообщения темы
+        if ($this->stick_fp || $this->poll_type) {
+            $ids[] = $this->first_post_id;
+        }
+
+        $vars = [
+            ':ids' => $ids,
+        ];
+        $sql = 'SELECT id, message, poster, posted
+                FROM ::warnings
+                WHERE id IN (?ai:ids)';
+
+        $warnings = $this->c->DB->query($sql, $vars)->fetchAll(\PDO::FETCH_GROUP);
+
+        $vars = [
+            ':ids' => $ids,
+        ];
+        $sql = 'SELECT u.warning_all, u.gender, u.email, u.title, u.url, u.location, u.signature,
+                       u.email_setting, u.num_posts, u.registered, u.admin_note, u.messages_enable,
+                       u.group_id,
+                       p.id, p.poster as username, p.poster_id, p.poster_ip, p.poster_email, p.message,
+                       p.hide_smilies, p.posted, p.edited, p.edited_by, p.edit_post, p.user_agent,
+                       g.g_user_title, g.g_promote_next_group, g.g_pm
+                FROM ::posts AS p
+                INNER JOIN ::users AS u ON u.id=p.poster_id
+                INNER JOIN ::groups AS g ON g.g_id=u.group_id
+                WHERE p.id IN (?ai:ids) ORDER BY p.id';
+
+        $posts = $this->c->DB->query($sql, $vars)->fetchAll();
+
+        $postCount = 0;
+        $timeMax = 0;
+
+        foreach ($posts as &$cur) {
+            if ($cur['posted'] > $timeMax) {
+                $timeMax = $cur['posted'];
+            }
+
+            // номер сообшения в теме
+            if ($cur['id'] == $this->first_post_id && $offset > 0) {
+                $cur['postNumber'] = 1;
+            } else {
+                ++$postCount;
+                $cur['postNumber'] = $offset + $postCount;
+            }
+
+            if (isset($warnings[$cur['id']])) {
+                $cur['warnings'] = $warnings[$cur['id']];
+            }
+
+            $cur['parent'] = $this;
+
+            $cur = $this->c->ModelPost->setAttrs($cur);
+        }
+        unset($cur);
+        return $posts;
+    }
+}

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

@@ -0,0 +1,93 @@
+<?php
+
+namespace ForkBB\Models\Topic;
+
+use ForkBB\Models\MethodModel;
+
+class Load extends MethodModel
+{
+    /**
+     * Заполняет модель данными из БД
+     *
+     * @param int $id
+     * @param bool $isPost
+     *
+     * @return Topic
+     */
+    public function load($id, $isPost = false)
+    {
+        if ($isPost) {
+            $vars = [
+                ':pid' => $id,
+                ':uid' => $this->c->user->id,
+            ];
+            if ($this->c->user->isGuest) {
+                $sql = 'SELECT t.*, f.moderators
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        INNER JOIN ::posts AS p ON t.id=p.topic_id
+                        WHERE p.id=?i:pid';
+
+            } else {
+                $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        INNER JOIN ::posts AS p ON t.id=p.topic_id
+                        LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
+                        LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
+                        LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
+                        WHERE p.id=?i:pid';
+            }
+        } else {
+            $vars = [
+                ':tid' => $id,
+                ':uid' => $this->c->user->id,
+            ];
+            if ($this->c->user->isGuest) {
+                $sql = 'SELECT t.*, f.moderators
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        WHERE t.id=?i:tid';
+
+            } else {
+                $sql = 'SELECT t.*, f.moderators, s.user_id AS is_subscribed, mof.mf_mark_all_read, mot.mt_last_visit, mot.mt_last_read
+                        FROM ::topics AS t
+                        INNER JOIN ::forums AS f ON f.id=t.forum_id
+                        LEFT JOIN ::topic_subscriptions AS s ON (t.id=s.topic_id AND s.user_id=?i:uid)
+                        LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND f.id=mof.fid)
+                        LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND t.id=mot.tid)
+                        WHERE t.id=?i:tid';
+            }
+        }
+
+        $data = $this->c->DB->query($sql, $vars)->fetch();
+
+        // тема отсутствует или недоступна
+        if (empty($data)) {
+            return $this->emptyTopic();
+        }
+
+        $forForum['moderators'] = $data['moderators'];
+        unset($data['moderators']);
+        if (! $this->c->user->isGuest) {
+            $forForum['mf_mark_all_read'] = $data['mf_mark_all_read'];
+            unset($data['mf_mark_all_read']);
+        }
+        $this->model->setAttrs($data);
+        $forum = $this->model->parent;
+
+        // раздел не доступен
+        if (empty($forum)) {
+            return $this->emptyTopic();
+        }
+
+        $forum->replAttrs($forForum);
+
+        return $this->model;
+    }
+
+    protected function emptyTopic()
+    {
+        return $this->model->setAttrs([]);
+    }
+}

+ 50 - 11
app/Models/User.php

@@ -16,49 +16,48 @@ class User extends DataModel
     /**
      * Конструктор
      *
-     * @param array $data
      * @param Container $container
      */
-    public function __construct(array $data = [], Container $container)
+    public function __construct(Container $container)
     {
         $this->now = time();
-        parent::__construct($data, $container);
+        parent::__construct($container);
     }
 
-    protected function getIsUnverified()
+    protected function getisUnverified()
     {
         return $this->group_id == $this->c->GROUP_UNVERIFIED;
     }
 
-    protected function getIsGuest()
+    protected function getisGuest()
     {
         return $this->group_id == $this->c->GROUP_GUEST
             || $this->id < 2
             || $this->group_id == $this->c->GROUP_UNVERIFIED;
     }
 
-    protected function getIsAdmin()
+    protected function getisAdmin()
     {
         return $this->group_id == $this->c->GROUP_ADMIN;
     }
 
-    protected function getIsAdmMod()
+    protected function getisAdmMod()
     {
         return $this->group_id == $this->c->GROUP_ADMIN
             || $this->g_moderator == '1';
     }
 
-    protected function getLogged()
+    protected function getlogged()
     {
         return empty($this->a['logged']) ? $this->now : $this->a['logged'];
     }
 
-    protected function getIsLogged()
+    protected function getisLogged()
     {
         return ! empty($this->a['logged']);
     }
 
-    protected function getLanguage()
+    protected function getlanguage()
     {
         $langs = $this->c->Func->getLangs();
 
@@ -73,7 +72,7 @@ class User extends DataModel
         }
     }
 
-    protected function getStyle()
+    protected function getstyle()
     {
         $styles = $this->c->Func->getStyles();
 
@@ -87,4 +86,44 @@ class User extends DataModel
             return isset($styles[0]) ? $styles[0] : 'ForkBB';
         }
     }
+
+    protected function getlink()
+    {
+        return $this->c->Router->link('User', ['id' => $this->id, 'name' => $this->username]);
+    }
+
+    protected function getavatar()
+    {
+        $filetypes = array('jpg', 'gif', 'png');
+            
+        foreach ($filetypes as $type) {
+            $path = $this->c->DIR_PUBLIC . "/{$this->c->config->o_avatars_dir}/{$this->id}.{$type}";
+        
+            if (file_exists($path) && getimagesize($path)) {
+                return $this->c->PUBLIC_URL . "/{$this->c->config->o_avatars_dir}/{$this->id}.{$type}";
+            }
+        }
+    
+        return null;
+    }
+
+    public function title()
+    {
+        if (isset($this->c->bans->userList[mb_strtolower($this->username)])) { //????
+            return __('Banned');
+        } elseif ($this->title != '') {
+            return $this->cens()->title;
+        } elseif ($this->g_user_title != '') {
+            return $this->cens()->g_user_title;
+        } elseif ($this->isGuest) {
+            return __('Guest');
+        } else {
+            return __('Member');
+        }
+    }
+
+    protected function getonline()
+    {
+        return isset($this->c->Online->online[$this->id]);
+    }
 }

+ 4 - 4
app/Models/User/LoadUserFromCookie.php

@@ -78,13 +78,13 @@ class LoadUserFromCookie extends MethodModel
         $data = null;
         $ip = $this->getIp();
         if ($id > 1) {
-            $data = $this->c->DB->query('SELECT u.*, g.*, o.logged, o.idle FROM ::users AS u INNER JOIN ::groups AS g ON u.group_id=g.g_id LEFT JOIN ::online AS o ON o.user_id=u.id WHERE u.id=?i:id', [':id' => $id])->fetch();
+            $data = $this->c->DB->query('SELECT u.*, g.*, o.logged FROM ::users AS u INNER JOIN ::groups AS g ON u.group_id=g.g_id LEFT JOIN ::online AS o ON o.user_id=u.id WHERE u.id=?i:id', [':id' => $id])->fetch();
         }
         if (empty($data['id'])) {
             $data = $this->c->DB->query('SELECT u.*, g.*, o.logged, o.last_post, o.last_search FROM ::users AS u INNER JOIN ::groups AS g ON u.group_id=g.g_id LEFT JOIN ::online AS o ON (o.user_id=1 AND o.ident=?s:ip) WHERE u.id=1', [':ip' => $ip])->fetch();
-        }
-        if (empty($data['id'])) {
-            throw new RuntimeException('Unable to fetch guest information. Your database must contain both a guest user and a guest user group.');
+            if (empty($data['id'])) {
+                throw new RuntimeException('Unable to fetch guest information. Your database must contain both a guest user and a guest user group');
+            }
         }
         $this->model->setAttrs($data);
         $this->model->__ip = $ip;

+ 2 - 2
app/bootstrap.php

@@ -26,7 +26,7 @@ if (file_exists(__DIR__ . '/config/main.php')) {
 } elseif (file_exists(__DIR__ . '/config/install.php')) {
     $c = new Container(include __DIR__ . '/config/install.php');
 } else {
-    throw new RuntimeException('Application is not configured.');
+    throw new RuntimeException('Application is not configured');
 }
 
 require __DIR__ . '/functions.php';
@@ -60,7 +60,7 @@ if (null !== $page->onlinePos) {
     $c->Online->calc($page);
 }
 $tpl = $c->View->rendering($page);
-if ($tpl !== null && $c->DEBUG > 0) {
+if (null !== $tpl && $c->DEBUG > 0) {
     $debug = $c->View->rendering($c->Debug->debug());
     $tpl = str_replace('<!-- debuginfo -->', $debug, $tpl);
 }

+ 1 - 1
app/lang/English/admin_index.po

@@ -79,7 +79,7 @@ msgid "Server load label"
 msgstr "Server load"
 
 msgid "Server load data"
-msgstr "%s - %s user(s) online"
+msgstr "%1$s - %2$s visitor(s) online"
 
 msgid "Environment label"
 msgstr "Environment"

+ 1 - 1
app/lang/Russian/admin_groups.po

@@ -262,4 +262,4 @@ msgid "Report flood help"
 msgstr "Количество секунд, которые необходимо подождать до отправления следующего сигнала. Поставьте 0 чтобы выключить ограничение."
 
 msgid "Moderator info"
-msgstr "Пожалуйста обратите внимание, что пока пользователь не назначен модератором конкретного раздела, он не сможет реализовать свои модераторские права. Назначение производится в разделе "Модерация" пользовательского профиля."
+msgstr "Пожалуйста, обратите внимание, что пока пользователь не назначен модератором конкретного раздела, он не сможет реализовать свои модераторские права. Назначение производится в разделе "Модерация" пользовательского профиля."

+ 1 - 1
app/lang/Russian/admin_index.po

@@ -79,7 +79,7 @@ msgid "Server load label"
 msgstr "Загрузка сервера"
 
 msgid "Server load data"
-msgstr "%s - %s пользователей online"
+msgstr "%1$s - %2$s посетитель(ей)"
 
 msgid "Environment label"
 msgstr "Окружение"

+ 29 - 35
app/templates/forum.tpl

@@ -34,14 +34,13 @@
         </nav>
 @endsection
 @extends('layouts/main')
-@if($p->forums)
+@if($forums = $p->forums)
     <div class="f-nav-links">
 @yield('crumbs')
     </div>
     <section class="f-subforums">
       <ol class="f-ftlist">
-@foreach($p->forums as $id => $cat)
-        <li id="id-subforums{!! $id !!}" class="f-category">
+        <li id="id-subforums{!! $p->forum->id !!}" class="f-category">
           <h2>{{ __('Sub forum', 2) }}</h2>
           <ol class="f-table">
             <li class="f-row f-thead" value="0">
@@ -52,7 +51,6 @@
 @include('layouts/subforums')
           </ol>
         </li>
-@endforeach
       </ol>
     </section>
 @endif
@@ -65,13 +63,9 @@
       </div>
 @endif
     </div>
-@if(!$p->topics)
-    <section class="f-main f-message">
-      <h2>{!! __('Empty forum') !!}</h2>
-    </section>
-@else
+@if($p->topics)
     <section class="f-main f-forum">
-      <h2>{{ $p->forumName }}</h2>
+      <h2>{{ $p->forum->forum_name }}</h2>
       <div class="f-ftlist">
         <ol class="f-table">
           <li class="f-row f-thead" value="0">
@@ -80,50 +74,50 @@
             <div class="f-hcell f-clast">{!! __('Last post') !!}</div>
           </li>
 @foreach($p->topics as $topic)
-@if($topic['moved_to'])
-          <li id="topic-{!! $topic['id']!!}" class="f-row f-fredir">
+@if($topic->moved_to)
+          <li id="topic-{!! $topic->id !!}" 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">{!! __('Moved') !!}</span> <a class="f-ftname" href="{!! $topic['link'] !!}">{{ $topic['subject'] }}</a></h3>
+                <h3><span class="f-fredirtext">{!! __('Moved') !!}</span> <a class="f-ftname" href="{!! $topic->link !!}">{{ $topic->cens()->subject }}</a></h3>
               </div>
             </div>
           </li>
 @else
-          <li id="topic-{!! $topic['id'] !!}" class="f-row<!-- inline -->
-@if($topic['link_new']) f-fnew
+          <li id="topic-{!! $topic->id !!}" class="f-row<!-- inline -->
+@if($topic->link_new) f-fnew
 @endif
-@if($topic['link_unread']) f-funread
+@if($topic->link_unread) f-funread
 @endif
-@if($topic['sticky']) f-fsticky
+@if($topic->sticky) f-fsticky
 @endif
-@if($topic['closed']) f-fclosed
+@if($topic->closed) f-fclosed
 @endif
-@if($topic['poll_type']) f-fpoll
+@if($topic->poll_type) f-fpoll
 @endif
-@if($topic['dot']) f-fposted
+@if($topic->dot) f-fposted
 @endif
             "><!-- endinline -->
             <div class="f-cell f-cmain">
               <div class="f-ficon"></div>
               <div class="f-finfo">
                 <h3>
-@if($topic['dot'])
+@if($topic->dot)
                   <span class="f-tdot">·</span>
 @endif
-@if($topic['sticky'])
+@if($topic->sticky)
                   <span class="f-stickytxt">{!! __('Sticky') !!}</span>
 @endif
-@if($topic['closed'])
+@if($topic->closed)
                   <span class="f-closedtxt">{!! __('Closed') !!}</span>
 @endif
-@if($topic['poll_type'])
+@if($topic->poll_type)
                   <span class="f-polltxt">{!! __('Poll') !!}</span>
 @endif
-                  <a class="f-ftname" href="{!! $topic['link'] !!}">{{ $topic['subject'] }}</a>
-@if($topic['pages'])
+                  <a class="f-ftname" href="{!! $topic->link !!}">{{ $topic->cens()->subject }}</a>
+@if($topic->pages)
                   <span class="f-tpages">
-@foreach($topic['pages'] as $cur)
+@foreach($topic->pages as $cur)
 @if($cur[1] === 'space')
                     <span class="f-page f-pspacer">{!! __('Spacer') !!}</span>
 @else
@@ -132,25 +126,25 @@
 @endforeach
                   </span>
 @endif
-@if($topic['link_new'])
-                  <span class="f-newtxt"><a href="{!! $topic['link_new'] !!}" title="{!! __('New posts info') !!}">{!! __('New posts') !!}</a></span>
+@if($topic->link_new)
+                  <span class="f-newtxt"><a href="{!! $topic->link_new !!}" title="{!! __('New posts info') !!}">{!! __('New posts') !!}</a></span>
 @endif
                 </h3>
-                <p class="f-cmposter">{!! __('by') !!} {{ $topic['poster'] }}</p>
+                <p class="f-cmposter">{!! __('by') !!} {{ $topic->poster }}</p>
               </div>
             </div>
             <div class="f-cell f-cstats">
               <ul>
-                <li>{!! __('%s Reply', $topic['num_replies'], $topic['replies']) !!}</li>
-@if($topic['views'])
-                <li>{!! __('%s View', $topic['num_views'], $topic['views']) !!}</li>
+                <li>{!! __('%s Reply', $topic->num_replies, $topic->num()->num_replies) !!}</li>
+@if($topic->num_views)
+                <li>{!! __('%s View', $topic->num_views, $topic->num()->num_views) !!}</li>
 @endif
               </ul>
             </div>
             <div class="f-cell f-clast">
               <ul>
-                <li class="f-cltopic"><a href="{!! $topic['link_last'] !!}" title="&quot;{{ $topic['subject'] }}&quot; - {!! __('Last post') !!}">{{ $topic['last_post'] }}</a></li>
-                <li class="f-clposter">{!! __('by') !!} {{ $topic['last_poster'] }}</li>
+                <li class="f-cltopic"><a href="{!! $topic->link_last !!}" title="&quot;{{ $topic->cens()->subject }}&quot; - {!! __('Last post') !!}">{{ $topic->dt()->last_post }}</a></li>
+                <li class="f-clposter">{!! __('by') !!} {{ $topic->last_poster }}</li>
               </ul>
             </div>
           </li>

+ 3 - 7
app/templates/index.tpl

@@ -1,10 +1,10 @@
 @extends('layouts/main')
-@if($p->forums)
+@if($p->categoryes)
     <section class="f-main">
       <ol class="f-ftlist">
-@foreach($p->forums as $id => $cat)
+@foreach($p->categoryes as $id => $forums)
         <li id="cat-{!! $id !!}" class="f-category">
-          <h2>{{ $cat['name'] }}</h2>
+          <h2>{{ current($forums)->cat_name }}</h2>
           <ol class="f-table">
             <li class="f-row f-thead" value="0">
               <div class="f-hcell f-cmain">{!! __('Forum') !!}</div>
@@ -17,9 +17,5 @@
 @endforeach
       </ol>
     </section>
-@else
-    <section class="f-main f-message">
-      <h2>{!! __('Empty board') !!}</h2>
-    </section>
 @endif
 @include('layouts/stats')

+ 13 - 11
app/templates/layouts/stats.tpl

@@ -4,29 +4,31 @@
 @if($p->stats)
         <dl class="right">
           <dt>{!! __('Board stats') !!}</dt>
-          <dd>{!! __('No of users') !!} <strong>{!! $p->stats['total_users'] !!}</strong></dd>
-          <dd>{!! __('No of topics') !!} <strong>{!! $p->stats['total_topics'] !!}</strong></dd>
-          <dd>{!! __('No of posts') !!} <strong>{!! $p->stats['total_posts'] !!}</strong></dd>
+          <dd>{!! __('No of users') !!} <strong>{!! $p->stats->num()->userTotal !!}</strong></dd>
+          <dd>{!! __('No of topics') !!} <strong>{!! $p->stats->num()->topicTotal !!}</strong></dd>
+          <dd>{!! __('No of posts') !!} <strong>{!! $p->stats->num()->postTotal !!}</strong></dd>
         </dl>
 @endif
         <dl class="left">
           <dt>{!! __('User info') !!}</dt>
-@if($p->stats && is_string($p->stats['newest_user']))
-          <dd>{!! __('Newest user')  !!} {{ $p->stats['newest_user'] }}</dd>
-@elseif($p->stats)
-          <dd>{!! __('Newest user')  !!} <a href="{!! $p->stats['newest_user'][0] !!}">{{ $p->stats['newest_user'][1] }}</a></dd>
+@if($p->stats)
+@if(is_string($p->stats->userLast))
+          <dd>{!! __('Newest user')  !!} {{ $p->stats->userLast }}</dd>
+@else
+          <dd>{!! __('Newest user')  !!} <a href="{!! $p->stats->userLast[0] !!}">{{ $p->stats->userLast[1] }}</a></dd>
+@endif
 @endif
 @if($p->online)
-          <dd>{!! __('Visitors online', $p->online['number_of_users'], $p->online['number_of_guests']) !!}</dd>
+          <dd>{!! __('Visitors online', $p->online->num()->numUsers, $p->online->num()->numGuests) !!}</dd>
 @endif
 @if($p->stats)
-          <dd>{!! __('Most online', $p->online['max'], $p->online['max_time']) !!}</dd>
+          <dd>{!! __('Most online', $p->online->num()->maxNum, $p->online->dt()->maxTime) !!}</dd>
 @endif
         </dl>
-@if($p->online && $p->online['list'])
+@if($p->online && $p->online->info)
         <dl class="f-inline f-onlinelist"><!-- inline -->
           <dt>{!! __('Online users') !!}</dt>
-@foreach($p->online['list'] as $cur)
+@foreach($p->online->info as $cur)
 @if(is_string($cur))
           <dd>{{ $cur }}</dd>
 @else

+ 26 - 26
app/templates/layouts/subforums.tpl

@@ -1,46 +1,46 @@
-@foreach($cat['forums'] as $cur)
-@if($cur['redirect_url'])
-            <li id="forum-{!! $cur['fid']!!}" class="f-row f-fredir">
+@foreach($forums as $cur)
+@if($cur->redirect_url)
+            <li id="forum-{!! $cur->id !!}" 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 class="f-ftname" href="{!! $cur['redirect_url'] !!}">{{ $cur['forum_name'] }}</a></h3>
-@if($cur['forum_desc'])
-                  <p class="f-fdesc">{!! $cur['forum_desc'] !!}</p>
+                  <h3><span class="f-fredirtext">{!! __('Link to') !!}</span> <a class="f-ftname" 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">
+@if($cur->tree->newMessages)
+            <li id="forum-{!! $cur->id !!}" class="f-row f-fnew">
 @else
-            <li id="forum-{!! $cur['fid'] !!}" class="f-row">
+            <li id="forum-{!! $cur->id !!}" class="f-row">
 @endif
               <div class="f-cell f-cmain">
                 <div class="f-ficon"></div>
                 <div class="f-finfo">
                   <h3>
-                    <a class="f-ftname" href="{!! $cur['forum_link'] !!}">{{ $cur['forum_name'] }}</a>
-@if($cur['new'])
+                    <a class="f-ftname" href="{!! $cur->link !!}">{{ $cur->forum_name }}</a>
+@if($cur->tree->newMessages)
                     <span class="f-newtxt"><a href="">{!! __('New posts') !!}</a></span>
 @endif
                   </h3>
-@if($cur['subforums'])
+@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>
+                    <dt>{!! __('Sub forum', count($cur->subforums)) !!}</dt>
+@foreach($cur->subforums as $sub)
+                    <dd><a href="{!! $sub->link !!}">{{ $sub->forum_name }}</a></dd>
 @endforeach
                   </dl><!-- endinline -->
 @endif
-@if($cur['forum_desc'])
-                  <p class="f-fdesc">{!! $cur['forum_desc'] !!}</p>
+@if($cur->forum_desc)
+                  <p class="f-fdesc">{!! $cur->forum_desc !!}</p>
 @endif
-@if($cur['moderators'])
+@if($cur->moderators)
                   <dl class="f-inline f-modlist"><!-- inline -->
-                    <dt>{!! __('Moderated by', count($cur['moderators'])) !!}</dt>
-@foreach($cur['moderators'] as $mod)
+                    <dt>{!! __('Moderated by', count($cur->moderators)) !!}</dt>
+@foreach($cur->moderators as $mod)
 @if(is_string($mod))
                     <dd>{{ $mod }}</dd>
 @else
@@ -53,16 +53,16 @@
               </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>
+                  <li>{!! __('%s Topic', $cur->tree->num_topics, $cur->tree->num()->num_topics) !!}</li>
+                  <li>{!! __('%s Post', $cur->tree->num_posts, $cur->tree->num()->num_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>
+@if($cur->tree->last_post_id)
+                  <li class="f-cltopic"><a href="{!! $cur->tree->last_post_link !!}" title="&quot;{{ $cur->tree->cens()->last_topic }}&quot; - {!! __('Last post') !!}">{{ $cur->tree->cens()->last_topic }}</a></li>
+                  <li class="f-clposter">{!! __('by') !!} {{ $cur->tree->last_poster }}</li>
+                  <li class="f-cltime">{!! $cur->tree->dt()->last_post !!}</li>
 @else
                   <li class="f-cltopic">{!! __('Never') !!}</li>
 @endif

+ 32 - 26
app/templates/topic.tpl

@@ -10,9 +10,9 @@
       </ul>
 @endsection
 @section('linkpost')
-@if($p->NewReply !== null)
+@if($p->topic->post_replies || $p->topic->closed)
         <div class="f-link-post">
-@if($p->NewReply === false)
+@if($p->topic->closed)
           __('Topic closed')
 @else
           <a class="f-btn" href="{!! $p->NewReply !!}">{!! __('Post reply') !!}</a>
@@ -40,7 +40,7 @@
 @extends('layouts/main')
     <div class="f-nav-links">
 @yield('crumbs')
-@if($p->NewReply || $p->pages)
+@if($p->topic->post_replies || $p->topic->closed || $p->pages)
       <div class="f-links-b clearfix">
 @yield('pages')
 @yield('linkpost')
@@ -48,49 +48,55 @@
 @endif
     </div>
     <section class="f-main f-topic">
-      <h2>{{ $p->topic['subject'] }}</h2>
+      <h2>{{ $p->topic->cens()->subject }}</h2>
 @foreach($p->posts as $post)
-      <article id="p{!! $post['id'] !!}" class="f-post{!! $post['poster_gender'].$post['poster_online'] !!} clearfix">
+      <article id="p{!! $post->id !!}" class="clearfix f-post<!-- inline -->
+@if($post->user->gender == 1) f-user-male
+@elseif($post->user->gender == 2) f-user-female
+@endif
+@if($post->user->online) f-user-online
+@endif
+      "><!-- endinline -->
         <header class="f-post-header clearfix">
-          <h3>{{ $p->topic['subject'] }} - #{!! $post['post_number'] !!}</h3>
-          <span class="left"><time datetime="{{ $post['posted_utc'] }}">{{ $post['posted'] }}</time></span>
-          <span class="right"><a href="{!! $post['link'] !!}" rel="bookmark">#{!! $post['post_number'] !!}</a></span>
+          <h3>{{ $p->topic->cens()->subject }} - #{!! $post->postNumber !!}</h3>
+          <span class="left"><time datetime="{{ $post->utc()->posted }}">{{ $post->dt()->posted }}</time></span>
+          <span class="right"><a href="{!! $post->link !!}" rel="bookmark">#{!! $post->postNumber !!}</a></span>
         </header>
         <div class="f-post-body clearfix">
           <address class="f-post-left clearfix">
             <ul class="f-user-info">
-@if($post['poster_link'])
-              <li class="f-username"><a href="{!! $post['poster_link'] !!}">{{ $post['poster'] }}</a></li>
+@if($post->showUserLink)
+              <li class="f-username"><a href="{!! $post->user->link !!}">{{ $post->user->username }}</a></li>
 @else
-              <li class="f-username">{{ $post['poster'] }}</li>
+              <li class="f-username">{{ $post->user->username }}</li>
 @endif
-@if($post['poster_avatar'])
+@if($post->showUserAvatar)
               <li class="f-avatar">
-                <img alt="{{ $post['poster'] }}" src="{!! $post['poster_avatar'] !!}">
+                <img alt="{{ $post->user->username }}" src="{!! $post->user->avatar !!}">
               </li>
 @endif
-              <li class="f-usertitle"><span>{{$post['poster_title']}}</span></li>
-@if($post['poster_posts'])
-              <li class="f-postcount"><span>{!! __('%s post', $post['poster_num_posts'], $post['poster_posts']) !!}</span></li>
+              <li class="f-usertitle"><span>{{ $post->user->title() }}</span></li>
+@if($post->showUserInfo)
+              <li class="f-postcount"><span>{!! __('%s post', $post->user->num_posts, $post->user->num()->num_posts) !!}</span></li>
 @endif
             </ul>
-@if($post['poster_info_add'])
+@if($post->showUserInfo)
             <ul class="f-user-info-add">
-              <li><span>{!! __('Registered:') !!} {{ $post['poster_registered'] }}</span></li>
-@if($post['poster_location'])
-              <li><span>{!! __('From') !!} {{ $post['poster_location'] }}</span></li>
+              <li><span>{!! __('Registered:') !!} {{ $post->user->dt(true)->registered }}</span></li>
+@if($post->user->location)
+              <li><span>{!! __('From') !!} {{ $post->user->cens()->location }}</span></li>
 @endif
               <li><span></span></li>
             </ul>
 @endif
           </address>
           <div class="f-post-right f-post-main">
-            {!! $post['message'] !!}
+            {!! $post->message !!}
           </div>
-@if(isset($p->signs[$post['poster_id']]))
+@if($post->showSignature && $post->user->signature)
           <div class="f-post-right f-post-signature">
             <hr>
-            {!! $p->signs[$post['poster_id']] !!}
+            {!! $post->user->signature !!}
           </div>
 @endif
         </div>
@@ -98,10 +104,10 @@
           <div class="f-post-left">
             <span></span>
           </div>
-@if($post['controls'])
+@if($post->controls)
           <div class="f-post-right clearfix">
             <ul>
-@foreach($post['controls'] as $key => $control)
+@foreach($post->controls as $key => $control)
               <li class="f-post{!! $key !!}"><a class="f-btn" href="{!! $control[0] !!}">{!! __($control[1]) !!}</a></li>
 @endforeach
             </ul>
@@ -112,7 +118,7 @@
 @endforeach
     </section>
     <div class="f-nav-links">
-@if($p->NewReply || $p->pages)
+@if($p->topic->post_replies || $p->topic->closed || $p->pages)
       <div class="f-links-a clearfix">
 @yield('linkpost')
 @yield('pages')

+ 1 - 1
composer.json

@@ -19,6 +19,6 @@
     "require": {
         "php": ">=5.6.0",
         "artoodetoo/dirk": "dev-master",
-        "MioVisman/Parserus": "^0.9.1"
+        "miovisman/parserus": "dev-master"
     }
 }