Visman 7 سال پیش
والد
کامیت
9becae3750

+ 21 - 16
app/Controllers/Routing.php

@@ -72,23 +72,28 @@ class Routing
             // юзеры
             if ($user->gViewUsers == '1') {
                 // список пользователей
-                $r->add('GET', '/userlist[/page/{page:[1-9]\d*}]', 'Userlist:view', 'Userlist');
+                $r->add('GET', '/userlist[/{page:[1-9]\d*}]', 'Userlist:view', 'Userlist');
                 // юзеры
-                $r->add('GET', '/user/{id:[1-9]\d*}[/{name}]', 'Profile:view', 'User'); //????
+                $r->add('GET', '/user/{id:[1-9]\d*}/{name}', 'Profile:view', 'User'); //????
             }
 
             // разделы
-            $r->add('GET', '/forum/{id:[1-9]\d*}/new/topic', 'Post:newTopic', 'NewTopic');
-            $r->add('GET', '/forum/{id:[1-9]\d*}[/{name}][/page/{page:[1-9]\d*}]', 'Forum:view', 'Forum');
+            $r->add('GET',  '/forum/{id:[1-9]\d*}/{name}[/{page:[1-9]\d*}]', 'Forum:view', 'Forum');
+            $r->add('GET',  '/forum/{id:[1-9]\d*}/new/topic', 'Post:newTopic', 'NewTopic');
+            $r->add('POST', '/forum/{id:[1-9]\d*}/new/topic', 'Post:newTopicPost');
             // темы
-            $r->add('GET', '/topic/{id:[1-9]\d*}[/{name}][/page/{page:[1-9]\d*}]', 'Topic:viewTopic', 'Topic');
-            $r->add('GET', '/topic/{id:[1-9]\d*}/goto/new', 'Topic:goToNew', 'TopicGoToNew');
-            $r->add('GET', '/topic/{id:[1-9]\d*}/goto/unread', 'Topic:goToUnread', 'TopicGoToUnread');
-            $r->add('GET', '/topic/{id:[1-9]\d*}/goto/last', 'Topic:goToLast', 'TopicGoToLast');
-            $r->add('GET', '/topic/{id:[1-9]\d*}/new/post', 'Post:new', 'NewPost');
+            $r->add('GET',  '/topic/{id:[1-9]\d*}/{name}[/{page:[1-9]\d*}]', 'Topic:viewTopic', 'Topic');
+            $r->add('GET',  '/topic/{id:[1-9]\d*}/view/new', 'Topic:viewNew', 'TopicViewNew');
+            $r->add('GET',  '/topic/{id:[1-9]\d*}/view/unread', 'Topic:viewUnread', 'TopicViewUnread');
+            $r->add('GET',  '/topic/{id:[1-9]\d*}/view/last', 'Topic:viewLast', 'TopicViewLast');
+            $r->add('GET',  '/topic/{id:[1-9]\d*}/new/reply[/{quote:[1-9]\d*}]', 'Post:newReply', 'NewReply');
+            $r->add('POST', '/topic/{id:[1-9]\d*}/new/reply', 'Post:newReplyPost');
             // сообщения
-            $r->add('GET', '/post/{id:[1-9]\d*}#p{id}', 'Topic:viewPost', 'ViewPost'); //????
-
+            $r->add('GET', '/post/{id:[1-9]\d*}#p{id}', 'Topic:viewPost', 'ViewPost');
+            $r->add('GET', '/post/{id:[1-9]\d*}/delete', 'Delete:delete', 'DeletePost'); //????
+            $r->add('GET', '/post/{id:[1-9]\d*}/edit', 'Edit:edit', 'EditPost'); //????
+            $r->add('GET', '/post/{id:[1-9]\d*}/report', 'Report:report', 'ReportPost'); //????
+            
         }
         // админ и модератор
         if ($user->isAdmMod) {
@@ -97,13 +102,13 @@ class Routing
         }
         // только админ
         if ($user->isAdmin) {
-            $r->add('GET', '/admin/statistics/info', 'AdminStatistics:info', 'AdminInfo');
-            $r->add('GET', '/admin/groups', 'AdminGroups:view', 'AdminGroups');
+            $r->add('GET',  '/admin/statistics/info', 'AdminStatistics:info', 'AdminInfo');
+            $r->add('GET',  '/admin/groups', 'AdminGroups:view', 'AdminGroups');
             $r->add('POST', '/admin/groups/new[/{base:[1-9]\d*}]', 'AdminGroups:newPost', 'AdminGroupsNew');
             $r->add('POST', '/admin/groups/default', 'AdminGroups:defaultPost', 'AdminGroupsDefault');
-            $r->add('GET', '/admin/groups/edit/{id:[1-9]\d*}', 'AdminGroups:edit', 'AdminGroupsEdit');
-            $r->add('POST', '/admin/groups/edit/{id:[1-9]\d*}', 'AdminGroups:editPost');
-            $r->add('GET', '/admin/groups/delete/{id:[1-9]\d*}', 'AdminGroups:delete', 'AdminGroupsDelete');
+            $r->add('GET',  '/admin/groups/{id:[1-9]\d*}/edit', 'AdminGroups:edit', 'AdminGroupsEdit');
+            $r->add('POST', '/admin/groups/{id:[1-9]\d*}/edit', 'AdminGroups:editPost');
+            $r->add('GET',  '/admin/groups/{id:[1-9]\d*}/delete', 'AdminGroups:delete', 'AdminGroupsDelete');
         }
 
         $uri = $_SERVER['REQUEST_URI'];

