Visman 7 gadi atpakaļ
vecāks
revīzija
66ac330784

+ 1 - 1
app/Core/Mail.php

@@ -106,7 +106,7 @@ class Mail
         $domainASCII = idn_to_ascii($domain);
 
         if ($strict) {
-            $mx = dns_get_record($domainASCII, DNS_MX);
+            $mx = @dns_get_record($domainASCII, DNS_MX); //????
             if (empty($mx)) {
                 return false;
             }

+ 4 - 2
app/Core/Parser.php

@@ -30,8 +30,10 @@ class Parser extends Parserus
      */
     protected function init()
     {
-        $bbcodes = include $this->c->DIR_CONFIG . '/defaultBBCode.php';
-        $this->setBBCodes($bbcodes);
+        if ($this->c->config->p_message_bbcode == '1' || $this->c->config->p_sig_bbcode == '1') {
+            $bbcodes = include $this->c->DIR_CONFIG . '/defaultBBCode.php';
+            $this->setBBCodes($bbcodes);
+        }
 
         if ($this->c->user->show_smilies == '1'
             && ($this->c->config->o_smilies_sig == '1' || $this->c->config->o_smilies == '1')

+ 16 - 7
app/Core/Router.php

@@ -65,9 +65,9 @@ class Router
     public function __construct($base)
     {
         $this->baseUrl = $base;
-        $this->host = parse_url($base, PHP_URL_HOST);
-        $this->prefix = parse_url($base, PHP_URL_PATH);
-        $this->length = strlen($this->prefix);
+        $this->host    = parse_url($base, PHP_URL_HOST);
+        $this->prefix  = parse_url($base, PHP_URL_PATH);
+        $this->length  = strlen($this->prefix);
     }
 
     /**
@@ -107,14 +107,23 @@ class Router
             $s = $this->links[$marker];
             foreach ($args as $key => $val) {
                 if ($key == '#') {
-                    $s .= '#' . rawurlencode($val); //????
+                    $s .= '#' . rawurlencode($val);
                     continue;
                 } elseif ($key == 'page' && $val === 1) {
                     continue;
                 }
-                $s = preg_replace(
-                    '%\{' . preg_quote($key, '%') . '(?::[^{}]+)?\}%',
-                    rawurlencode($val),
+                $s = preg_replace_callback( //????
+                    '%\{' . preg_quote($key, '%') . '(?::[^{}]+)?\}%', 
+                    function($match) use ($val) {
+                        if (is_string($val)) {
+                            $val = trim(preg_replace('%[^\p{L}\p{N}_]+%u', '-', $val), '_-');
+                        } elseif (is_numeric($val)) { //????
+                            $val = (string) $val;
+                        } else {
+                            $val = null;
+                        }
+                        return isset($val[0]) ? rawurlencode($val) : '-';
+                    },
                     $s
                 );
             }

+ 154 - 114
app/Core/Validator.php

@@ -73,6 +73,18 @@ class Validator
      */
     protected $raw;
 
+    /**
+     * Данные для текущей обработки
+     * @var array
+     */
+    protected $curData = [];
+
+    /**
+     * Флаг ошибки
+     * @var mixed
+     */
+    protected $error;
+
     /**
      * Конструктор
      *
@@ -208,9 +220,10 @@ class Validator
         if (empty($this->rules)) {
             throw new RuntimeException('Rules not found');
         }
-        $this->errors = [];
-        $this->status = [];
-        $this->raw = $raw;
+        $this->errors  = [];
+        $this->status  = [];
+        $this->curData = [];
+        $this->raw     = $raw;
         foreach ($this->fields as $field) {
             $this->__get($field);
         }
@@ -258,70 +271,87 @@ class Validator
             }
         }
 
-        $error = false;
         foreach ($rules as $validator => $attr) {
-            $args = $this->getArguments($field, $validator);
-            list($value, $error) = $this->validators[$validator]($this, $value, $attr, $args);
-            if (false !== $error) {
+            // данные для обработчика ошибок
+            $this->error     = null;
+            $this->curData[] = [
+                'field' => $field,
+                'rule'  => $validator,
+                'attr'  => $attr,
+            ];
+
+            $value = $this->validators[$validator]($this, $value, $attr, $this->getArguments($field, $validator));
+            
+            array_pop($this->curData);
+
+            if (null !== $this->error) {
                 break;
             }
         }
 
-        if (! is_bool($error)) {
-            $this->error($error, $field, $validator, $attr);
-            $this->status[$field] = false;
-        } else {
-            $this->status[$field] = true;
-        }
-
+        $this->status[$field] = true !== $this->error; // в $this->error может быть состояние false
         $this->result[$field] = $value;
+
         return $value;
     }
 
     /**
-     * Получение дополнительных аргументов
+     * Добавление ошибки
      *
-     * @param string $field
-     * @param string $rule
-     *
-     * @return mixed
+     * @param mixed $error
+     * @param string $type
+     * 
+     * @throws RuntimeException
      */
-    protected function getArguments($field, $rule)
+    public function addError($error, $type = 'v')
     {
-        if (isset($this->arguments[$field . '.' . $rule])) {
-            return $this->arguments[$field . '.' . $rule];
-        } elseif (isset($this->arguments[$field])) {
-            return $this->arguments[$field];
-        } else {
-            return null;
+        if (empty($vars = end($this->curData))) {
+            throw new RuntimeException('The array of variables is empty');
         }
-    }
 
-    /**
-     * Обработка ошибки
-     *
-     * @param mixed $error
-     * @param string $field
-     * @param string $rule
-     * @param string $attr
-     */
-    protected function error($error, $field, $rule, $attr)
-    {
+        // нет ошибки, для выхода из цикла проверки правил
+        if (true === $error) {
+            $this->error = false;
+            return;
+        }
+
+        extract($vars);
+
         // псевдоним имени поля
         $alias = isset($this->aliases[$field]) ? $this->aliases[$field] : $field;
+
         // текст ошибки
         if (isset($this->messages[$field . '.' . $rule])) {
             $error = $this->messages[$field . '.' . $rule];
         } elseif (isset($this->messages[$field])) {
             $error = $this->messages[$field];
         }
-        $type = 'v';
-        // ошибка содержит тип
         if (is_array($error)) {
             $type = $error[1];
             $error = $error[0];
         }
+
         $this->errors[$type][] = __($error, [':alias' => $alias, ':attr' => $attr]);
+        $this->error           = true;
+    }
+
+    /**
+     * Получение дополнительных аргументов
+     *
+     * @param string $field
+     * @param string $rule
+     *
+     * @return mixed
+     */
+    protected function getArguments($field, $rule)
+    {
+        if (isset($this->arguments[$field . '.' . $rule])) {
+            return $this->arguments[$field . '.' . $rule];
+        } elseif (isset($this->arguments[$field])) {
+            return $this->arguments[$field];
+        } else {
+            return null;
+        }
     }
 
     /**
@@ -367,48 +397,53 @@ class Validator
 
     protected function vAbsent($v, $value)
     {
-        if (null === $value) {
-            return [$value, false];
-        } else {
-            return [null, 'The :alias should be absent'];
+        if (null !== $value) {
+            $this->addError('The :alias should be absent');
         }
+        return null;
     }
 
     protected function vRequired($v, $value)
     {
         if (is_string($value)) {
             if (strlen(preg_replace('%^\s+|\s+$%u', '', $value)) > 0) {
-                return [$value, false];
+                return $value;
             }
         } elseif (is_array($value)) {
             if (! empty($value)) {
-                return [$value, false];
+                return $value;
             }
         } elseif (null !== $value) {
-            return [$value, false];
+            return $value;
         }
-        return [null, 'The :alias is required'];
+        $this->addError('The :alias is required');
+        return null;
     }
 
-    protected function vRequiredWith($v, $value, $attr)
+    protected function vRequiredWith($v, $value, $attr)  //???????????????????????
     {
         foreach (explode(',', $attr) as $field) {
-            if (null !== $this->__get($field)) {
-                return $this->vRequired($v, $value);
-            }
+            if (null !== $this->__get($field)) {     // если есть хотя бы одно поле,
+                return $this->vRequired($v, $value); // то проверяем данное поле
+            }                                        // на обязательное наличие
         }
-        list(, $error) = $this->vRequired($v, $value);
-        if (false === $error) {
-            return [null, 'The :alias is not required'];
-        } else {
-            return [$value, true];
+        if (null === $value) {                       // если данное поле отсутствует,
+            $this->addError(true);                   // то прерываем его проверку
         }
+        return $value;
+
+#        list(, $error) = $this->vRequired($v, $value);
+#        if (false === $error) {
+#            return [null, 'The :alias is not required'];
+#        } else {
+#            return [$value, true];
+#        }
     }
 
     protected function vString($v, $value, $attr)
     {
         if (null === $value) {
-            return [null, false];
+            return null;
         } elseif (is_string($value)) {
             foreach(explode(',', $attr) as $action) {
                 switch ($action) {
@@ -423,83 +458,87 @@ class Validator
                         break;
                 }
             }
-            return [$value, false];
+            return $value;
         } else {
-            return [null, 'The :alias must be string'];
+            $this->addError('The :alias must be string');
+            return null;
         }
     }
 
     protected function vNumeric($v, $value)
     {
         if (null === $value) {
-            return [null, false];
+            return null;
         } elseif (is_numeric($value)) {
-            return [0 + $value, false];
+            return 0 + $value;
         } else {
-            return [null, 'The :alias must be numeric'];
+            $this->addError('The :alias must be numeric');
+            return null;
         }
     }
 
     protected function vInteger($v, $value)
     {
         if (null === $value) {
-            return [null, false];
+            return null;
         } elseif (is_numeric($value) && is_int(0 + $value)) {
-            return [(int) $value, false];
+            return (int) $value;
         } else {
-            return [null, 'The :alias must be integer'];
+            $this->addError('The :alias must be integer');
+            return null;
         }
     }
 
     protected function vArray($v, $value)
     {
-        if (null === $value) {
-            return [null, false];
-        } elseif (is_array($value)) {
-            return [$value, false];
+        if (null !== $value && ! is_array($value)) {
+            $this->addError('The :alias must be array');
+            return null;
         } else {
-            return [null, 'The :alias must be array'];
+            return $value;
         }
     }
 
     protected function vMin($v, $value, $attr)
     {
-        if (is_numeric($value)) {
-            if (0 + $value < $attr) {
-                return [$value, 'The :alias minimum is :attr'];
-            }
-        } elseif (is_string($value)) {
+        if (is_string($value)) {
             if (mb_strlen($value, 'UTF-8') < $attr) {
-                return [$value, 'The :alias minimum is :attr characters'];
+                $this->addError('The :alias minimum is :attr characters');
+            }
+        } elseif (is_numeric($value)) {
+            if (0 + $value < $attr) {
+                $this->addError('The :alias minimum is :attr');
             }
         } elseif (is_array($value)) {
             if (count($value) < $attr) {
-                return [$value, 'The :alias minimum is :attr elements'];
+                $this->addError('The :alias minimum is :attr elements');
             }
-        } else {
-            return [null, null === $value ? false : 'The :alias minimum is :attr'];
+        } elseif (null !== $value) {
+            $this->addError('The :alias minimum is :attr');
+            return null;
         }
-        return [$value, false];
+        return $value;
     }
 
     protected function vMax($v, $value, $attr)
     {
-        if (is_numeric($value)) {
-            if (0 + $value > $attr) {
-                return [$value, 'The :alias maximum is :attr'];
-            }
-        } elseif (is_string($value)) {
+        if (is_string($value)) {
             if (mb_strlen($value, 'UTF-8') > $attr) {
-                return [$value, 'The :alias maximum is :attr characters'];
+                $this->addError('The :alias maximum is :attr characters');
+            }
+        } elseif (is_numeric($value)) {
+            if (0 + $value > $attr) {
+                $this->addError('The :alias maximum is :attr');
             }
         } elseif (is_array($value)) {
             if (count($value) > $attr) {
-                return [$value, 'The :alias maximum is :attr elements'];
+                $this->addError('The :alias maximum is :attr elements');
             }
-        } else {
-            return [null, null === $value ? false : 'The :alias maximum is :attr'];
+        } elseif (null !== $value) {
+            $this->addError('The :alias maximum is :attr'); //????
+            return null;
         }
-        return [$value, false];
+        return $value;
     }
 
     protected function vToken($v, $value, $attr, $args)
@@ -507,17 +546,17 @@ class Validator
         if (! is_array($args)) {
             $args = [];
         }
-        $value = (string) $value;
-        if ($this->c->Csrf->verify($value, $attr, $args)) {
-            return [$value, false];
+        if (! is_string($value) || ! $this->c->Csrf->verify($value, $attr, $args)) {
+            $this->addError('Bad token', 'e');
+            return null;
         } else {
-            return [$value, ['Bad token', 'e']];
+            return $value;
         }
     }
 
     protected function vCheckbox($v, $value)
     {
-        return [! empty($value), false];
+        return ! empty($value);
     }
 
     protected function vReferer($v, $value, $attr, $args)
@@ -525,39 +564,42 @@ class Validator
         if (! is_array($args)) {
             $args = [];
         }
-        return [$this->c->Router->validate($value, $attr, $args), false];
+        return $this->c->Router->validate($value, $attr, $args);
     }
 
     protected function vEmail($v, $value)
     {
         if (null === $value) {
-            return [$value, false];
+            return null;
         }
         $email = $this->c->Mail->valid($value, true);
         if (false === $email) {
-            return [(string) $value, 'The :alias is not valid email'];
+            $this->addError('The :alias is not valid email');
+            return $value;
         } else {
-            return [$email, false];
+            return $email;
         }
     }
 
     protected function vSame($v, $value, $attr)
     {
         if (! $this->getStatus($attr) || $value === $this->__get($attr)) {
-            return [$value, false];
+            return $value;
         } else {
-            return [null, 'The :alias must be same with original'];
+            $this->addError('The :alias must be same with original');
+            return null;
         }
     }
 
     protected function vRegex($v, $value, $attr)
     {
-        if (null === $value) {
-            return [$value, false];
-        } elseif (is_string($value) && preg_match($attr, $value)) {
-            return [$value, false];
+        if (null !== $value 
+            && (! is_string($value) || ! preg_match($attr, $value))
+        ) {
+            $this->addError('The :alias is not valid format');
+            return null;
         } else {
-            return [null, 'The :alias is not valid format'];
+            return $value;
         }
     }
 
@@ -573,19 +615,17 @@ class Validator
 
     protected function vIn($v, $value, $attr)
     {
-        if (null === $value || in_array($value, explode(',', $attr))) {
-            return [$value, false];
-        } else {
-            return [$value, 'The :alias contains an invalid value'];
+        if (null !== $value && ! in_array($value, explode(',', $attr))) {
+            $this->addError('The :alias contains an invalid value');
         }
+        return $value;
     }
 
     protected function vNotIn($v, $value, $attr)
     {
-        if (null === $value || ! in_array($value, explode(',', $attr))) {
-            return [$value, false];
-        } else {
-            return [$value, 'The :alias contains an invalid value'];
+        if (null !== $value && in_array($value, explode(',', $attr))) {
+            $this->addError('The :alias contains an invalid value');
         }
+        return $value;
     }
 }

+ 55 - 0
app/Models/Forum/CalcStat.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace ForkBB\Models\Forum;
+
+use ForkBB\Models\MethodModel;
+use RuntimeException;
+
+class CalcStat extends MethodModel
+{
+    /**
+     * Пересчитывает статистику
+     * 
+     * @throws RuntimeException
+     * 
+     * @return Forum
+     */
+    public function calcStat()
+    {
+        if ($this->model->id < 1) {
+            throw new RuntimeException('The model does not have ID');
+        }
+
+        $vars = [':fid' => $this->model->id];
+        $sql = 'SELECT COUNT(id) as num_topics, SUM(num_replies) as num_replies 
+                FROM ::topics 
+                WHERE forum_id=?i:fid';
+
+        $result = $this->c->DB->query($sql, $vars)->fetch();
+
+        $this->model->num_topics = $result['num_topics'];
+        $this->model->num_posts  = $result['num_topics'] + $result['num_replies'];
+
+        $sql = 'SELECT last_post, last_post_id, last_poster, subject as last_topic
+                FROM ::topics 
+                WHERE forum_id=?i:fid AND moved_to IS NULL 
+                ORDER BY last_post DESC 
+                LIMIT 1';
+
+        $result = $this->c->DB->query($sql, $vars)->fetch();
+
+        if (empty($result)) {
+            $this->model->last_post    = null;
+            $this->model->last_post_id = null;
+            $this->model->last_poster  = null;
+            $this->model->last_topic   = null;
+        } else {
+            $this->model->last_post    = $result['last_post'];
+            $this->model->last_post_id = $result['last_post_id'];
+            $this->model->last_poster  = $result['last_poster'];
+            $this->model->last_topic   = $result['last_topic'];
+        }
+
+        return $this->model;
+    }
+}

+ 79 - 0
app/Models/Forum/Save.php

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

+ 9 - 11
app/Models/Pages/Auth.php

@@ -112,12 +112,11 @@ class Auth extends Page
      */
     public function vLoginProcess(Validator $v, $password)
     {
-        $error = false;
         if (! empty($v->getErrors())) {
         } elseif (! ($user = $this->c->ModelUser->load($v->username, 'username')) instanceof User) {
-            $error = __('Wrong user/pass');
+            $v->addError('Wrong user/pass');
         } elseif ($user->isUnverified) {
-            $error = [__('Account is not activated'), 'w'];
+            $v->addError('Account is not activated', 'w');
         } else {
             $authorized = false;
             $hash = $user->password;
@@ -133,7 +132,7 @@ class Auth extends Page
             }
             // ошибка в пароле
             if (! $authorized) {
-                $error = __('Wrong user/pass');
+                $v->addError('Wrong user/pass');
             } else {
                 // перезаписываем ip админа и модератора - Visman
                 if ($user->isAdmMod
@@ -149,7 +148,7 @@ class Auth extends Page
                 $this->c->Cookie->setUser($user);
             }
         }
-        return [$password, $error];
+        return $password;
     }
 
     /**
@@ -248,26 +247,25 @@ class Auth extends Page
     public function vCheckEmail(Validator $v, $email)
     {
         if (! empty($v->getErrors())) {
-            return [$email, false];
+            return $email;
         }
             
-        $error = false;
         $user = $this->c->ModelUser;
         $user->__email = $email;
 
         // email забанен
         if ($this->c->bans->isBanned($user) > 0) {
-            $error = __('Banned email');
+            $v->addError('Banned email');
         // нет пользователя с таким email
         } elseif (! $user->load($email, 'email') instanceof User) {
-            $error = __('Invalid email');
+            $v->addError('Invalid email');
         // за последний час уже был запрос на этот email
         } elseif (! empty($user->last_email_sent) && time() - $user->last_email_sent < 3600) {
-            $error = [__('Email flood', (int) (($user->last_email_sent + 3600 - time()) / 60)), 'e'];
+            $v->addError(__('Email flood', (int) (($user->last_email_sent + 3600 - time()) / 60)), 'e');
         } else {
             $this->tmpUser = $user;
         }
-        return [$email, $error];
+        return $email;
     }
 
     /**

+ 221 - 54
app/Models/Pages/Post.php

@@ -3,6 +3,7 @@
 namespace ForkBB\Models\Pages;
 
 use ForkBB\Core\Validator;
+use ForkBB\Models\Model;
 use ForkBB\Models\Page;
 
 class Post extends Page
@@ -21,16 +22,7 @@ class Post extends Page
         $forum = $this->c->forums->forum($args['id']);
 
         // раздел отсутствует в доступных или является ссылкой
-        if (empty($forum) || $forum->redirect_url) {
-            return $this->c->Message->message('Bad request');
-        }
-
-        $user = $this->c->user;
-
-        if (! $user->isAdmin
-            && (null === $forum->post_topics && $user->g_post_topics == '0' || $forum->post_topics == '0')
-            && ! $user->isModerator($forum)
-        ) {
+        if (empty($forum) || $forum->redirect_url || ! $forum->canCreateTopic) {
             return $this->c->Message->message('Bad request');
         }
 
@@ -42,53 +34,87 @@ class Post extends Page
         $this->robots    = 'noindex';
         $this->crumbs    = $this->crumbs(__('Post new topic'), $forum);
         $this->form      = $this->messageForm($forum, 'NewTopic', $args, true);
+        $this->titleForm = __('Post new topic');
         
         return $this;
     }
 
     public function newTopicPost(array $args)
     {
+        $forum = $this->c->forums->forum($args['id']);
+        
+        // раздел отсутствует в доступных или является ссылкой
+        if (empty($forum) || $forum->redirect_url || ! $forum->canCreateTopic) {
+            return $this->c->Message->message('Bad request');
+        }
+
         $this->c->Lang->load('post');
 
-        if ($this->c->user->isGuest) {
-            $ruleEmail    = ($this->c->config->p_force_guest_email == '1' ? 'required|' : '') . 'string:trim,lower|email|check_email';
-            $ruleUsername = 'required|string:trim,spaces|min:2|max:25|login|check_username';
-        } else {
-            $ruleEmail    = 'absent';
-            $ruleUsername = 'absent';
-        }
-            
-        $v = $this->c->Validator->addValidators([
-            'check_email'    => [$this, 'vCheckEmail'],
-            'check_username' => [$this, 'vCheckUsername'],
-            'check_subject'  => [$this, 'vCheckSubject'],
-        ])->setRules([
-            'token'    => 'token:NewTopic',
-            'message'  => 'required|string:trim|max:65536',
-            'email'    => [$ruleEmail, __('Email')],
-            'username' => [$ruleUsername, __('Username')],
-            'subject'  => ['required|string:trim,spaces|min:1|max:70|check_subject', __('Subject')],
-        ])->setArguments([
-            'token' => $args,
-        ])->setMessages([
-            'username.login'    => __('Login format'),
-        ]);
+        $v = $this->messageValidator($forum, 'NewTopic', $args, true);
 
-        if (! $v->validation($_POST)) {
+        if (! $v->validation($_POST) || isset($v->preview)) {
             $this->fIswev = $v->getErrors();
             $args['_vars'] = $v->getData();
             return $this->newTopic($args);
         }
 
+        $now = time();
+        $poster = $v->username ?: $this->c->user->username;
 
-        exit('ok');
+        // создание темы
+        $topic = $this->c->ModelTopic;
+
+        $topic->subject     = $v->subject;
+        $topic->poster      = $poster;
+        $topic->last_poster = $poster;
+        $topic->posted      = $now;
+        $topic->last_post   = $now;
+        $topic->sticky      = $v->stick_topic ? 1 : 0;
+        $topic->stick_fp    = $v->stick_fp ? 1 : 0;
+#       $topic->poll_type = ;
+#       $topic->poll_time = ;
+#       $topic->poll_term = ;
+#       $topic->poll_kol = ;
+
+        $topic->insert();
+
+        // создание сообщения
+        $post = $this->c->ModelPost;
+
+        $post->poster       = $poster;
+        $post->poster_id    = $this->c->user->id;
+        $post->poster_ip    = $this->c->user->ip;
+        $post->poster_email = $v->email;
+        $post->message      = $v->message; //?????
+        $post->hide_smilies = $v->hide_smilies ? 1 : 0;
+#       $post->edit_post    =
+        $post->posted       = $now;
+#       $post->edited       =
+#       $post->edited_by    =
+        $post->user_agent   = $this->c->user->userAgent;
+        $post->topic_id     = $topic->id;
+
+        $post->insert();
+
+        // обновление созданной темы
+        $topic->forum_id      = $forum->id; //????
+        $topic->first_post_id = $post->id;
+        $topic->last_post_id  = $post->id;
+
+        $topic->update();
+        
+        $forum->calcStat()->update();
+        
+        return $this->c->Redirect
+            ->page('Topic', ['id' => $topic->id, 'name' => $topic->cens()->subject])
+            ->message(__('Post redirect'));
     }
 
     public function newReply(array $args)
     {
-        $topic = $this->c->ModelTopic->load($args['id']); //????
+        $topic = $this->c->ModelTopic->load((int) $args['id']);
 
-        if (empty($topic->id) || $topic->moved_to || ! $topic->canReply) { //????
+        if (empty($topic) || $topic->moved_to || ! $topic->canReply) {
             return $this->c->Message->message('Bad request');
         }
 
@@ -100,13 +126,31 @@ class Post extends Page
         $this->robots    = 'noindex';
         $this->crumbs    = $this->crumbs(__('Post a reply'), $topic);
         $this->form      = $this->messageForm($topic, 'NewReply', $args);
+        $this->titleForm = __('Post a reply');
                 
         return $this;
     }
 
     public function newReplyPost(array $args)
     {
+        $topic = $this->c->ModelTopic->load((int) $args['id']);
+        
+        if (empty($topic) || $topic->moved_to || ! $topic->canReply) {
+            return $this->c->Message->message('Bad request');
+        }
         
+        $this->c->Lang->load('post');
+                
+        $v = $this->messageValidator($topic, 'NewReply', $args);
+
+        if (! $v->validation($_POST) || isset($v->preview)) {
+            $this->fIswev = $v->getErrors();
+            $args['_vars'] = $v->getData();
+            return $this->newReply($args);
+        }
+
+
+        exit('ok');
     }
 
     /**
@@ -119,15 +163,14 @@ class Post extends Page
      */
     public function vCheckEmail(Validator $v, $email)
     {
-        $error = false;
         $user = $this->c->ModelUser;
         $user->__email = $email;
 
         // email забанен
         if ($this->c->bans->isBanned($user) > 0) {
-            $error = __('Banned email');
+            $v->addError('Banned email');
         }
-        return [$email, $error];
+        return $email;
     }
 
     /**
@@ -140,43 +183,167 @@ class Post extends Page
      */
     public function vCheckUsername(Validator $v, $username)
     {
-        $error = false;
         $user = $this->c->ModelUser;
         $user->__username = $username;
 
         // username = Гость
         if (preg_match('%^(guest|' . preg_quote(__('Guest'), '%') . ')$%iu', $username)) {
-            $error = __('Username guest');
+            $v->addError('Username guest');
         // цензура
         } elseif ($user->cens()->$username !== $username) {
-            $error = __('Username censor');
+            $v->addError('Username censor');
         // username забанен
         } elseif ($this->c->bans->isBanned($user) > 0) {
-            $error = __('Banned username');
+            $v->addError('Banned username');
         }
-        return [$username, $error];
+        return $username;
     }
 
     /**
      * Дополнительная проверка subject
      * 
      * @param Validator $v
-     * @param string $username
+     * @param string $subject
      * 
      * @return array
      */
     public function vCheckSubject(Validator $v, $subject)
     {
-        $error = false;
         if ($this->c->censorship->censor($subject) == '') {
-            $error = __('No subject after censoring');
-        } elseif ($this->c->config->p_subject_all_caps == '0' 
-            && mb_strtolower($subject, 'UTF-8') !== $subject
-            && mb_strtoupper($subject, 'UTF-8') === $subject
+            $v->addError('No subject after censoring');
+        } elseif (! $this->tmpAdmMod
+            && $this->c->config->p_subject_all_caps == '0'
+            && preg_match('%\p{Lu}%u', $subject)
+            && ! preg_match('%\p{Ll}%u', $subject)
+        ) {
+            $v->addError('All caps subject');
+        }
+        return $subject;
+    }
+
+    /**
+     * Дополнительная проверка message
+     * 
+     * @param Validator $v
+     * @param string $message
+     * 
+     * @return array
+     */
+    public function vCheckMessage(Validator $v, $message)
+    {
+        if ($this->c->censorship->censor($message) == '') {
+            $v->addError('No message after censoring');
+        } elseif (! $this->tmpAdmMod
+            && $this->c->config->p_message_all_caps == '0'
+            && preg_match('%\p{Lu}%u', $message)
+            && ! preg_match('%\p{Ll}%u', $message)
         ) {
-            $error = __('All caps subject');
+            $v->addError('All caps message');
+        } else {
+            
+            $bbWList = $this->c->config->p_message_bbcode == '1' ? null : [];
+            $bbBList = $this->c->config->p_message_img_tag == '1' ? [] : ['img'];
+
+            $this->c->Parser->setAttr('isSign', false)
+                ->setWhiteList($bbWList)
+                ->setBlackList($bbBList)
+                ->parse($message, ['strict' => true])
+                ->stripEmptyTags(" \n\t\r\v", true);
+
+            if ($this->c->config->o_make_links == '1') {
+                $this->c->Parser->detectUrls();
+            }
+
+            if ($v->hide_smilies != '1' && $this->c->config->o_smilies == '1') {
+                $this->c->Parser->detectSmilies();
+            }
+
+            $errors = $this->c->Parser->getErrors();
+            if ($errors) {
+                foreach($errors as $error) {
+                    $v->addError($error);
+                } 
+            } else {
+                $this->parser = $this->c->Parser;
+            }
+        }
+
+        return $message;
+    }
+
+    /**
+     * Проверка данных поступивших из формы сообщения
+     * 
+     * @param Model $model
+     * @param string $marker
+     * @param array $args
+     * @param bool $editSubject
+     * 
+     * @return Validator
+     */
+    protected function messageValidator(Model $model, $marker, array $args, $editSubject = false)
+    {
+        if ($this->c->user->isGuest) {
+            $ruleEmail    = ($this->c->config->p_force_guest_email == '1' ? 'required|' : '') . 'string:trim,lower|email|check_email';
+            $ruleUsername = 'required|string:trim,spaces|min:2|max:25|login|check_username';
+        } else {
+            $ruleEmail    = 'absent';
+            $ruleUsername = 'absent';
+        }
+
+        if ($editSubject) {
+            $ruleSubject = 'required|string:trim,spaces|min:1|max:70|check_subject';
+        } else {
+            $ruleSubject = 'absent';
+        }
+
+        if ($this->c->user->isAdmin || $this->c->user->isModerator($model)) {
+            $this->tmpAdmMod   = true;
+            $ruleStickTopic    = 'checkbox';
+
+            if ($editSubject) {
+                $ruleStickFP   = 'checkbox';
+                $ruleMergePost = 'absent';
+            } else {
+                $ruleStickFP   = 'absent';
+                $ruleMergePost = 'checkbox';
+            }
+        } else {
+            $ruleStickTopic    = 'absent';
+            $ruleStickFP       = 'absent';
+            $ruleMergePost     = 'absent';
+        }
+
+        if ($this->c->config->o_smilies == '1') {
+            $ruleHideSmilies = 'checkbox';
+        } else {
+            $ruleHideSmilies = 'absent';
         }
-        return [$subject, $error];
+            
+        $v = $this->c->Validator->addValidators([
+            'check_email'    => [$this, 'vCheckEmail'],
+            'check_username' => [$this, 'vCheckUsername'],
+            'check_subject'  => [$this, 'vCheckSubject'],
+            'check_message'  => [$this, 'vCheckMessage'],
+        ])->setRules([
+            'token'        => 'token:' . $marker,
+            'email'        => [$ruleEmail, __('Email')],
+            'username'     => [$ruleUsername, __('Username')],
+            'subject'      => [$ruleSubject, __('Subject')],
+            'stick_topic'  => $ruleStickTopic,
+            'stick_fp'     => $ruleStickFP,
+            'merge_post'   => $ruleMergePost,
+            'hide_smilies' => $ruleHideSmilies,
+            'submit'       => 'string', //????
+            'preview'      => 'string', //????
+            'message'      => 'required|string:trim|max:65536|check_message',
+        ])->setArguments([
+            'token' => $args,
+        ])->setMessages([
+            'username.login'    => __('Login format'),
+        ]);
+
+        return $v;
     }
 
     /**
@@ -189,7 +356,7 @@ class Post extends Page
      * 
      * @return array
      */
-    protected function messageForm($model, $marker, array $args, $editSubject = false)
+    protected function messageForm(Model $model, $marker, array $args, $editSubject = false)
     {
         $vars = isset($args['_vars']) ? $args['_vars'] : null;
         unset($args['_vars']);

+ 9 - 10
app/Models/Pages/Register.php

@@ -72,18 +72,17 @@ class Register extends Page
      */
     public function vCheckEmail(Validator $v, $email)
     {
-        $error = false;
         $user = $this->c->ModelUser;
         $user->__email = $email;
 
         // email забанен
         if ($this->c->bans->isBanned($user) > 0) {
-            $error = __('Banned email');
+            $v->addError('Banned email');
         // найден хотя бы 1 юзер с таким же email
         } elseif (empty($v->getErrors()) && $user->load($email, 'email') !== 0) {
-            $error = __('Dupe email');
+            $v->addError('Dupe email');
         }
-        return [$email, $error];
+        return $email;
     }
 
     /**
@@ -96,24 +95,23 @@ class Register extends Page
      */
     public function vCheckUsername(Validator $v, $username)
     {
-        $error = false;
         $user = $this->c->ModelUser;
         $user->__username = $username;
 
         // username = Гость
         if (preg_match('%^(guest|' . preg_quote(__('Guest'), '%') . ')$%iu', $username)) {
-            $error = __('Username guest');
+            $v->addError('Username guest');
         // цензура
         } elseif ($this->c->censorship->censor($username) !== $username) {
-            $error = __('Username censor');
+            $v->addError('Username censor');
         // username забанен
         } elseif ($this->c->bans->isBanned($user) > 0) {
-            $error = __('Banned username');
+            $v->addError('Banned username');
         // есть пользователь с похожим именем
         } elseif (empty($v->getErrors()) && ! $user->isUnique()) {
-            $error = __('Username not unique');
+            $v->addError('Username not unique');
         }
-        return [$username, $error];
+        return $username;
     }
 
     /**
@@ -134,6 +132,7 @@ class Register extends Page
         }
 
         $user = $this->c->ModelUser;
+        
         $user->username        = $v->username;
         $user->password        = password_hash($v->password, PASSWORD_DEFAULT);
         $user->group_id        = $groupId;

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

@@ -106,7 +106,7 @@ class Topic extends Page
     {
         $topic = $this->c->ModelTopic->load((int) $args['id'], $type === 'post');
 
-        if (! $topic->id || ! $topic->last_post_id) {
+        if (empty($topic) || ! $topic->last_post_id) {
             return $this->c->Message->message('Bad request');
         }
 
@@ -189,7 +189,7 @@ class Topic extends Page
 
             $fieldset = [];
             if ($user->isAdmin || $user->isModerator($topic)) {
-                $fieldset['merge'] = [
+                $fieldset['merge_post'] = [
                     'type'    => 'checkbox',
                     'label'   => __('Merge posts'),
                     'value'   => '1',

+ 79 - 0
app/Models/Post/Save.php

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

+ 3 - 3
app/Models/Topic/Load.php

@@ -12,7 +12,7 @@ class Load extends MethodModel
      * @param int $id
      * @param bool $isPost
      *
-     * @return Topic
+     * @return null|Topic
      */
     public function load($id, $isPost = false)
     {
@@ -60,7 +60,7 @@ class Load extends MethodModel
 
         // тема отсутствует или недоступна
         if (empty($data)) {
-            return $this->model->setAttrs([]);
+            return null;
         }
 
         if (! $this->c->user->isGuest) {
@@ -72,7 +72,7 @@ class Load extends MethodModel
 
         // раздел недоступен
         if (empty($forum)) {
-            return $this->model->setAttrs([]);
+            return null;
         }
 
         if (! empty($forForum)) {

+ 79 - 0
app/Models/Topic/Save.php

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

+ 13 - 1
app/Models/User/LoadUserFromCookie.php

@@ -88,6 +88,7 @@ class LoadUserFromCookie extends MethodModel
         }
         $this->model->setAttrs($data);
         $this->model->__ip = $ip;
+        $this->model->__userAgent = $this->getUserAgent();
     }
 
     /**
@@ -100,6 +101,17 @@ class LoadUserFromCookie extends MethodModel
        return filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) ?: 'unknow';
     }
 
+    /**
+     * Возврат юзер агента браузера пользователя
+     * 
+     * @return string
+     */
+    protected function getUserAgent()
+    {
+        $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
+        return is_string($ua) ? trim($ua) : '';
+    }
+
     /**
      * Проверка на робота
      * Если робот, то возврат имени
@@ -108,7 +120,7 @@ class LoadUserFromCookie extends MethodModel
      */
     protected function isBot()
     {
-        $agent = trim($_SERVER['HTTP_USER_AGENT']);
+        $agent = $this->getUserAgent();
         if ($agent == '') {
             return false;
         }

+ 13 - 5
app/Models/User/Save.php

@@ -11,15 +11,17 @@ class Save extends MethodModel
      * Обновляет данные пользователя
      *
      * @throws RuntimeException
+     * 
+     * @return User
      */
-    public function save()
+    public function update()
     {
         if (empty($this->model->id)) {
             throw new RuntimeException('The model does not have ID');
         }
         $modified = $this->model->getModified();
         if (empty($modified)) {
-            return;
+            return $this->model;
         }
         $values = $this->model->getAttrs();
         $fileds = $this->c->dbMap->users;
@@ -32,17 +34,21 @@ class Save extends MethodModel
             $set[] = $name . '=?' . $fileds[$name];
         }
         if (empty($set)) {
-            return;
+            return $this->model;
         }
         $vars[] = $this->model->id;
         $this->c->DB->query('UPDATE ::users SET ' . implode(', ', $set) . ' WHERE id=?i', $vars);
         $this->model->resModified();
+
+        return $this->model;
     }
 
     /**
      * Добавляет новую запись в таблицу пользователей
      *
      * @throws RuntimeException
+     * 
+     * @return int
      */
     public function insert()
     {
@@ -62,10 +68,12 @@ class Save extends MethodModel
             $set2[] = '?' . $fileds[$name];
         }
         if (empty($set)) {
-            return;
+            throw new RuntimeException('The model is empty');
         }
         $this->c->DB->query('INSERT INTO ::users (' . implode(', ', $set) . ') VALUES (' . implode(', ', $set2) . ')', $vars);
+        $this->model->id = $this->c->DB->lastInsertId();
         $this->model->resModified();
-        return $this->c->DB->lastInsertId();
+
+        return $this->model->id;
     }
 }

+ 10 - 3
app/templates/post.tpl

@@ -15,12 +15,19 @@
     <div class="f-nav-links">
 @yield ('crumbs')
     </div>
-    <section class="f-main f-topic">
-      <h2>{{ '' }}</h2>
+@if ($p->parser)
+    <section class="f-main f-preview">
+      <h2>{!! __('Post preview') !!}</h2>
+      <div class="f-post-body clearfix">
+        <div class="f-post-right f-post-main">
+          {!! $p->parser->getHtml() !!}
+        </div>
+      </div>
     </section>
+@endif
 @if ($form = $p->form)
     <section class="post-form">
-      <h2>{!! __('Quick post') !!}</h2>
+      <h2>{!! $p->titleForm !!}</h2>
       <div class="f-fdiv">
   @include ('layouts/form')
       </div>

+ 33 - 1
public/style/ForkBB/style.css

@@ -928,6 +928,7 @@ select {
 .f-ftlist .f-cell {
   padding-top: 0.625rem;
   padding-bottom: 0.625rem;
+  overflow: hidden;
 }
 
 .f-ftlist .f-hcell {
@@ -1285,13 +1286,44 @@ li + li .f-btn {
   }
 } /* @media screen and (min-width: 50rem) */
 
+/****************/
+/* Предпросмотр */
+/****************/
+.f-preview {
+  border-top: 0.0625rem solid #AA7939;
+}
+
+.f-preview h2 {
+  padding: 0.625rem;
+  font-family: Arial, Helvetica, sans-serif;
+  font-size: 1rem;
+  line-height: 1.5;
+  border-bottom: 0.0625rem dotted #AA7939;
+  background-color: #F8F4E3;
+}
+
+.f-preview .f-post-body {
+  float: none;
+  margin: 0;
+}
+
+.f-preview .f-post-right {
+  padding: 0.625rem;
+}
+
 /*****************************/
 .post-form {
   border-top: 0.0625rem solid #AA7939;
 }
 
 .post-form > h2 {
-  display: none;
+/*  display: none; */
+  margin-bottom: -0.625rem;
+  padding-left: 0.625rem;
+  font-family: Arial, Helvetica, sans-serif;
+  font-size: 1rem;
+  padding-right: 0.625rem;
+  margin-top: 1rem;
 }
 
 .post-form > .f-fdiv {