+ 87 - 26
app/Core/DB.php

@@ -5,6 +5,7 @@ namespace ForkBB\Core;
 use PDO;
 use PDOStatement;
 use PDOException;
+use ForkBB\Core\DBStatement;
 
 class DB extends PDO
 {
@@ -26,6 +27,24 @@ class DB extends PDO
      */
     protected $dbDrv;
 
+    /**
+     * Количество выполненных запросов
+     * @var int
+     */
+    protected $qCount = 0;
+
+    /**
+     * Выполненные запросы
+     * @var array
+     */
+    protected $queries = [];
+
+    /**
+     * Дельта времени для следующего запроса
+     * @var float
+     */
+    protected $delta = 0;
+
     /**
      * Конструктор
      *
@@ -50,6 +69,7 @@ class DB extends PDO
             self::ATTR_DEFAULT_FETCH_MODE => self::FETCH_ASSOC,
             self::ATTR_EMULATE_PREPARES   => false,
             self::ATTR_ERRMODE            => self::ERRMODE_EXCEPTION,
+            self::ATTR_STATEMENT_CLASS    => array(DBStatement::class, [$this]),
         ];
 
         parent::__construct($dsn, $username, $password, $options);
@@ -106,10 +126,9 @@ class DB extends PDO
         $idxIn = 0;
         $idxOut = 1;
         $map = [];
-        $bind = [];
         $query = preg_replace_callback(
             '%(?=[?:])(?<![\w?])(\?(?![:?])(\w+)?)?(?:(::?)(\w+))?%i', 
-            function($matches) use ($params, &$idxIn, &$idxOut, &$map, &$bind) {
+            function($matches) use ($params, &$idxIn, &$idxOut, &$map) {
                 if (isset($matches[3]) && $matches[3] === '::') {
                     return $this->dbPrefix . $matches[4];
                 }
@@ -137,23 +156,17 @@ class DB extends PDO
                     case 'p':
                         return (string) $value;
                     case 'ai':
-                        $bindType = self::PARAM_INT;
-                        break;
                     case 'as':
                     case 'a':
-                        $bindType = self::PARAM_STR;
                         break;
                     case 'i':
-                        $bindType = self::PARAM_INT;
                         $value = [$value];
                         break;
                     case 'b':
-                        $bindType = self::PARAM_BOOL;
                         $value = [$value];
                         break;
                     case 's':
                     default:
-                        $bindType = self::PARAM_STR;
                         $value = [$value];
                         $type = 's';
                         break;
@@ -164,7 +177,6 @@ class DB extends PDO
                     $name = ':' . $idxOut;
                     ++$idxOut;
                     $res[] = $name;
-                    $bind[$name] = [$val, $bindType];
 
                     if (empty($map[$key2])) {
                         $map[$key2] = [$type, $name];
@@ -177,9 +189,8 @@ class DB extends PDO
             $query
         );
 //var_dump($query);
-//var_dump($bind);
 //var_dump($map);
-        return [$query, $bind, $map];
+        return [$query, $map];
     }
 
     /**
@@ -195,6 +206,39 @@ class DB extends PDO
         }
     }
 
+    /**
+     * Метод для получения количества выполненных запросов
+     * 
+     * @return int
+     */
+    public function getCount()
+    {
+        return $this->qCount;
+    }
+
+    /**
+     * Метод для получения статистики выполненных запросов
+     * 
+     * @return array
+     */
+    public function getQueries()
+    {
+        return $this->queries;
+    }
+
+    /**
+     * Метод для сохранения статистики по выполненному запросу
+     * 
+     * @param string $query
+     * @param float $time
+     */
+    public function saveQuery($query, $time)
+    {
+        $this->qCount++;
+        $this->queries[] = [$query, $time + $this->delta];
+        $this->delta = 0;
+    }
+
     /**
      * Метод расширяет PDO::exec()
      *
@@ -205,16 +249,22 @@ class DB extends PDO
      */
     public function exec($query, array $params = [])
     {
-        list($query, $bind, ) = $this->parse($query, $params);
+        list($query, $map) = $this->parse($query, $params);
 
-        if (empty($bind)) {
-            return parent::exec($query);
+        if (empty($params)) {
+            $start  = microtime(true);
+            $result = parent::exec($query);
+            $this->saveQuery($query, microtime(true) - $start);
+            return $result;
         }
 
-        $stmt = parent::prepare($query);
-        $this->bind($stmt, $bind);
+        $start = microtime(true);
+        $stmt  = parent::prepare($query);
+        $this->delta = microtime(true) - $start;
+
+        $stmt->setMap($map);
 
-        if ($stmt->execute()) {
+        if ($stmt->execute($params)) {
             return $stmt->rowCount(); //??? Для запроса SELECT... не ясно поведение!
         }
 
@@ -243,9 +293,14 @@ class DB extends PDO
             $options = [];
         }
 
-        list($query, $bind, $map) = $this->parse($query, $params);
-        $stmt = parent::prepare($query, $options);
-        $this->bind($stmt, $bind);
+        list($query, $map) = $this->parse($query, $params);
+        $start = microtime(true);
+        $stmt  = parent::prepare($query, $options);
+        $this->delta = microtime(true) - $start;
+        
+        $stmt->setMap($map);
+
+        $stmt->bindValueList($params);
 
         return $stmt;
     }
@@ -266,16 +321,22 @@ class DB extends PDO
             $params = [];
         }
 
-        list($query, $bind, ) = $this->parse($query, $params);
+        list($query, $map) = $this->parse($query, $params);
 
-        if (empty($bind)) {
-            return parent::query($query, ...$args);
+        if (empty($params)) {
+            $start  = microtime(true);
+            $result = parent::query($query, ...$args);
+            $this->saveQuery($query, microtime(true) - $start);
+            return $result;
         }
 
-        $stmt = parent::prepare($query);
-        $this->bind($stmt, $bind);
+        $start = microtime(true);
+        $stmt  = parent::prepare($query);
+        $this->delta = microtime(true) - $start;
+        
+        $stmt->setMap($map);
 
-        if ($stmt->execute()) {
+        if ($stmt->execute($params)) {
             if (! empty($args)) {
                 $stmt->setFetchMode(...$args);
             }

+ 108 - 0
app/Core/DBStatement.php

@@ -0,0 +1,108 @@
+<?php
+
+namespace ForkBB\Core;
+
+use PDO;
+use PDOStatement;
+use PDOException;
+
+class DBStatement extends PDOStatement
+{
+    /**
+     * Префикс для таблиц базы
+     * @var PDO
+     */
+    protected $db;
+
+    /**
+     * Карта преобразования переменных
+     * @var array
+     */
+    protected $map = [];
+
+    /**
+     * Карта типов
+     * @var array
+     */
+    protected $types = [
+        's'  => PDO::PARAM_STR,
+        'i'  => PDO::PARAM_INT,
+        'b'  => PDO::PARAM_BOOL,
+        'a'  => PDO::PARAM_STR,
+        'as' => PDO::PARAM_STR,
+        'ai' => PDO::PARAM_INT,
+    ];
+
+    /**
+     * Конструктор
+     *
+     * @param PDO $db
+     */
+    protected function __construct(PDO $db)
+    {
+        $this->db = $db;
+    }
+
+
+    /**
+     * Метод задает карту преобразования перменных
+     *
+     * @param array $map
+     */
+    public function setMap(array $map)
+    {
+        $this->map = $map;
+    }
+
+    /**
+     * Метод привязывает параметры к значениям на основе карты
+     *
+     * @param array $params
+     *
+     * @throws PDOException
+     */
+    public function bindValueList(array $params)
+    {
+        foreach ($this->map as $key => $data) {
+            if (isset($params[$key])) {
+                $bValue = $params[$key];
+            } elseif (isset($params[':' . $key])) {
+                $bValue = $params[':' . $key];
+            } else {
+                throw new PDOException("The value for :{$key} placeholder isn't found");
+            }
+
+            $type = array_shift($data);
+            $bType = $this->types[$type];
+
+            if ($type{0} === 'a') {
+                foreach ($data as $bParam) {
+                    $bVal = array_shift($bValue); //????
+                    parent::bindValue($bParam, $bVal, $bType); //????
+                }
+            } else {
+                foreach ($data as $bParam) {
+                    parent::bindValue($bParam, $bValue, $bType); //????
+                }
+            }
+        }
+    }
+
+    /**
+     * Метод расширяет PDOStatement::execute()
+     *
+     * @param array|null $params
+     *
+     * @return bool
+     */
+    public function execute($params = null)
+    {
+        if (is_array($params) && ! empty($params)) {
+            $this->bindValueList($params);
+        }
+        $start = microtime(true);
+        $result = parent::execute();
+        $this->db->saveQuery($this->queryString, microtime(true) - $start);
+        return $result;
+    }
+}

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

@@ -24,13 +24,22 @@ class Debug extends Page
     {
         $this->data = [
             'time' => $this->number(microtime(true) - $this->c->START, 3),
-            'numQueries' => 0, //$this->c->DB->get_num_queries(),
+            'numQueries' => $this->c->DB->getCount(),
             'memory' => $this->size(memory_get_usage()),
             'peak' => $this->size(memory_get_peak_usage()),
         ];
 
         if ($this->c->DEBUG > 1) {
-            $this->data['queries'] = $this->c->DB->get_saved_queries();
+            $total = 0;
+            $this->data['queries'] = array_map(
+                function($a) use (&$total) {
+                    $total += $a[1];
+                    $a[1] = $this->number($a[1], 3);
+                    return $a;
+                }, 
+                $this->c->DB->getQueries()
+            );
+            $this->data['total'] = $this->number($total, 3);
         } else {
             $this->data['queries'] = null;
         }

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

@@ -166,13 +166,13 @@ class Forum extends Page
                 }
                 // новые сообщения
                 if ($time > max($upper, (int) $cur['mt_last_visit'])) {
-                    $cur['link_new'] = $this->c->Router->link('TopicGoToNew', ['id' => $cur['id']]);
+                    $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('TopicGoToUnread', ['id' => $cur['id']]);
+                    $cur['link_unread'] = $this->c->Router->link('TopicViewUnread', ['id' => $cur['id']]);
                 } else {
                     $cur['link_unread'] = null;
                 }

+ 175 - 58
app/Models/Pages/Topic.php

@@ -7,7 +7,7 @@ class Topic extends Page
     use UsersTrait;
     use OnlineTrait;
     use CrumbTrait;
-    
+
     /**
      * Имя шаблона
      * @var string
@@ -27,7 +27,7 @@ class Topic extends Page
      * @var bool
      */
     protected $onlineType = true;
-     
+
     /**
      * Тип возврата данных при onlineType === true
      * Если true, то из online должны вернутся только пользователи находящиеся на этой же странице
@@ -41,7 +41,7 @@ class Topic extends Page
      * @param array $args
      * @return Page
      */
-     public function goToNew(array $args)
+     public function viewNew(array $args)
      {
 
      }
@@ -51,9 +51,9 @@ class Topic extends Page
      * @param array $args
      * @return Page
      */
-     public function goToUnread(array $args)
+     public function viewUnread(array $args)
      {
-         
+
      }
 
     /**
@@ -61,7 +61,7 @@ class Topic extends Page
      * @param array $args
      * @return Page
      */
-     public function goToLast(array $args)
+     public function viewLast(array $args)
      {
         $vars = [
             ':tid' => $args['id'],
@@ -86,20 +86,23 @@ class Topic extends Page
      {
         $vars = [
             ':pid' => $args['id'],
+            ':uid' => $this->c->user->id,
         ];
 
         if ($this->c->user->isGuest) {
-            $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed 
-                    FROM ::topics AS t 
-                    INNER JOIN ::forums AS f ON f.id=t.forum_id 
+            $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 
-                    FROM ::topics AS t 
-                    INNER JOIN ::forums AS f ON f.id=t.forum_id 
+            $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 ::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';
         }
 
@@ -115,18 +118,21 @@ class Topic extends Page
     {
         $vars = [
             ':tid' => $args['id'],
+            ':uid' => $this->c->user->id,
         ];
 
         if ($this->c->user->isGuest) {
-            $sql = 'SELECT t.*, f.moderators, 0 AS is_subscribed 
-                    FROM ::topics AS t 
-                    INNER JOIN ::forums AS f ON f.id=t.forum_id 
+            $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 
-                    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) 
+            $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';
         }
 
@@ -149,19 +155,19 @@ class Topic extends Page
         $vars[':uid'] = $user->id;
 
         $topic = $this->c->DB->query($sql, $vars)->fetch();
-        
+
         // тема отсутствует или недоступна
         if (empty($topic)) {
             return $this->c->Message->message('Bad request');
         }
-        
+
         list($fTree, $fDesc, $fAsc) = $this->c->forums;
-        
+
         // раздел отсутствует в доступных
         if (empty($fDesc[$topic['forum_id']])) {
             return $this->c->Message->message('Bad request');
         }
-        
+
         if (null === $page) {
             $vars[':tid'] = $topic['id'];
             $sql = 'SELECT COUNT(id) FROM ::posts WHERE topic_id=?i:tid AND id<?i:pid';
@@ -185,28 +191,32 @@ class Topic extends Page
             ':offset' => $offset,
             ':rows'   => $user->dispPosts,
         ];
-        $sql = 'SELECT id 
-                FROM ::posts 
-                WHERE topic_id=?i:tid 
+        $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->goToLast($topic['id']);
+            return $this->viewLast($topic['id']);
         }
 
         $moders = empty($topic['moderators']) ? [] : array_flip(unserialize($topic['moderators']));
         $parent = isset($fDesc[$topic['forum_id']][0]) ? $fDesc[$topic['forum_id']][0] : 0;
         $perm = $fTree[$parent][$topic['forum_id']];
 
+        if($user->isBot) {
+            $perm['post_replies'] = 0;
+        }
+
         $newOn = null;
         if ($user->isAdmin) {
             $newOn = true;
         } elseif ($topic['closed'] == '1') {
             $newOn = false;
-        } elseif ($perm['post_replies'] === 1 
+        } elseif ($perm['post_replies'] === 1
             || (null === $perm['post_replies'] && $user->gPostReplies == '1')
             || ($user->isAdmMod && isset($moders[$user->id]))
         ) {
@@ -222,23 +232,23 @@ class Topic extends Page
         $vars = [
             ':ids' => $ids,
         ];
-        $sql = 'SELECT id, message, poster, posted 
-                FROM ::warnings 
+        $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 
+        $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);
@@ -268,6 +278,7 @@ class Topic extends Page
         $posts = [];
         $signs = [];
         $posters = [];
+        $timeMax = 0;
         while ($cur = $stmt->fetch()) {
             // данные по автору сообшения
             if (isset($posters[$cur['poster_id']])) {
@@ -285,7 +296,7 @@ class Topic extends Page
                     'poster_posts'      => null,
                     'poster_gender'     => '',
                     'poster_online'     => '',
-   
+
                 ];
                 if ($cur['poster_id'] > 1) {
                     if ($user->gViewUsers == '1') {
@@ -296,12 +307,12 @@ class Topic extends Page
                     }
                     if ($this->config['o_show_user_info'] == '1') {
                         $post['poster_info_add'] = true;
-                        
+
                         $post['poster_registered'] = $this->time($cur['registered'], true);
-                        
+
                         $post['poster_posts']     = $this->number($cur['num_posts']);
                         $post['poster_num_posts'] = $cur['num_posts'];
-                        
+
                         if ($cur['location'] != '') {
                             $post['poster_location'] = $this->censor($cur['location']);
                         }
@@ -313,9 +324,9 @@ class Topic extends Page
 
                     $posters[$cur['poster_id']] = $post;
 
-                    if ($this->config['o_signatures'] == '1' 
-                        && $cur['signature'] != '' 
-                        && $user->showSig == '1' 
+                    if ($this->config['o_signatures'] == '1'
+                        && $cur['signature'] != ''
+                        && $user->showSig == '1'
                         && ! isset($signs[$cur['poster_id']])
                     ) {
                         $signs[$cur['poster_id']] = $cur['signature'];
@@ -329,6 +340,8 @@ class Topic extends Page
             $post['posted']     = $this->time($cur['posted']);
             $post['posted_utc'] = gmdate('Y-m-d\TH:i:s\Z', $cur['posted']);
 
+            $timeMax = max($timeMax, $cur['posted']);
+
             $parser->parse($this->censor($cur['message']));
             if ($this->config['o_smilies'] == '1' && $user->showSmilies == '1' && $cur['hide_smilies'] == '0') {
                 $parser->detectSmilies();
@@ -345,31 +358,32 @@ class Topic extends Page
 
             // данные по элементам управления
             $controls = [];
+            $vars = ['id' => $cur['id']];
             if (! $user->isAdmin && ! $user->isGuest) {
-                $controls['report'] = ['#', 'Report'];
+                $controls['report'] = [$this->c->Router->link('ReportPost', $vars), 'Report'];
             }
-            if ($user->isAdmin 
+            if ($user->isAdmin
                 || ($user->isAdmMod && isset($moders[$user->id]) && ! in_array($cur['poster_id'], $this->c->admins))
             ) {
-                $controls['delete'] = ['#', 'Delete'];
-                $controls['edit'] = ['#', 'Edit'];
-            } elseif ($topic['closed'] != '1' 
+                $controls['delete'] = [$this->c->Router->link('DeletePost', $vars), 'Delete'];
+                $controls['edit'] = [$this->c->Router->link('EditPost', $vars), 'Edit'];
+            } elseif ($topic['closed'] != '1'
                 && $cur['poster_id'] == $user->id
                 && ($user->gDeleditInterval == '0' || $cur['edit_post'] == '1' || time() - $cur['posted'] < $user->gDeleditInterval)
             ) {
                 if (($cur['id'] == $topic['first_post_id'] && $user->gDeleteTopics == '1') || ($cur['id'] != $topic['first_post_id'] && $user->gDeletePosts == '1')) {
-                    $controls['delete'] = ['#', 'Delete'];
+                    $controls['delete'] = [$this->c->Router->link('DeletePost', $vars), 'Delete'];
                 }
                 if ($user->gEditPosts == '1') {
-                    $controls['edit'] = ['#', 'Edit'];
+                    $controls['edit'] = [$this->c->Router->link('EditPost', $vars), 'Edit'];
                 }
             }
             if ($newOn) {
-                $controls['quote'] = ['#', 'Reply'];
+                $controls['quote'] = [$this->c->Router->link('NewReply', ['id' => $topic['id'], 'quote' => $cur['id']]), 'Reply'];
             }
 
             $post['controls'] = $controls;
-            
+
             $posts[] = $post;
         }
 
@@ -392,9 +406,76 @@ class Topic extends Page
         }
 
         $topic['subject'] = $this->censor($topic['subject']);
-        
+
         $this->onlinePos = 'topic-' . $topic['id'];
 
+        // данные для формы быстрого ответа
+        $form = null;
+        if ($newOn && $this->config['o_quickpost'] == '1') {
+            $form = [
+                'action' => $this->c->Router->link('NewReply', ['id' => $topic['id']]),
+                'hidden' => [
+                    'token' => $this->c->Csrf->create('NewReply', ['id' => $topic['id']]),
+                ],
+                'sets'   => [],
+                'btns'   => [
+                    'submit'  => ['submit', __('Submit'), 's'],
+                    'preview' => ['submit', __('Preview'), 'p'],
+                ],
+            ];
+
+            $fieldset = [];
+            if ($user->isGuest) {
+                $fieldset['username'] = [
+                    'dl'        => 't1',
+                    'type'      => 'text',
+                    'maxlength' => 25,
+                    'title'     => __('Username'),
+                    'required'  => true,
+                    'pattern'   => '^.{2,25}$',
+                ];
+                $fieldset['email'] = [
+                    'dl'        => 't2',
+                    'type'      => 'text',
+                    'maxlength' => 80,
+                    'title'     => __('Email'),
+                    'required'  => $this->config['p_force_guest_email'] == '1',
+                    'pattern'   => '.+@.+',
+                ];
+            }
+
+            $fieldset['message'] = [
+                'type'     => 'textarea',
+                'title'    => __('Message'),
+                'required' => true,
+                'bb'       => [
+                    ['link', __('BBCode'), __($this->config['p_message_bbcode'] == '1' ? 'on' : 'off')],
+                    ['link', __('url tag'), __($this->config['p_message_bbcode'] == '1' && $user->gPostLinks == '1' ? 'on' : 'off')],
+                    ['link', __('img tag'), __($this->config['p_message_bbcode'] == '1' && $this->config['p_message_img_tag'] == '1' ? 'on' : 'off')],
+                    ['link', __('Smilies'), __($this->config['o_smilies'] == '1' ? 'on' : 'off')],
+                ],
+            ];
+            $form['sets'][] = [
+                'fields' => $fieldset,
+            ];
+
+            $fieldset = [];
+            if ($user->isAdmin || ($user->isAdmMod && isset($moders[$user->id]))) {
+                $fieldset['merge'] = [
+                    'type'    => 'checkbox',
+                    'label'   => __('Merge posts'),
+                    'value'   => '1',
+                    'checked' => true,
+                ];
+            }
+            if ($fieldset) {
+                $form['sets'][] = [
+                    'legend' => __('Options'),
+                    'fields' => $fieldset,
+                ];
+            }
+        }
+
         $this->data = [
             'topic'    => $topic,
             'posts'    => $posts,
@@ -404,11 +485,12 @@ class Topic extends Page
                 ['Topic', ['id' => $topic['id'], 'name' => $topic['subject']]],
                 [$fDesc, $topic['forum_id']]
             ),
-            'newPost'  => $newOn ? $this->c->Router->link('NewPost', ['id' => $topic['id']]) : $newOn,
+            'NewReply' => $newOn ? $this->c->Router->link('NewReply', ['id' => $topic['id']]) : $newOn,
             'stickFP'  => $stickFP,
             'pages'    => $this->c->Func->paginate($pages, $page, 'Topic', ['id' => $topic['id'], 'name' => $topic['subject']]),
             'online'   => $this->getUsersOnlineInfo(),
             'stats'    => null,
+            'form'     => $form,
         ];
 
         $this->canonical = $this->c->Router->link('Topic', ['id' => $topic['id'], 'name' => $topic['subject'], 'page' => $page]);
@@ -422,6 +504,41 @@ class Topic extends Page
             $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'],
+            ];
+            $flag = false;
+            $lower = max((int) $user->uMarkAllRead, (int) $topic['mf_mark_all_read'], (int) $topic['mt_last_read']); //????
+            if ($timeMax > $lower) {
+                $vars[':read'] = $timeMax;
+                $flag = true;
+            }
+            $upper = max($lower, (int) $topic['mt_last_visit'], (int) $user->lastVisit); //????
+            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'])) {
+                    $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 
+                                        WHERE NOT EXISTS (SELECT 1 
+                                                          FROM ::mark_of_topic 
+                                                          WHERE uid=?i:uid AND tid=?i:tid) 
+                                        LIMIT 1', $vars);
+                } else {
+                    $this->c->DB->exec('UPDATE ::mark_of_topic 
+                    SET mt_last_visit=?i:visit, mt_last_read=?i:read 
+                    WHERE uid=?i:uid AND tid=?i:tid', $vars);
+                }
+            }
+        }
+
         return $this;
     }
 }

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

@@ -106,3 +106,6 @@ msgstr "Online:"
 
 msgid "Stats info"
 msgstr "Topic information"
+
+msgid "Merge posts"
+msgstr "Merge with previous if it yours"

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

@@ -107,3 +107,6 @@ msgstr "Активны:"
 
 msgid "Stats info"
 msgstr "Информация о теме"
+
+msgid "Merge posts"
+msgstr "Соединить с предыдущим сообщением, если оно ваше"

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

@@ -16,6 +16,10 @@
             <td class="tcr">{{ $cur[0] }}</td>
           </tr>
 @endforeach
+          <tr>
+            <td class="tcl">{{ $total }}</td>
+            <td class="tcr"></td>
+          </tr>
         </tbody>
       </table>
 @endif

+ 51 - 0
app/templates/layouts/form.tpl

@@ -0,0 +1,51 @@
+        <form class="f-form" method="post" action="{!! $form['action'] !!}">
+@if($form['hidden'])
+@foreach($form['hidden'] as $key => $val)
+          <input type="hidden" name="{{ $key }}" value="{{ $val }}">
+@endforeach
+@endif
+@foreach($form['sets'] as $fieldset)
+          <fieldset>
+@if(isset($fieldset['legend']))
+            <legend>{!! $fieldset['legend'] !!}</legend>
+@endif
+@foreach($fieldset['fields'] as $key => $cur)
+@if(isset($cur['dl']))
+            <dl class="f-field-{{ $cur['dl'] }}">
+@else
+            <dl>
+@endif
+@if(isset($cur['title']))
+              <dt><label class="f-child1{!! empty($cur['required']) ? '' : ' f-req' !!}" for="id-{{ $key }}">{!! $cur['title'] !!}</label></dt>
+@else
+              <dt></dt>
+@endif
+              <dd>
+@if($cur['type'] === 'textarea')
+                <textarea{!! empty($cur['required']) ? '' : ' required' !!} class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}">{{ $cur['value'] or '' }}</textarea>
+@if(isset($cur['bb']))
+                  <ul class="f-child5">
+@foreach($cur['bb'] as $val)
+                    <li><span><a href="{!! $val[0] !!}">{!! $val[1] !!}</a> {!! $val[2] !!}</span></li>
+@endforeach
+                  </ul>
+@endif
+@elseif($cur['type'] === 'text')
+                <input{!! empty($cur['required']) ? '' : ' required' !!} class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}" type="text" maxlength="{{ $cur['maxlength'] or '' }}" pattern="{{ $cur['pattern'] or '' }}" value="{{ $cur['value'] or '' }}">
+@elseif($cur['type'] === 'checkbox')
+                <label class="f-child2"><input type="checkbox" id="id-{{ $key }}" name="{{ $key }}" value="{{ $cur['value'] or '0' }}"{!! empty($cur['checked']) ? '' : ' checked' !!}>{!! $cur['label'] !!}</label>
+@endif
+@if(isset($cur['info']))
+                <p class="f-child4">{!! $cur['info'] !!}</p>
+@endif
+              </dd>
+            </dl>
+@endforeach
+          </fieldset>
+@endforeach
+          <p>
+@foreach($form['btns'] as $key => $cur)
+            <input class="f-btn" type="{{ $cur[0] }}" name="{{ $key }}" value="{{ $cur[1] }}" accesskey="{{ $cur[2] }}">
+@endforeach
+          </p>
+        </form>

+ 1 - 1
app/templates/login.tpl

@@ -21,7 +21,7 @@
             <dl>
               <dt></dt>
 @if($save)
-              <dd><label class="f-child2"><input type="checkbox" name="save" value="1" tabindex="3" checked="checked">{!! __('Remember me') !!}</label></dd>
+              <dd><label class="f-child2"><input type="checkbox" name="save" value="1" tabindex="3" checked>{!! __('Remember me') !!}</label></dd>
 @else
               <dd><label class="f-child2"><input type="checkbox" name="save" value="1" tabindex="3">{!! __('Remember me') !!}</label></dd>
 @endif

+ 8 - 35
app/templates/topic.tpl

@@ -10,12 +10,12 @@
       </ul>
 @endsection
 @section('linkpost')
-@if($newPost !== null)
+@if($NewReply !== null)
         <div class="f-link-post">
-@if($newPost === false)
+@if($NewReply === false)
           __('Topic closed')
 @else
-          <a class="f-btn" href="{!! $newPost !!}">{!! __('Post reply') !!}</a>
+          <a class="f-btn" href="{!! $NewReply !!}">{!! __('Post reply') !!}</a>
 @endif
         </div>
 @endif
@@ -40,7 +40,7 @@
 @extends('layouts/main')
     <div class="f-nav-links">
 @yield('crumbs')
-@if($newPost || $pages)
+@if($NewReply || $pages)
       <div class="f-links-b clearfix">
 @yield('pages')
 @yield('linkpost')
@@ -112,7 +112,7 @@
 @endforeach
     </section>
     <div class="f-nav-links">
-@if($newPost || $pages)
+@if($NewReply || $pages)
       <div class="f-links-a clearfix">
 @yield('linkpost')
 @yield('pages')
@@ -123,38 +123,11 @@
 @if($online)
 @include('layouts/stats')
 @endif
+@if($form)
     <section class="post-form">
       <h2>{!! __('Quick post') !!}</h2>
       <div class="f-fdiv">
-        <form class="f-form" method="post" action="">
-          <fieldset>
-            <dl>
-              <dt><label class="f-child1 f-req" for="id-username">{!! __('Username') !!}</label></dt>
-              <dd>
-                <input required class="f-ctrl" id="id-username" type="text" name="username" value="" maxlength="25" pattern="^.{2,25}$" spellcheck="false">
-              </dd>
-            </dl>
-            <dl>
-              <dt><label class="f-child1 f-req" for="id-email">{!! __('Email') !!}</label></dt>
-              <dd>
-                <input required class="f-ctrl" id="id-email" type="text" name="email" value="" maxlength="80" pattern=".+@.+" spellcheck="false">
-              </dd>
-            </dl>
-            <dl>
-              <dt><label class="f-child1 f-req" for="id-message">{!! __('Message') !!}</label></dt>
-              <dd>
-                <textarea required class="f-ctrl" id="id-message" name="message"></textarea>
-              </dd>
-            </dl>
-          </fieldset>
-          <fieldset>
-            <legend>{!! __('Options') !!}</legend>
-            <div>
-            </div>
-          </fieldset>
-          <p>
-            <input class="f-btn" type="submit" name="submit" value="{!! __('Submit') !!}">
-          </p>
-        </form>
+@include('layouts/form')
       </div>
     </section>
+@endif

+ 51 - 3
public/style/ForkBB/style.css

@@ -419,7 +419,8 @@ select {
 .f-fdiv .f-child1,
 .f-fdiv .f-child2,
 .f-fdiv .f-child3,
-.f-fdiv .f-child4 {
+.f-fdiv .f-child4,
+.f-fdiv .f-child5 {
   display: block;
   width: 100%;
 }
@@ -428,7 +429,8 @@ select {
   font-weight: bold;
 }
 
-.f-fdiv .f-child2 {
+.f-fdiv .f-child2,
+.f-fdiv .f-child5 {
   font-size: 0.875rem;
 }
 
@@ -451,6 +453,11 @@ select {
   text-align: justify;
 }
 
+.f-fdiv .f-child5 {
+  margin-top: -0.375rem;
+  margin-bottom: 1rem
+}
+
 .f-fdiv .f-finfo {
   margin: 1rem -0.625rem;
   background-color: #AA7939;
@@ -550,6 +557,10 @@ select {
   border-bottom: 0.0625rem solid #AA7939;
 }
 
+.f-debug .tcl {
+  width: 5rem;
+}
+
 /******************/
 /* Хлебные крошки */
 /******************/
@@ -1274,10 +1285,15 @@ li + li .f-btn {
   }
 } /* @media screen and (min-width: 50rem) */
 
+/*****************************/
 .post-form {
   border-top: 0.0625rem solid #AA7939;
 }
 
+.post-form > h2 {
+  display: none;
+}
+
 .post-form > .f-fdiv {
   margin: 1rem 0;
   background-color: #F8F4E3;
@@ -1288,8 +1304,40 @@ li + li .f-btn {
   padding-bottom: 0.625rem;
   margin-bottom: 0.625rem;
   border-bottom: 0.0625rem dotted #AA7939;
+  font-weight: bold;
+}
+
+.post-form .f-child1 {
+  font-size: 0.875rem;
+}
+
+.post-form .f-child5 > li {
+  display: inline;
+}
+
+.post-form .f-child5 a {
+  border: 0;
 }
 
 .post-form .f-btn {
   width: auto;
-}
+  display: inline;
+}
+
+#id-message {
+  height: 11rem;
+}
+
+@media screen and (min-width: 50rem) {
+  .f-fdiv .f-field-t1 {
+    width: 30%;
+    float: left;
+    padding-right: 0.3125rem;
+  }
+
+  .f-fdiv .f-field-t2 {
+    width: 70%;
+    float: left;
+    padding-left: 0.3125rem;
+  }
+}