瀏覽代碼

Mail, Auth, Register, UserMapper, Validator

Visman 8 年之前
父節點
當前提交
a1f5f61a50
共有 43 個文件被更改,包括 1616 次插入439 次删除
  1. 3 1
      app/Controllers/Routing.php
  2. 1 1
      app/Core/Container.php
  3. 5 0
      app/Core/Install.php
  4. 425 4
      app/Core/Mail.php
  5. 3 2
      app/Core/Router.php
  6. 2 5
      app/Models/Actions/CacheGenerator.php
  7. 59 44
      app/Models/Actions/CheckBans.php
  8. 135 119
      app/Models/Pages/Auth.php
  9. 20 8
      app/Models/Pages/Page.php
  10. 251 0
      app/Models/Pages/Register.php
  11. 27 1
      app/Models/Pages/Rules.php
  12. 0 12
      app/Models/User.php
  13. 29 5
      app/Models/UserMapper.php
  14. 243 133
      app/Models/Validator.php
  15. 12 13
      app/bootstrap.php
  16. 30 3
      app/lang/English/auth.po
  17. 7 1
      app/lang/English/common.po
  18. 3 3
      app/lang/English/mail/new_user.tpl
  19. 3 5
      app/lang/English/mail/password_reset.tpl
  20. 5 6
      app/lang/English/mail/welcome.tpl
  21. 3 0
      app/lang/English/profile.po
  22. 70 0
      app/lang/English/register.po
  23. 30 3
      app/lang/Russian/auth.po
  24. 7 1
      app/lang/Russian/common.po
  25. 4 4
      app/lang/Russian/mail/new_user.tpl
  26. 6 8
      app/lang/Russian/mail/password_reset.tpl
  27. 5 6
      app/lang/Russian/mail/welcome.tpl
  28. 3 0
      app/lang/Russian/profile.po
  29. 70 0
      app/lang/Russian/register.po
  30. 13 7
      app/templates/change_password.tpl
  31. 1 1
      app/templates/layouts/main.tpl
  32. 1 1
      app/templates/layouts/redirect.tpl
  33. 17 9
      app/templates/login.tpl
  34. 9 5
      app/templates/password_reset.tpl
  35. 29 0
      app/templates/register.tpl
  36. 15 2
      app/templates/rules.tpl
  37. 5 0
      composer.json
  38. 2 2
      composer.lock
  39. 1 0
      db_update.php
  40. 2 6
      include/functions.php
  41. 51 18
      public/style/ForkBB/style.css
  42. 1 0
      vendor/composer/autoload_psr4.php
  43. 8 0
      vendor/composer/autoload_static.php

+ 3 - 1
app/Controllers/Routing.php

@@ -45,7 +45,9 @@ class Routing
 
 
             // регистрация
             // регистрация
             if ($config['o_regs_allow'] == '1') {
             if ($config['o_regs_allow'] == '1') {
-                $r->add('GET', '/registration', 'Registration:reg', 'Registration'); //????
+                $r->add('GET', '/registration', 'Rules:confirmation', 'Register');
+                $r->add('POST', '/registration/agree', 'Register:reg', 'RegisterForm');
+                $r->add('GET', '/registration/activate/{id:\d+}/{key}/{hash}', 'Register:activate', 'RegActivate');
             }
             }
         } else {
         } else {
             // выход
             // выход

+ 1 - 1
app/Core/Container.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 /**
 /**
- * based on code Container https://github.com/artoodetoo/container
+ * based on Container https://github.com/artoodetoo/container
  * by artoodetoo
  * by artoodetoo
  */
  */
 namespace ForkBB\Core;
 namespace ForkBB\Core;

+ 5 - 0
app/Core/Install.php

@@ -1520,6 +1520,11 @@ foreach ($styles as $temp)
                         'allow_null'    => false,
                         'allow_null'    => false,
                         'default'        => '\'\''
                         'default'        => '\'\''
                     ),
                     ),
+                    'email_confirmed'        => array(
+                        'datatype'        => 'TINYINT(1)',
+                        'allow_null'    => false,
+                        'default'        => '0'
+                    ),
                     'title'                => array(
                     'title'                => array(
                         'datatype'        => 'VARCHAR(50)',
                         'datatype'        => 'VARCHAR(50)',
                         'allow_null'    => true
                         'allow_null'    => true

+ 425 - 4
app/Core/Mail.php

@@ -2,6 +2,8 @@
 
 
 namespace ForkBB\Core;
 namespace ForkBB\Core;
 
 
+use RuntimeException;
+
 class Mail
 class Mail
 {
 {
     /**
     /**
@@ -14,6 +16,68 @@ class Mail
      */
      */
     protected $language;
     protected $language;
 
 
+    /**
+     * @var array
+     */
+    protected $to = [];
+
+    /**
+     * @var array
+     */
+    protected $headers = [];
+
+    /**
+     * @var string
+     */
+    protected $message;
+
+    /**
+     * @var array
+     */
+    protected $smtp;
+
+    /**
+     * @var string
+     */
+    protected $EOL;
+
+    /**
+     * @var Resource
+     */
+    protected $connect;
+
+    /**
+     * var int
+     */
+    protected $auth = 0;
+
+    /**
+     * Конструктор
+     * @param mixed $host
+     * @param mixed $user
+     * @param mixed $pass
+     * @param mixed $ssl
+     * @param mixed $eol
+     */
+    public function __construct($host, $user, $pass, $ssl, $eol)
+    {
+        if (is_string($host) && strlen(trim($host)) > 0 ) {
+            list ($host, $port) = explode(':', $host);
+            if (empty($port) || $port < 1 || $port > 65535) {
+                $port = 25;
+            }
+            $this->smtp = [
+                'host' => ($ssl ? 'ssl://' : '') . $host,
+                'port' => (int) $port,
+                'user' => (string) $user,
+                'pass' => (string) $pass,
+            ];
+            $this->EOL = "\r\n";
+        } else {
+            $this->EOL = in_array($eol, ["\r\n", "\n", "\r"]) ? $eol : PHP_EOL;
+        }
+    }
+
     /**
     /**
      * Валидация email
      * Валидация email
      * @param mixed $email
      * @param mixed $email
@@ -23,9 +87,152 @@ class Mail
     {
     {
         return is_string($email)
         return is_string($email)
             && strlen($email) <= 80
             && strlen($email) <= 80
+            && trim($email) === $email
             && preg_match('%^.+@.+$%D', $email);
             && preg_match('%^.+@.+$%D', $email);
     }
     }
 
 
+    /**
+     * Сброс
+     * @return Mail
+     */
+    public function reset()
+    {
+        $this->to = [];
+        $this->headers = [];
+        $this->message = null;
+        return $this;
+    }
+
+    /**
+     * Задает тему письма
+     * @param string $subject
+     * @return Mail
+     */
+    public function setSubject($subject)
+    {
+        $this->headers['Subject'] = $this->encodeText(preg_replace('%[\x00-\x1F]%', '', trim($subject)));
+        return $this;
+    }
+
+    /**
+     * Добавляет заголовок To
+     * @param string|array $email
+     * @param string $name
+     * @return Mail
+     */
+    public function addTo($email, $name = null)
+    {
+        if (is_array($email)) {
+        } else {
+            $email = preg_split('%[,\n\r]%', (string) $email, -1, PREG_SPLIT_NO_EMPTY);
+        }
+        foreach($email as $cur) {
+            $cur = trim((string) $cur);
+            if ($this->valid($cur)) {
+                $this->to[] = $this->formatAddress($cur, $name);
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Задает заголовок To
+     * @param string|array $email
+     * @param string $name
+     * @return Mail
+     */
+    public function setTo($email, $name = null)
+    {
+        $this->to = [];
+        return $this->addTo($email, $name);
+    }
+
+    /**
+     * Задает заголовок From
+     * @param string $email
+     * @param string $name
+     * @return Mail
+     */
+    public function setFrom($email, $name = null)
+    {
+        if ($this->valid($email)) {
+            $this->headers['From'] = $this->formatAddress($email, $name);
+        }
+        return $this;
+    }
+
+    /**
+     * Задает заголовок Reply-To
+     * @param string $email
+     * @param string $name
+     * @return Mail
+     */
+    public function setReplyTo($email, $name = null)
+    {
+        if ($this->valid($email)) {
+            $this->headers['Reply-To'] = $this->formatAddress($email, $name);
+        }
+        return $this;
+    }
+
+    /**
+     * Форматирование адреса
+     * @param string|array $email
+     * @param string $name
+     * @return string
+     */
+    protected function formatAddress($email, $name = null)
+    {
+        $email = $this->filterEmail($email);
+        if (null === $name || ! is_string($name) || strlen(trim($name)) == 0) {
+            return $email;
+        } else {
+            $name = $this->encodeText($this->filterName($name));
+            return sprintf('"%s" <%s>', $name, $email);
+        }
+    }
+
+    /**
+     * Кодирование заголовка/имени
+     * @param string $str
+     * @return string
+     */
+    protected function encodeText($str)
+    {
+        if (preg_match('%[^\x20-\x7F]%', $str)) {
+            return '=?UTF-8?B?' . base64_encode($str) . '?=';
+        } else {
+            return $str;
+        }
+    }
+
+    /**
+     * Фильтрация email
+     * @param string $email
+     * @return string
+     */
+    protected function filterEmail($email)
+    {
+        return preg_replace('%[\x00-\x1F",<>]%', '', $email);
+    }
+
+    /**
+     * Фильтрация имени
+     * @param string $name
+     * @return string
+     */
+    protected function filterName($name)
+    {
+        return strtr(trim($name), [
+            "\r" => '',
+            "\n" => '',
+            "\t" => '',
+            '"'  => '\'',
+            '<'  => '[',
+            '>'  => ']',
+        ]);
+    }
+
     /**
     /**
      * Установка папки для поиска шаблонов писем
      * Установка папки для поиска шаблонов писем
      * @param string $folder
      * @param string $folder
@@ -49,15 +256,229 @@ class Mail
     }
     }
 
 
     /**
     /**
-     * Отправка письма
-     * @param string $email
+     * Задает сообщение по шаблону
      * @param string $tpl
      * @param string $tpl
      * @param array $data
      * @param array $data
+     * @throws \RuntimeException
+     * @return Mail
+     */
+    public function setTpl($tpl, array $data)
+    {
+        $file = rtrim($this->folder, '\\/') . '/' . $this->language . '/mail/' . $tpl;
+        if (! file_exists($file)) {
+            throw new RuntimeException('Tpl not found');
+        }
+        $tpl = trim(file_get_contents($file));
+        foreach ($data as $key => $val) {
+            $tpl = str_replace('<' . $key . '>', (string) $val, $tpl);
+        }
+        list($subject, $tpl) = explode("\n", $tpl, 2);
+        if (! isset($tpl)) {
+            throw new RuntimeException('Tpl empty');
+        }
+        $this->setSubject(substr($subject, 8));
+        return $this->setMessage($tpl);
+    }
+
+    /**
+     * Задает сообщение
+     * @param string $message
+     * @throws \RuntimeException
+     * @return Mail
+     */
+    public function setMessage($message)
+    {
+        $this->message = str_replace("\0", $this->EOL,
+                         str_replace(["\r\n", "\n", "\r"], "\0",
+                         str_replace("\0", '', trim($message))));
+//        $this->message = wordwrap ($this->message, 75, $this->EOL, false);
+        return $this;
+    }
+
+    /**
+     * Отправляет письмо
+     * @return bool
+     */
+    public function send()
+    {
+        if (empty($this->to)) {
+            throw new RuntimeException('No recipient(s)');
+        }
+        if (empty($this->headers['From'])) {
+            throw new RuntimeException('No sender');
+        }
+        if (! isset($this->headers['Subject'])) {
+            throw new RuntimeException('Subject empty');
+        }
+        if (trim($this->message) == '') {
+            throw new RuntimeException('Message empty');
+        }
+
+        $this->headers = array_replace($this->headers, [
+            'Date' => gmdate('r'),
+            'MIME-Version' => '1.0',
+            'Content-transfer-encoding' => '8bit',
+            'Content-type' => 'text/plain; charset=utf-8',
+            'X-Mailer' => 'ForkBB Mailer',
+        ]);
+
+        if (is_array($this->smtp)) {
+            return $this->smtp();
+        } else {
+            return $this->mail();
+        }
+    }
+
+    /**
+     * Отправка письма через функцию mail
      * @return bool
      * @return bool
      */
      */
-    public function send($email, $tpl, array $data)
+    protected function mail()
+    {
+        $to = implode(', ', $this->to);
+        $subject = $this->headers['Subject'];
+        $headers = $this->headers;
+        unset($headers['Subject']);
+        $headers = $this->strHeaders($headers);
+        return @mail($to, $subject, $this->message, $headers);
+    }
+
+    /**
+     * Переводит заголовки из массива в строку
+     * @param array $headers
+     * @return string
+     */
+    protected function strHeaders(array $headers)
     {
     {
-        var_dump($data);
+        foreach ($headers as $key => &$value) {
+            $value = $key . ': ' . $value;
+        }
+        unset($value);
+        return join($this->EOL, $headers);
+    }
+
+    /**
+     * Отправка письма через smtp
+     * @throws \RuntimeException
+     * @return bool
+     */
+    protected function smtp()
+    {
+        // подлючение
+        if (! is_resource($this->connect)) {
+            if (($connect = @fsockopen($this->smtp['host'], $this->smtp['port'], $errno, $errstr, 5)) === false) {
+                throw new RuntimeException('Could not connect to smtp host "' . $this->smtp['host'] . '" (' . $errno . ') (' . $errstr . ')');
+            }
+            stream_set_timeout($connect, 5);
+            $this->connect = $connect;
+            $this->smtpData(null, '220');
+        }
+
+        $message = $this->EOL . str_replace("\n.", "\n..", $this->EOL . $this->message) . $this->EOL . '.';
+        $headers = $this->strHeaders($this->headers);
+
+        // цикл по получателям
+        foreach ($this->to as $to) {
+            $this->smtpHello();
+            $this->smtpData('MAIL FROM: <' . $this->getEmailFrom($this->headers['From']). '>', '250');
+            $this->smtpData('RCPT TO: <' . $this->getEmailFrom($to) . '>', ['250', '251']);
+            $this->smtpData('DATA', '354');
+            $this->smtpData('To: ' . $to . $this->EOL . $headers . $message, '250');
+            $this->smtpData('NOOP', '250');
+        }
         return true;
         return true;
     }
     }
+
+    public function __destruct()
+    {
+        // завершение сеанса smtp
+        if (is_resource($this->connect)) {
+            $this->smtpData('QUIT', null);
+            @fclose($this->connect);
+        }
+    }
+
+    /**
+     * Hello SMTP server
+     */
+    protected function smtpHello()
+    {
+        switch ($this->auth) {
+            case 1:
+                $this->smtpData('EHLO ' . $this->hostname(), '250');
+                return;
+            case 0:
+                if ($this->smtp['user'] != '' && $this->smtp['pass'] != '') {
+                   $code = $this->smtpData('EHLO ' . $this->hostname(), ['250', '500', '501', '502', '550']);
+                   if ($code === '250') {
+                       $this->smtpData('AUTH LOGIN', '334');
+                       $this->smtpData(base64_encode($this->smtp['user']), '334');
+                       $this->smtpData(base64_encode($this->smtp['pass']), '235');
+                       $this->auth = 1;
+                       return;
+                   }
+                }
+            default:
+                $this->auth = -1;
+                $this->smtpData('HELO ' . $this->hostname(), '250');
+        }
+    }
+
+    /**
+     * @param string $data
+     * @param mixed $code
+     * @throws \RuntimeException
+     * @return string
+     */
+    protected function smtpData($data, $code)
+    {
+//var_dump($data);
+        if (is_string($data)) {
+            @fwrite($this->connect, $data . $this->EOL);
+        }
+
+        $response = '';
+//        while (! isset($get{3}) || $get{3} !== ' ') {
+        while (is_resource($this->connect) && !feof($this->connect)) {
+            if (($get = @fgets($this->connect, 512)) === false) {
+                throw new RuntimeException('Couldn\'t get mail server response codes');
+            }
+            $response .= $get;
+            if (isset($get{3}) && $get{3} === ' ') {
+                $return = substr($get, 0, 3);
+                break;
+            }
+        }
+//var_dump($response);
+        if ($code !== null && ! in_array($return, (array) $code)) {
+            throw new RuntimeException('Unable to send email. Response of the SMTP server: "'.$get.'"');
+        }
+        return $return;
+    }
+
+    /**
+     * Выделяет email из заголовка
+     * @param string $str
+     * @return string
+     */
+    protected function getEmailFrom($str)
+    {
+        $match = explode('" <', $str);
+        if (count($match) == 2 && substr($match[1], -1) == '>') {
+            return rtrim($match[1], '>');
+        } else {
+            return $str;
+        }
+    }
+
+    /**
+     * Возвращает имя сервера или его ip
+     * @return string
+     */
+    protected function hostname()
+    {
+        return empty($_SERVER['SERVER_NAME'])
+            ? (isset($_SERVER['SERVER_ADDR']) ? '[' . $_SERVER['SERVER_ADDR'] . ']' : '[127.0.0.1]')
+            : $_SERVER['SERVER_NAME'];
+    }
 }
 }

+ 3 - 2
app/Core/Router.php

@@ -71,14 +71,15 @@ class Router
 
 
     /**
     /**
      * Проверка url на принадлежность форуму
      * Проверка url на принадлежность форуму
-     * @param string $url
+     * @param mixed $url
      * @param string $defMarker
      * @param string $defMarker
      * @param array $defArgs
      * @param array $defArgs
      * @return string
      * @return string
      */
      */
     public function validate($url, $defMarker, array $defArgs = [])
     public function validate($url, $defMarker, array $defArgs = [])
     {
     {
-        if (parse_url($url, PHP_URL_HOST) === $this->host
+        if (is_string($url)
+            && parse_url($url, PHP_URL_HOST) === $this->host
             && ($route = $this->route('GET', rawurldecode(parse_url($url, PHP_URL_PATH))))
             && ($route = $this->route('GET', rawurldecode(parse_url($url, PHP_URL_PATH))))
             && $route[0] === self::OK
             && $route[0] === self::OK
         ) {
         ) {

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

@@ -65,14 +65,11 @@ class CacheGenerator
         $search_for = $replace_with = [];
         $search_for = $replace_with = [];
         for ($i = 0; $i < $num_words; $i++) {
         for ($i = 0; $i < $num_words; $i++) {
             list($search_for[$i], $replace_with[$i]) = $this->db->fetch_row($result);
             list($search_for[$i], $replace_with[$i]) = $this->db->fetch_row($result);
-            $search_for[$i] = '%(?<=[^\p{L}\p{N}])('.str_replace('\*', '[\p{L}\p{N}]*?', preg_quote($search_for[$i], '%')).')(?=[^\p{L}\p{N}])%iu';
+            $search_for[$i] = '%(?<![\p{L}\p{N}])('.str_replace('\*', '[\p{L}\p{N}]*?', preg_quote($search_for[$i], '%')).')(?![\p{L}\p{N}])%iu';
         }
         }
         $this->db->free_result($result);
         $this->db->free_result($result);
 
 
-        return [
-            'search_for' => $search_for,
-            'replace_with' => $replace_with
-        ];
+        return [$search_for, $replace_with];
     }
     }
 
 
     /**
     /**

+ 59 - 44
app/Models/Actions/CheckBans.php

@@ -12,6 +12,11 @@ class CheckBans
      */
      */
     protected $c;
     protected $c;
 
 
+    /**
+     * Содержит массив с описание бана для проверяемого юзера
+     */
+    protected $ban;
+
     /**
     /**
      * Конструктор
      * Конструктор
      * @param Container $container
      * @param Container $container
@@ -27,54 +32,72 @@ class CheckBans
      */
      */
     public function check()
     public function check()
     {
     {
-        $bans = $this->c->bans;
         $user = $this->c->user;
         $user = $this->c->user;
 
 
         // Для админов и при отсутствии банов прекращаем проверку
         // Для админов и при отсутствии банов прекращаем проверку
-        if ($user->isAdmin || empty($bans)) {
-           return null;
+        if ($user->isAdmin) {
+            return null;
+        } elseif ($user->isGuest) {
+            $banned = $this->isBanned(null, null, $user->ip);
+        } else {
+            $banned = $this->isBanned($user->username, $user->email, $user->ip);
         }
         }
 
 
-        // Add a dot or a colon (depending on IPv4/IPv6) at the end of the IP address to prevent banned address
-        // 192.168.0.5 from matching e.g. 192.168.0.50
-        $userIp = $user->ip;
-        $add = strpos($userIp, '.') !== false ? '.' : ':';
-        $userIp .= $add;
+        if ($banned) {
+            $this->c->Online->delete($user); //???? а зачем это надо?
+            return $this->ban;
+        }
 
 
-        $username = mb_strtolower($user->username);
+        return null;
+    }
 
 
-        $banned = false;
+    /**
+     * Проверяет наличие бана на основании имени юзера, email и(или) ip
+     * Удаляет просроченные баны
+     * @param string $username
+     * @param string $email
+     * @param string $userIp
+     * @return int
+     */
+    public function isBanned($username = null, $email = null, $userIp = null)
+    {
+        $bans = $this->c->bans;
+        if (empty($bans)) {
+            return 0;
+        }
+        if (isset($username)) {
+            $username = mb_strtolower($username, 'UTF-8');
+        }
+        if (isset($userIp)) {
+            // Add a dot or a colon (depending on IPv4/IPv6) at the end of the IP address to prevent banned address
+            // 192.168.0.5 from matching e.g. 192.168.0.50
+            $add = strpos($userIp, '.') !== false ? '.' : ':';
+            $userIp .= $add;
+        }
+
+        $banned = 0;
         $remove = [];
         $remove = [];
+        $now = time();
 
 
-        foreach ($bans as $cur)
-        {
-            // Has this ban expired?
-            if ($cur['expire'] != '' && $cur['expire'] <= time())
-            {
+        foreach ($bans as $cur) {
+            if ($cur['expire'] != '' && $cur['expire'] < $now) {
                 $remove[] = $cur['id'];
                 $remove[] = $cur['id'];
                 continue;
                 continue;
-            } elseif ($banned) {
-                continue;
-            }
-
-            if (! $user->isGuest) {
-                if ($cur['username'] != '' && $username == mb_strtolower($cur['username'])) {
-                    $banned = $cur;
-                    continue;
-                } elseif ($cur['email'] != '' && $user->email == $cur['email']) {
-                    $banned = $cur;
-                    continue;
-                }
-            }
-
-            if ($cur['ip'] != '')
-            {
-                $ips = explode(' ', $cur['ip']);
-                foreach ($ips as $ip) {
+            } elseif (isset($username) && $cur['username'] != '' && $username == mb_strtolower($cur['username'])) {
+                $this->ban = $cur;
+                $banned = 1;
+                break;
+            } elseif (isset($email) && $cur['email'] != '' && $email == $cur['email']) {
+                $this->ban = $cur;
+                $banned = 2;
+                break;
+            } elseif (isset($userIp) && $cur['ip'] != '') {
+                foreach (explode(' ', $cur['ip']) as $ip) {
                     $ip .= $add;
                     $ip .= $add;
                     if (substr($userIp, 0, strlen($ip)) == $ip) {
                     if (substr($userIp, 0, strlen($ip)) == $ip) {
-                        $banned = $cur;
-                        break;
+                        $this->ban = $cur;
+                        $banned = 3;
+                        break 2;
                     }
                     }
                 }
                 }
             }
             }
@@ -87,14 +110,6 @@ class CheckBans
             $db->query('DELETE FROM '.$db->prefix.'bans WHERE id IN (' . implode(',', $remove) . ')') or error('Unable to delete expired ban', __FILE__, __LINE__, $db->error());
             $db->query('DELETE FROM '.$db->prefix.'bans WHERE id IN (' . implode(',', $remove) . ')') or error('Unable to delete expired ban', __FILE__, __LINE__, $db->error());
             $this->c->{'bans update'};
             $this->c->{'bans update'};
         }
         }
-
-        if ($banned)
-        {
-            //???? а зачем это надо?
-            $this->c->Online->delete($user);
-            return $banned;
-        }
-
-        return null;
+        return $banned;
     }
     }
 }
 }

+ 135 - 119
app/Models/Pages/Auth.php

@@ -2,6 +2,9 @@
 
 
 namespace ForkBB\Models\Pages;
 namespace ForkBB\Models\Pages;
 
 
+use ForkBB\Models\Validator;
+use ForkBB\Models\User;
+
 class Auth extends Page
 class Auth extends Page
 {
 {
     /**
     /**
@@ -22,6 +25,12 @@ class Auth extends Page
      */
      */
     protected $index = 'login';
     protected $index = 'login';
 
 
+    /**
+     * Для передачи User из vCheckEmail() в forgetPost()
+     * @var User
+     */
+    protected $tmpUser;
+
     /**
     /**
      * Выход пользователя
      * Выход пользователя
      * @param array $args
      * @param array $args
@@ -37,7 +46,7 @@ class Auth extends Page
         $this->c->Online->delete($this->c->user);
         $this->c->Online->delete($this->c->user);
         $this->c->UserMapper->updateLastVisit($this->c->user);
         $this->c->UserMapper->updateLastVisit($this->c->user);
 
 
-        $this->c->Lang->load('login');
+        $this->c->Lang->load('auth');
         return $this->c->Redirect->setPage('Index')->setMessage(__('Logout redirect'));
         return $this->c->Redirect->setPage('Index')->setMessage(__('Logout redirect'));
     }
     }
 
 
@@ -48,7 +57,7 @@ class Auth extends Page
      */
      */
     public function login(array $args)
     public function login(array $args)
     {
     {
-        $this->c->Lang->load('login');
+        $this->c->Lang->load('auth');
 
 
         if (! isset($args['_username'])) {
         if (! isset($args['_username'])) {
             $args['_username'] = '';
             $args['_username'] = '';
@@ -62,15 +71,15 @@ class Auth extends Page
             __('Login'),
             __('Login'),
         ];
         ];
         $this->data = [
         $this->data = [
-            'username' => $args['_username'],
             'formAction' => $this->c->Router->link('Login'),
             'formAction' => $this->c->Router->link('Login'),
             'formToken' => $this->c->Csrf->create('Login'),
             'formToken' => $this->c->Csrf->create('Login'),
             'forgetLink' => $this->c->Router->link('Forget'),
             'forgetLink' => $this->c->Router->link('Forget'),
             'regLink' => $this->config['o_regs_allow'] == '1'
             'regLink' => $this->config['o_regs_allow'] == '1'
-                ? $this->c->Router->link('Registration')
+                ? $this->c->Router->link('Register')
                 : null,
                 : null,
-            'formRedirect' => $args['_redirect'],
-            'formSave' => ! empty($args['_save'])
+            'username' => $args['_username'],
+            'redirect' => $args['_redirect'],
+            'save' => ! empty($args['_save'])
         ];
         ];
 
 
         return $this;
         return $this;
@@ -82,93 +91,78 @@ class Auth extends Page
      */
      */
     public function loginPost()
     public function loginPost()
     {
     {
-        $this->c->Lang->load('login');
+        $this->c->Lang->load('auth');
 
 
-        $v = $this->c->Validator;
-        $v->setRules([
+        $v = $this->c->Validator->addValidators([
+            'login_process' => [$this, 'vLoginProcess'],
+        ])->setRules([
             'token'    => 'token:Login',
             'token'    => 'token:Login',
             'redirect' => 'referer:Index',
             'redirect' => 'referer:Index',
-            'username' => ['required|string|min:2|max:25', __('Username')],
-            'password' => ['required|string', __('Password')],
+            'username' => ['required|string', __('Username')],
+            'password' => ['required|string|login_process', __('Password')],
             'save'     => 'checkbox',
             'save'     => 'checkbox',
         ]);
         ]);
 
 
-        $ok = $v->validation($_POST);
-        $data = $v->getData();
-        $this->iswev = $v->getErrors();
-
-        if ($ok && ! $this->loginProcess($data['username'], $data['password'], $data['save'])) {
-            $this->iswev['v'][] = __('Wrong user/pass');
-            $ok = false;
-        }
-
-        if ($ok) {
-            return $this->c->Redirect->setUrl($data['redirect'])->setMessage(__('Login redirect'));
+        if ($v->validation($_POST)) {
+            return $this->c->Redirect->setUrl($v->redirect)->setMessage(__('Login redirect'));
         } else {
         } else {
+            $this->iswev = $v->getErrors();
             return $this->login([
             return $this->login([
-                '_username' => $data['username'],
-                '_redirect' => $data['redirect'],
-                '_save'     => $data['save'],
+                '_username' => $v->username,
+                '_redirect' => $v->redirect,
+                '_save'     => $v->save,
             ]);
             ]);
         }
         }
     }
     }
 
 
     /**
     /**
-     * Вход на форум
-     * @param string $username
+     * Проверка по базе и вход на форум
+     * @param Validator $v
      * @param string $password
      * @param string $password
-     * @param bool $save
-     * @return bool
+     * @param int $type
+     * @return array
      */
      */
-    protected function loginProcess($username, $password, $save)
+    public function vLoginProcess(Validator $v, $password, $type)
     {
     {
-        $user = $this->c->UserMapper->getUser($username, 'username');
-        if (null == $user) {
-            return false;
-        }
-
-        $authorized = false;
-        $hash = $user->password;
-        $update = [];
-
-        // For FluxBB by Visman 1.5.10.74 and above
-        if (strlen($hash) == 40) {
-            if (hash_equals($hash, sha1($password . $this->c->SALT1))) {
-                $hash = password_hash($password, PASSWORD_DEFAULT);
-                $update['password'] = $hash;
-                $authorized = true;
-            }
+        $error = false;
+        if (! empty($v->getErrors())) {
+        } elseif (! ($user = $this->c->UserMapper->getUser($v->username, 'username')) instanceof User) {
+            $error = __('Wrong user/pass');
+        } elseif ($user->isUnverified) {
+            $error = [__('Account is not activated'), 'w'];
         } else {
         } else {
-            $authorized = password_verify($password, $hash);
-        }
-
-        if (! $authorized) {
-            return false;
-        }
-
-        // Update the status if this is the first time the user logged in
-        if ($user->isUnverified) {
-            $update['group_id'] = (int) $this->config['o_default_user_group'];
-        }
-
-        // перезаписываем ip админа и модератора - Visman
-        if ($user->isAdmMod
-            && $this->config['o_check_ip']
-            && $user->registrationIp != $this->c->user->ip
-        ) {
-            $update['registration_ip'] = $this->c->user->ip;
-        }
-
-        // изменения юзера в базе
-        $this->c->UserMapper->updateUser($user->id, $update);
-        // обновления кэша
-        if (isset($update['group_id'])) {
-            $this->c->{'users_info update'};
+            $authorized = false;
+            $hash = $user->password;
+            $update = [];
+            // For FluxBB by Visman 1.5.10.74 and above
+            if (strlen($hash) == 40) {
+                if (hash_equals($hash, sha1($password . $this->c->SALT1))) {
+                    $hash = password_hash($password, PASSWORD_DEFAULT);
+                    $update['password'] = $hash;
+                    $authorized = true;
+                }
+            } else {
+                $authorized = password_verify($password, $hash);
+            }
+            // ошибка в пароле
+            if (! $authorized) {
+                $error = __('Wrong user/pass');
+            } else {
+                // перезаписываем ip админа и модератора - Visman
+                if ($user->isAdmMod
+                    && $this->config['o_check_ip']
+                    && $user->registrationIp != $this->c->user->ip
+                ) {
+                    $update['registration_ip'] = $this->c->user->ip;
+                }
+                // изменения юзера в базе
+                $this->c->UserMapper->updateUser($user->id, $update);
+
+                $this->c->Online->delete($this->c->user);
+                $this->c->UserCookie->setUserCookie($user->id, $hash, $v->save);
+            }
         }
         }
-        $this->c->Online->delete($this->c->user);
-        $this->c->UserCookie->setUserCookie($user->id, $hash, $save);
-
-        return true;
+        return [$password, $type, $error];
     }
     }
 
 
     /**
     /**
@@ -178,8 +172,6 @@ class Auth extends Page
      */
      */
     public function forget(array $args)
     public function forget(array $args)
     {
     {
-        $this->c->Lang->load('login');
-
         $this->nameTpl = 'password_reset';
         $this->nameTpl = 'password_reset';
         $this->onlinePos = 'password_reset';
         $this->onlinePos = 'password_reset';
 
 
@@ -187,13 +179,15 @@ class Auth extends Page
             $args['_email'] = '';
             $args['_email'] = '';
         }
         }
 
 
+        $this->c->Lang->load('auth');
+
         $this->titles = [
         $this->titles = [
             __('Password reset'),
             __('Password reset'),
         ];
         ];
         $this->data = [
         $this->data = [
-            'email' => $args['_email'],
             'formAction' => $this->c->Router->link('Forget'),
             'formAction' => $this->c->Router->link('Forget'),
             'formToken' => $this->c->Csrf->create('Forget'),
             'formToken' => $this->c->Csrf->create('Forget'),
+            'email' => $args['_email'],
         ];
         ];
 
 
         return $this;
         return $this;
@@ -205,52 +199,75 @@ class Auth extends Page
      */
      */
     public function forgetPost()
     public function forgetPost()
     {
     {
-        $this->c->Lang->load('login');
+        $this->c->Lang->load('auth');
 
 
-        $v = $this->c->Validator;
-        $v->setRules([
+        $v = $this->c->Validator->addValidators([
+            'check_email' => [$this, 'vCheckEmail'],
+        ])->setRules([
             'token' => 'token:Forget',
             'token' => 'token:Forget',
-            'email' => 'required|email',
+            'email' => 'required|string:trim,lower|email|check_email',
         ])->setMessages([
         ])->setMessages([
-            'email' => __('Invalid email'),
+            'email.email' => __('Invalid email'),
         ]);
         ]);
 
 
-        $ok = $v->validation($_POST);
-        $data = $v->getData();
-        $this->iswev = $v->getErrors();
-
-        if ($ok && ($user = $this->c->UserMapper->getUser($data['email'], 'email')) === null) {
-            $this->iswev['v'][] = __('Invalid email');
-            $ok = false;
-        }
-        if ($ok && ! empty($user->lastEmailSent) && time() - $user->lastEmailSent < 3600) {
-            $this->iswev['e'][] = __('Email flood', (int) (($user->lastEmailSent + 3600 - time()) / 60));
-            $ok = false;
-        }
-
-        if (! $ok) {
+        if (! $v->validation($_POST)) {
+            $this->iswev = $v->getErrors();
             return $this->forget([
             return $this->forget([
-                '_email' => $data['email'],
+                '_email' => $v->email,
             ]);
             ]);
         }
         }
 
 
-        $mail = $this->c->Mail;
-        $mail->setFolder($this->c->DIR_LANG)
-            ->setLanguage($user->language);
-
-        $key = 'p' . $this->c->Secury->randomPass(75);
-        $hash = $this->c->Secury->hash($data['email'] . $key);
-        $link = $this->c->Router->link('ChangePassword', ['email' => $data['email'], 'key' => $key, 'hash' => $hash]);
-        $tplData = ['link' => $link];
-
-        if ($mail->send($data['email'], 'password_reset.tpl', $tplData)) {
-            $this->c->UserMapper->updateUser($user->id, ['activate_string' => $key, 'last_email_sent' => time()]);
+        $key = 'p' . $this->c->Secury->randomPass(79);
+        $hash = $this->c->Secury->hash($v->email . $key);
+        $link = $this->c->Router->link('ChangePassword', ['email' => $v->email, 'key' => $key, 'hash' => $hash]);
+        $tplData = [
+            'fRootLink' => $this->c->Router->link('Index'),
+            'fMailer' => __('Mailer', $this->config['o_board_title']),
+            'username' => $this->tmpUser->username,
+            'link' => $link,
+        ];
+        $mail = $this->c->Mail->reset()
+            ->setFolder($this->c->DIR_LANG)
+            ->setLanguage($this->tmpUser->language)
+            ->setTo($v->email, $this->tmpUser->username)
+            ->setFrom($this->config['o_webmaster_email'], __('Mailer', $this->config['o_board_title']))
+            ->setTpl('password_reset.tpl', $tplData);
+
+        if ($mail->send()) {
+            $this->c->UserMapper->updateUser($this->tmpUser->id, ['activate_string' => $key, 'last_email_sent' => time()]);
             return $this->c->Message->message(__('Forget mail', $this->config['o_admin_email']), false, 200);
             return $this->c->Message->message(__('Forget mail', $this->config['o_admin_email']), false, 200);
         } else {
         } else {
             return $this->c->Message->message(__('Error mail', $this->config['o_admin_email']), true, 200);
             return $this->c->Message->message(__('Error mail', $this->config['o_admin_email']), true, 200);
         }
         }
     }
     }
 
 
+    /**
+     * Дополнительная проверка email
+     * @param Validator $v
+     * @param string $username
+     * @param int $type
+     * @return array
+     */
+    public function vCheckEmail(Validator $v, $email, $type)
+    {
+        $error = false;
+        // есть ошибки
+        if (! empty($v->getErrors())) {
+        // email забанен
+        } elseif ($this->c->CheckBans->isBanned(null, $email) > 0) {
+            $error = __('Banned email');
+        // нет пользователя с таким email
+        } elseif (! ($user = $this->c->UserMapper->getUser($email, 'email')) instanceof User) {
+            $error = __('Invalid email');
+        // за последний час уже был запрос на этот email
+        } elseif (! empty($user->lastEmailSent) && time() - $user->lastEmailSent < 3600) {
+            $error = [__('Email flood', (int) (($user->lastEmailSent + 3600 - time()) / 60)), 'e'];
+        } else {
+            $this->tmpUser = $user;
+        }
+        return [$email, $type, $error];
+    }
+
     /**
     /**
      * Подготовка данных для формы изменения пароля
      * Подготовка данных для формы изменения пароля
      * @param array $args
      * @param array $args
@@ -267,7 +284,7 @@ class Auth extends Page
             // что-то пошло не так
             // что-то пошло не так
             if (! hash_equals($args['hash'], $this->c->Secury->hash($args['email'] . $args['key']))
             if (! hash_equals($args['hash'], $this->c->Secury->hash($args['email'] . $args['key']))
                 || ! $this->c->Mail->valid($args['email'])
                 || ! $this->c->Mail->valid($args['email'])
-                || ($user = $this->c->UserMapper->getUser($args['email'], 'email')) === null
+                || ! ($user = $this->c->UserMapper->getUser($args['email'], 'email')) instanceof User
                 || empty($user->activateString)
                 || empty($user->activateString)
                 || $user->activateString{0} !== 'p'
                 || $user->activateString{0} !== 'p'
                 || ! hash_equals($user->activateString, $args['key'])
                 || ! hash_equals($user->activateString, $args['key'])
@@ -276,8 +293,7 @@ class Auth extends Page
             }
             }
         }
         }
 
 
-        $this->c->Lang->load('login');
-        $this->c->Lang->load('profile');
+        $this->c->Lang->load('auth');
 
 
         $this->titles = [
         $this->titles = [
             __('Change pass'),
             __('Change pass'),
@@ -300,7 +316,7 @@ class Auth extends Page
         // что-то пошло не так
         // что-то пошло не так
         if (! hash_equals($args['hash'], $this->c->Secury->hash($args['email'] . $args['key']))
         if (! hash_equals($args['hash'], $this->c->Secury->hash($args['email'] . $args['key']))
             || ! $this->c->Mail->valid($args['email'])
             || ! $this->c->Mail->valid($args['email'])
-            || ($user = $this->c->UserMapper->getUser($args['email'], 'email')) === null
+            || ! ($user = $this->c->UserMapper->getUser($args['email'], 'email')) instanceof User
             || empty($user->activateString)
             || empty($user->activateString)
             || $user->activateString{0} !== 'p'
             || $user->activateString{0} !== 'p'
             || ! hash_equals($user->activateString, $args['key'])
             || ! hash_equals($user->activateString, $args['key'])
@@ -308,18 +324,18 @@ class Auth extends Page
             return $this->c->Message->message(__('Bad request'), false);
             return $this->c->Message->message(__('Bad request'), false);
         }
         }
 
 
-        $this->c->Lang->load('login');
-        $this->c->Lang->load('profile');
+        $this->c->Lang->load('auth');
 
 
         $v = $this->c->Validator;
         $v = $this->c->Validator;
         $v->setRules([
         $v->setRules([
             'token'     => 'token:ChangePassword',
             'token'     => 'token:ChangePassword',
-            'password'  => ['required|string|min:8', __('New pass')],
-            'password2' => 'required|same:password',
+            'password'  => ['required|string|min:8|password', __('New pass')],
+            'password2' => ['required|same:password', __('Confirm new pass')],
         ])->setArguments([
         ])->setArguments([
             'token' => $args,
             'token' => $args,
         ])->setMessages([
         ])->setMessages([
-            'password2' => __('Pass not match'),
+            'password.password'  => __('Pass format'),
+            'password2.same' => __('Pass not match'),
         ]);
         ]);
 
 
         if (! $v->validation($_POST)) {
         if (! $v->validation($_POST)) {
@@ -329,7 +345,7 @@ class Auth extends Page
         }
         }
         $data = $v->getData();
         $data = $v->getData();
 
 
-        $this->c->UserMapper->updateUser($user->id, ['password' => password_hash($data['password'], PASSWORD_DEFAULT), 'activate_string' => null]);
+        $this->c->UserMapper->updateUser($user->id, ['password' => password_hash($data['password'], PASSWORD_DEFAULT), 'email_confirmed' => 1, 'activate_string' => null]);
 
 
         $this->iswev['s'][] = __('Pass updated');
         $this->iswev['s'][] = __('Pass updated');
         return $this->login(['_redirect' => $this->c->Router->link('Index')]);
         return $this->login(['_redirect' => $this->c->Router->link('Index')]);

+ 20 - 8
app/Models/Pages/Page.php

@@ -165,6 +165,8 @@ abstract class Page
         return $this->data + [
         return $this->data + [
             'pageTitle' => $this->pageTitle(),
             'pageTitle' => $this->pageTitle(),
             'pageHeads' => $this->pageHeads(),
             'pageHeads' => $this->pageHeads(),
+            'fLang' => __('lang_identifier'),
+            'fDirection' => __('lang_direction'),
             'fTitle' => $this->config['o_board_title'],
             'fTitle' => $this->config['o_board_title'],
             'fDescription' => $this->config['o_board_desc'],
             'fDescription' => $this->config['o_board_desc'],
             'fNavigation' => $this->fNavigation(),
             'fNavigation' => $this->fNavigation(),
@@ -182,14 +184,24 @@ abstract class Page
     protected function getIswev()
     protected function getIswev()
     {
     {
         if ($this->config['o_maintenance'] == '1') {
         if ($this->config['o_maintenance'] == '1') {
-            $user = $this->c->user;
-            if ($user->isAdmMod) {
-                $this->iswev['w'][] = '<a href="' . $this->c->Router->link('AdminOptions', ['#' => 'maintenance']). '">' . __('Maintenance mode enabled') . '</a>';
+            if ($this->c->user->isAdmin) {
+                $this->iswev['w'][] = __('Maintenance mode enabled', $this->c->Router->link('AdminOptions', ['#' => 'maintenance']));
             }
             }
         }
         }
         return $this->iswev;
         return $this->iswev;
     }
     }
 
 
+    /**
+     * Установка info, success, warning, error, validation информации из вне
+     * @param array $iswev
+     * @return Page
+     */
+    public function setIswev(array $iswev)
+    {
+        $this->iswev = $iswev;
+        return $this;
+    }
+
     /**
     /**
      * Формирует title страницы
      * Формирует title страницы
      * @return string
      * @return string
@@ -207,7 +219,7 @@ abstract class Page
      */
      */
     protected function pageHeads()
     protected function pageHeads()
     {
     {
-        return [];
+        return []; //????
     }
     }
 
 
     /**
     /**
@@ -245,7 +257,7 @@ abstract class Page
         }
         }
 
 
         if ($user->isGuest) {
         if ($user->isGuest) {
-            $nav['register'] = ['register.php', __('Register')];
+            $nav['register'] = [$r->link('Register'), __('Register')];
             $nav['login'] = [$r->link('Login'), __('Login')];
             $nav['login'] = [$r->link('Login'), __('Login')];
         } else {
         } else {
             $nav['profile'] = [$r->link('User', [
             $nav['profile'] = [$r->link('User', [
@@ -294,11 +306,11 @@ abstract class Page
      * Заглушка
      * Заглушка
      * @param string $name
      * @param string $name
      * @param array $arguments
      * @param array $arguments
-     * @return Page
+     * @throws \RuntimeException
      */
      */
     public function __call($name, array $arguments)
     public function __call($name, array $arguments)
     {
     {
-        return $this;
+        throw new RuntimeException("'{$name}' method is not");
     }
     }
 
 
     /**
     /**
@@ -308,7 +320,7 @@ abstract class Page
      */
      */
     protected function size($size)
     protected function size($size)
     {
     {
-        $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB');
+        $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
 
 
         for ($i = 0; $size > 1024; $i++) {
         for ($i = 0; $size > 1024; $i++) {
             $size /= 1024;
             $size /= 1024;

+ 251 - 0
app/Models/Pages/Register.php

@@ -0,0 +1,251 @@
+<?php
+
+namespace ForkBB\Models\Pages;
+
+use ForkBB\Models\Validator;
+use ForkBB\Models\User;
+
+class Register extends Page
+{
+    /**
+     * Имя шаблона
+     * @var string
+     */
+    protected $nameTpl = 'register';
+
+    /**
+     * Позиция для таблицы онлайн текущего пользователя
+     * @var null|string
+     */
+    protected $onlinePos = 'register';
+
+    /**
+     * Указатель на активный пункт навигации
+     * @var string
+     */
+    protected $index = 'register';
+
+    /**
+     * Обработчик регистрации
+     * @retrun Page
+     */
+    public function reg()
+    {
+        $this->c->Lang->load('register');
+
+        $v = $this->c->Validator->addValidators([
+            'check_email'    => [$this, 'vCheckEmail'],
+            'check_username' => [$this, 'vCheckUsername'],
+        ])->setRules([
+            'token'    => 'token:RegisterForm',
+            'agree'    => 'required|token:Register',
+            'on'       => 'integer',
+            'email'    => ['required_with:on|string:trim,lower|email|check_email', __('Email')],
+            'username' => ['required_with:on|string:trim|min:2|max:25|login|check_username', __('Username')],
+            'password' => ['required_with:on|string|min:8|password', __('Password')],
+        ])->setMessages([
+            'agree.required'    => ['cancel', 'cancel'],
+            'agree.token'       => [__('Bad agree', $this->c->Router->link('Register')), 'w'],
+            'password.password' => __('Pass format'),
+            'username.login'    => __('Login format'),
+        ]);
+
+        // завершение регистрации
+        if ($v->validation($_POST) && $v->on === 1) {
+            return $this->regEnd($v);
+        }
+
+        $this->iswev = $v->getErrors();
+
+        // нет согласия с правилами
+        if (isset($this->iswev['cancel'])) {
+            return $this->c->Redirect->setPage('Index')->setMessage(__('Reg cancel redirect'));
+        }
+
+        $this->titles = [
+            __('Register'),
+        ];
+        $this->data = [
+            'formAction' => $this->c->Router->link('RegisterForm'),
+            'formToken' => $this->c->Csrf->create('RegisterForm'),
+            'agree' => $v->agree,
+            'on' => '1',
+            'email' => $v->email,
+            'username' => $v->username,
+        ];
+
+        return $this;
+    }
+
+    /**
+     * Дополнительная проверка email
+     * @param Validator $v
+     * @param string $username
+     * @param int $type
+     * @return array
+     */
+    public function vCheckEmail(Validator $v, $email, $type)
+    {
+        $error = false;
+        // email забанен
+        if ($this->c->CheckBans->isBanned(null, $email) > 0) {
+            $error = __('Banned email');
+        // найден хотя бы 1 юзер с таким же email
+        } elseif (empty($v->getErrors()) && $this->c->UserMapper->getUser($email, 'email') !== 0) {
+            $error = __('Dupe email');
+        }
+        return [$email, $type, $error];
+    }
+
+    /**
+     * Дополнительная проверка username
+     * @param Validator $v
+     * @param string $username
+     * @param int $type
+     * @return array
+     */
+    public function vCheckUsername(Validator $v, $username, $type)
+    {
+        $username = preg_replace('%\s+%su', ' ', $username);
+        $error = false;
+        // username = Гость
+        if (preg_match('%^(guest|' . preg_quote(__('Guest'), '%') . ')$%iu', $username)) {
+            $error = __('Username guest');
+        // цензура
+        } elseif ($this->config['o_censoring'] == '1' && censor_words($username) !== $username) {
+            $error = __('Username censor');
+        // username забанен
+        } elseif ($this->c->CheckBans->isBanned($username) > 0) {
+            $error = __('Banned username');
+        // есть пользователь с похожим именем
+        } elseif (empty($v->getErrors()) && ! $this->c->UserMapper->isUnique($username)) {
+            $error = __('Username not unique');
+        }
+        return [$username, $type, $error];
+    }
+
+    /**
+     * Завершение регистрации
+     * @param array @data
+     * @return Page
+     */
+    protected function regEnd(Validator $v)
+    {
+        if ($this->config['o_regs_verify'] == '1') {
+            $groupId = PUN_UNVERIFIED;
+            $key = 'w' . $this->c->Secury->randomPass(79);
+            $visit = 0;
+        } else {
+            $groupId = $this->config['o_default_user_group'];
+            $key = null;
+            $visit = time(); //????
+        }
+
+        $newUserId = $this->c->UserMapper->newUser(new User([
+            'group_id' => $groupId,
+            'username' => $v->username,
+            'password' => password_hash($v->password, PASSWORD_DEFAULT),
+            'email' => $v->email,
+            'email_confirmed' => 0,
+            'last_visit' => $visit,
+            'activate_string' => $key,
+        ], $this->c));
+
+        // обновление статистики по пользователям
+        if ($this->config['o_regs_verify'] != '1') {
+            $this->c->{'users_info update'};
+        }
+
+        // уведомление о регистрации
+        if ($this->config['o_mailing_list'] != '' && $this->config['o_regs_report'] == '1') {
+            $tplData = [
+                'fTitle' => $this->config['o_board_title'],
+                'fRootLink' => $this->c->Router->link('Index'),
+                'fMailer' => __('Mailer', $this->config['o_board_title']),
+                'username' => $v->username,
+                'userLink' => $this->c->Router->link('User', ['id' => $newUserId, 'name' => $v->username]),
+            ];
+            $mail = $this->c->Mail->reset()
+                ->setFolder($this->c->DIR_LANG)
+                ->setLanguage($this->config['o_default_lang'])
+                ->setTo($this->config['o_mailing_list'])
+                ->setFrom($this->config['o_webmaster_email'], __('Mailer', $this->config['o_board_title']))
+                ->setTpl('new_user.tpl', $tplData)
+                ->send();
+        }
+
+        $this->c->Lang->load('register');
+
+        // отправка письма активации аккаунта
+        if ($this->config['o_regs_verify'] == '1') {
+            $hash = $this->c->Secury->hash($newUserId . $key);
+            $link = $this->c->Router->link('RegActivate', ['id' => $newUserId, 'key' => $key, 'hash' => $hash]);
+            $tplData = [
+                'fTitle' => $this->config['o_board_title'],
+                'fRootLink' => $this->c->Router->link('Index'),
+                'fMailer' => __('Mailer', $this->config['o_board_title']),
+                'username' => $v->username,
+                'link' => $link,
+            ];
+            $mail = $this->c->Mail->reset()
+                ->setFolder($this->c->DIR_LANG)
+                ->setLanguage($this->c->user->language)
+                ->setTo($v->email)
+                ->setFrom($this->config['o_webmaster_email'], __('Mailer', $this->config['o_board_title']))
+                ->setTpl('welcome.tpl', $tplData);
+
+            // письмо активации аккаунта отправлено
+            if ($mail->send()) {
+                return $this->c->Message->message(__('Reg email', $this->config['o_admin_email']), false, 200);
+            // форма сброса пароля
+            } else {
+                return $this->c->Auth->setIswev([
+                    'w' => [
+                        __('Error welcom mail', $this->config['o_admin_email']),
+                    ],
+                ])->forget([
+                    '_email' => $v->email,
+                ]);
+            }
+        // форма логина
+        } else {
+            return $this->c->Auth->setIswev([
+                's' => [
+                    __('Reg complete'),
+                ],
+            ])->login([
+                '_username' => $v->username,
+            ]);
+        }
+    }
+
+    /**
+     * Активация аккаунта
+     * @param array $args
+     * @return Page
+     */
+    public function activate(array $args)
+    {
+        if (! hash_equals($args['hash'], $this->c->Secury->hash($args['id'] . $args['key']))
+            || ! ($user = $this->c->UserMapper->getUser($args['id'])) instanceof User
+            || empty($user->activateString)
+            || $user->activateString{0} !== 'w'
+            || ! hash_equals($user->activateString, $args['key'])
+        ) {
+            return $this->c->Message->message(__('Bad request'), false);
+        }
+
+        $this->c->UserMapper->updateUser($user->id, ['group_id' => $this->config['o_default_user_group'], 'email_confirmed' => 1, 'activate_string' => null]);
+        $this->c->{'users_info update'};
+
+        $this->c->Lang->load('register');
+
+        return $this->c->Auth->setIswev([
+            's' => [
+                __('Reg complete'),
+            ],
+        ])->login([
+            '_username' => $user->username,
+        ]);
+    }
+}

+ 27 - 1
app/Models/Pages/Rules.php

@@ -32,7 +32,33 @@ class Rules extends Page
             __('Forum rules'),
             __('Forum rules'),
         ];
         ];
         $this->data = [
         $this->data = [
-            'Rules' => $this->config['o_rules_message'],
+            'title' => __('Forum rules'),
+            'rules' => $this->config['o_rules_message'],
+            'formAction' => null,
+        ];
+        return $this;
+    }
+
+    /**
+     * Подготавливает данные для шаблона
+     * @return Page
+     */
+    public function confirmation()
+    {
+        $this->index = 'register';
+        $this->c->Lang->load('register');
+
+        $this->titles = [
+            __('Forum rules'),
+        ];
+        $this->data = [
+            'title' => __('Forum rules'),
+            'rules' => $this->config['o_rules'] == '1' ?
+                $this->config['o_rules_message']
+                : __('If no rules'),
+            'formAction' => $this->c->Router->link('RegisterForm'),
+            'formToken' => $this->c->Csrf->create('RegisterForm'),
+            'formHash' => $this->c->Csrf->create('Register'),
         ];
         ];
         return $this;
         return $this;
     }
     }

+ 0 - 12
app/Models/User.php

@@ -19,16 +19,6 @@ class User extends AbstractModel
      */
      */
     protected $config;
     protected $config;
 
 
-    /**
-     * @var UserCookie
-     */
-    protected $userCookie;
-
-    /**
-     * @var DB
-     */
-    protected $db;
-
     /**
     /**
      * Время
      * Время
      * @var int
      * @var int
@@ -43,8 +33,6 @@ class User extends AbstractModel
         $this->now = time();
         $this->now = time();
         $this->c = $container;
         $this->c = $container;
         $this->config = $container->config;
         $this->config = $container->config;
-        $this->userCookie = $container->UserCookie;
-        $this->db = $container->DB;
         parent::__construct($data);
         parent::__construct($data);
     }
     }
 
 

+ 29 - 5
app/Models/UserMapper.php

@@ -90,7 +90,8 @@ class UserMapper
      * Получение пользователя по условию
      * Получение пользователя по условию
      * @param int|string
      * @param int|string
      * @param string $field
      * @param string $field
-     * @return null|User
+     * @return int|User
+     * @throws \InvalidArgumentException
      */
      */
     public function getUser($value, $field = 'id')
     public function getUser($value, $field = 'id')
     {
     {
@@ -105,13 +106,13 @@ class UserMapper
                 $where = 'u.email=\'' . $this->db->escape($value) . '\'';
                 $where = 'u.email=\'' . $this->db->escape($value) . '\'';
                 break;
                 break;
             default:
             default:
-                return null;
+                throw new InvalidArgumentException('Field not supported');
         }
         }
         $result = $this->db->query('SELECT u.*, g.* FROM '.$this->db->prefix.'users AS u LEFT JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id WHERE '.$where) or error('Unable to fetch user information', __FILE__, __LINE__, $this->db->error());
         $result = $this->db->query('SELECT u.*, g.* FROM '.$this->db->prefix.'users AS u LEFT JOIN '.$this->db->prefix.'groups AS g ON u.group_id=g.g_id WHERE '.$where) or error('Unable to fetch user information', __FILE__, __LINE__, $this->db->error());
 
 
         // найдено несколько пользователей
         // найдено несколько пользователей
         if ($this->db->num_rows($result) !== 1) {
         if ($this->db->num_rows($result) !== 1) {
-            return null;
+            return $this->db->num_rows($result);
         }
         }
 
 
         $user = $this->db->fetch_assoc($result);
         $user = $this->db->fetch_assoc($result);
@@ -119,14 +120,25 @@ class UserMapper
 
 
         // найден гость
         // найден гость
         if ($user['id'] == 1) {
         if ($user['id'] == 1) {
-            return null;
+            return 1;
         }
         }
 
 
         return new User($user, $this->c);
         return new User($user, $this->c);
     }
     }
 
 
     /**
     /**
-     * Обновить данные юзера
+     * Проверка на уникальность имени пользователя
+     * @param string $username
+     * @return bool
+     */
+    public function isUnique($username)
+    {
+        $result = $this->db->query('SELECT username FROM '.$this->db->prefix.'users WHERE (UPPER(username)=UPPER(\''.$this->db->escape($username).'\') OR UPPER(username)=UPPER(\''.$this->db->escape(preg_replace('%[^\p{L}\p{N}]%u', '', $username)).'\'))') or error('Unable to fetch user info', __FILE__, __LINE__, $this->db->error());
+        return ! $this->db->num_rows($result);
+    }
+
+    /**
+     * Обновить данные пользователя
      * @param int $id
      * @param int $id
      * @param array $update
      * @param array $update
      */
      */
@@ -152,4 +164,16 @@ class UserMapper
         $this->db->query('UPDATE '.$this->db->prefix.'users SET '.implode(', ', $set).' WHERE id='.$id) or error('Unable to update user data', __FILE__, __LINE__, $this->db->error());
         $this->db->query('UPDATE '.$this->db->prefix.'users SET '.implode(', ', $set).' WHERE id='.$id) or error('Unable to update user data', __FILE__, __LINE__, $this->db->error());
     }
     }
 
 
+    /**
+     * Создание нового пользователя
+     * @param User $user
+     * @throws
+     * @return int
+     */
+    public function newUser(User $user)
+    {
+        $this->db->query('INSERT INTO '.$this->db->prefix.'users (username, group_id, password, email, email_confirmed, email_setting, timezone, dst, language, style, registered, registration_ip, last_visit, activate_string) VALUES(\''.$this->db->escape($user->username).'\', '.$user->groupId.', \''.$this->db->escape($user->password).'\', \''.$this->db->escape($user->email).'\', '.$user->emailConfirmed.', '.$this->config['o_default_email_setting'].', '.$this->config['o_default_timezone'].' , '.$this->config['o_default_dst'].', \''.$this->db->escape($user->language).'\', \''.$user->style.'\', '.time().', \''.$this->db->escape($this->getIpAddress()).'\', '.$user->lastVisit.', \''.$this->db->escape($user->activateString).'\')') or error('Unable to create user', __FILE__, __LINE__, $this->db->error());
+        $new_uid = $this->db->insert_id(); //????
+        return $new_uid;
+    }
 }
 }

+ 243 - 133
app/Models/Validator.php

@@ -12,6 +12,7 @@ class Validator
     const T_NUMERIC = 2;
     const T_NUMERIC = 2;
     const T_INT = 3;
     const T_INT = 3;
     const T_ARRAY = 4;
     const T_ARRAY = 4;
+    const T_BOOLEAN = 5;
 
 
     /**
     /**
      * Контейнер
      * Контейнер
@@ -19,6 +20,11 @@ class Validator
      */
      */
     protected $c;
     protected $c;
 
 
+    /**
+     * @var array
+     */
+    protected $validators;
+
     /**
     /**
      * @var array
      * @var array
      */
      */
@@ -27,7 +33,7 @@ class Validator
     /**
     /**
      * @var array
      * @var array
      */
      */
-    protected $data;
+    protected $result;
 
 
     /**
     /**
      * @var array
      * @var array
@@ -49,11 +55,9 @@ class Validator
      */
      */
     protected $errors;
     protected $errors;
 
 
-    /**
-     * Тип текущего поля при валидации
-     * @var int
-     */
-    protected $curType;
+    protected $fields;
+    protected $status;
+    protected $raw;
 
 
     /**
     /**
      * Конструктор
      * Конструктор
@@ -62,6 +66,35 @@ class Validator
     public function __construct(Container $container)
     public function __construct(Container $container)
     {
     {
         $this->c = $container;
         $this->c = $container;
+        $this->validators = [
+            'array'         => [$this, 'vArray'],
+            'checkbox'      => [$this, 'vCheckbox'],
+            'email'         => [$this, 'vEmail'],
+            'integer'       => [$this, 'vInteger'],
+            'login'         => [$this, 'vLogin'],
+            'max'           => [$this, 'vMax'],
+            'min'           => [$this, 'vMin'],
+            'numeric'       => [$this, 'vNumeric'],
+            'password'      => [$this, 'vPassword'],
+            'referer'       => [$this, 'vReferer'],
+            'regex'         => [$this, 'vRegex'],
+            'required'      => [$this, 'vRequired'],
+            'required_with' => [$this, 'vRequiredWith'],
+            'same'          => [$this, 'vSame'],
+            'string'        => [$this, 'vString'],
+            'token'         => [$this, 'vToken'],
+        ];
+    }
+
+    /**
+     * Добавление новых валидаторов
+     * @param array $validators
+     * @param Validator
+     */
+    public function addValidators(array $validators)
+    {
+        $this->validators = array_replace($this->validators, $validators);
+        return $this;
     }
     }
 
 
     /**
     /**
@@ -73,27 +106,27 @@ class Validator
     public function setRules(array $list)
     public function setRules(array $list)
     {
     {
         $this->rules = [];
         $this->rules = [];
-        $this->data = [];
+        $this->result = [];
         $this->alias = [];
         $this->alias = [];
         $this->errors = [];
         $this->errors = [];
         $this->arguments = [];
         $this->arguments = [];
+        $this->fields = [];
         foreach ($list as $field => $raw) {
         foreach ($list as $field => $raw) {
             $rules = [];
             $rules = [];
             // псевдоним содержится в списке правил
             // псевдоним содержится в списке правил
             if (is_array($raw)) {
             if (is_array($raw)) {
-                $this->aliases[$field] = $raw[1];
-                $raw = $raw[0];
+                list($raw, $this->aliases[$field]) = $raw;
             }
             }
             // перебор правил для текущего поля
             // перебор правил для текущего поля
-            $rawRules = explode('|', $raw);
-            foreach ($rawRules as $rule) {
+            foreach (explode('|', $raw) as $rule) {
                  $tmp = explode(':', $rule, 2);
                  $tmp = explode(':', $rule, 2);
-                 if (! method_exists($this, $tmp[0] . 'Rule')) {
-                     throw new RuntimeException('Rule not found');
+                 if (empty($this->validators[$tmp[0]])) {
+                     throw new RuntimeException($tmp[0] . ' validator not found');
                  }
                  }
                  $rules[$tmp[0]] = isset($tmp[1]) ? $tmp[1] : '';
                  $rules[$tmp[0]] = isset($tmp[1]) ? $tmp[1] : '';
             }
             }
             $this->rules[$field] = $rules;
             $this->rules[$field] = $rules;
+            $this->fields[$field] = $field;
         }
         }
         return $this;
         return $this;
     }
     }
@@ -127,7 +160,7 @@ class Validator
      */
      */
     public function setAliases(array $aliases)
     public function setAliases(array $aliases)
     {
     {
-        $this->aliases = $aliases;
+        $this->aliases = array_replace($this->aliases, $aliases);
         return $this;
         return $this;
     }
     }
 
 
@@ -143,38 +176,62 @@ class Validator
         if (empty($this->rules)) {
         if (empty($this->rules)) {
             throw new RuntimeException('Rules not found');
             throw new RuntimeException('Rules not found');
         }
         }
-        $ok = true;
         $this->errors = [];
         $this->errors = [];
-        // перебор всех полей
-        foreach ($this->rules as $field => $rules) {
-            $error = false;
-            $this->curType = self::T_UNKNOWN;
-            // обязательное поле отсутствует
-            if (! isset($raw[$field]) && isset($rules['required'])) {
-                $rule = 'required';
-                $attr = $rules['required'];
-                $args = $this->getArguments($field, $rule);
-                list($value, $error) = $this->requiredRule('', $attr, $args);
-            } else {
-                $value = isset($raw[$field])
-                    ? $this->c->Secury->replInvalidChars($raw[$field])
-                    : null;
-                // перебор правил для текущего поля
-                foreach ($rules as $rule => $attr) {
-                    $args = $this->getArguments($field, $rule);
-                    $method = $rule . 'Rule';
-                    list($value, $error) = $this->$method($value, $attr, $args);
-                    // ошибок нет
-                    if (false === $error) {
-                        continue;
-                    }
-                    break;
-                }
+        $this->status = [];
+        $this->raw = $raw;
+        foreach ($this->fields as $field) {
+            $this->$field;
+        }
+        $this->raw = null;
+        return empty($this->errors);
+    }
+
+    /**
+     * Проверяет поле согласно заданным правилам
+     * Возвращает значение запрашиваемого поля
+     * @param string
+     * @return mixed
+     * @throws \RuntimeException
+     */
+    public function __get($field)
+    {
+        if (isset($this->status[$field])) {
+            return $this->result[$field];
+        } elseif (empty($this->rules[$field])) {
+            throw new RuntimeException("No rules for '{$field}' field");
+        }
+
+        $value = null;
+        if (! isset($this->raw[$field]) && isset($this->rules[$field]['required'])) {
+            $rules = ['required' => ''];
+        } else {
+            $rules = $this->rules[$field];
+            if (isset($this->raw[$field])) {
+                $value = $this->c->Secury->replInvalidChars($this->raw[$field]);
             }
             }
-            $ok = $this->error($error, $field, $rule, $attr, $ok);
-            $this->data[$field] = $value;
         }
         }
-        return $ok;
+
+        $error = false;
+        $type = self::T_UNKNOWN;
+        foreach ($rules as $validator => $attr) {
+            $args = $this->getArguments($field, $validator);
+            list($value, $type, $error) = $this->validators[$validator]($this, $value, $type, $attr, $args);
+            // ошибок нет
+            if (false === $error) {
+                continue;
+            }
+            break;
+        }
+
+        if (! is_bool($error)) {
+            $this->error($error, $field, $validator, $attr);
+            $this->status[$field] = false;
+        } else {
+            $this->status[$field] = true;
+        }
+
+        $this->result[$field] = $value;
+        return $value;
     }
     }
 
 
     /**
     /**
@@ -185,8 +242,8 @@ class Validator
      */
      */
     protected function getArguments($field, $rule)
     protected function getArguments($field, $rule)
     {
     {
-        if (isset($this->arguments[$field . '.'. $rule])) {
-            return $this->arguments[$field . '.'. $rule];
+        if (isset($this->arguments[$field . '.' . $rule])) {
+            return $this->arguments[$field . '.' . $rule];
         } elseif (isset($this->arguments[$field])) {
         } elseif (isset($this->arguments[$field])) {
             return $this->arguments[$field];
             return $this->arguments[$field];
         } else {
         } else {
@@ -200,14 +257,9 @@ class Validator
      * @param string $field
      * @param string $field
      * @param string $rule
      * @param string $rule
      * @param string $attr
      * @param string $attr
-     * @param bool $ok
-     * return bool
      */
      */
-    protected function error($error, $field, $rule, $attr, $ok)
+    protected function error($error, $field, $rule, $attr)
     {
     {
-        if (is_bool($error)) {
-            return $ok;
-        }
         // псевдоним имени поля
         // псевдоним имени поля
         $alias = isset($this->aliases[$field]) ? $this->aliases[$field] : $field;
         $alias = isset($this->aliases[$field]) ? $this->aliases[$field] : $field;
         // текст ошибки
         // текст ошибки
@@ -223,7 +275,19 @@ class Validator
             $error = $error[0];
             $error = $error[0];
         }
         }
         $this->errors[$type][] = __($error, [':alias' => $alias, ':attr' => $attr]);
         $this->errors[$type][] = __($error, [':alias' => $alias, ':attr' => $attr]);
-        return false;
+    }
+
+    /**
+     * Возвращает статус проверки поля
+     * @param string $field
+     * @return bool
+     */
+    public function getStatus($field)
+    {
+        if (! isset($this->status[$field])) {
+            $this->$field;
+        }
+        return $this->status[$field];
     }
     }
 
 
     /**
     /**
@@ -234,10 +298,10 @@ class Validator
      */
      */
     public function getData()
     public function getData()
     {
     {
-        if (empty($this->data)) {
+        if (empty($this->result)) {
             throw new RuntimeException('Data not found');
             throw new RuntimeException('Data not found');
         }
         }
-        return $this->data;
+        return $this->result;
     }
     }
 
 
     /**
     /**
@@ -249,175 +313,221 @@ class Validator
         return $this->errors;
         return $this->errors;
     }
     }
 
 
-    /**
-     * Правило "required"
-     * @param mixed $value
-     * @param string $attrs
-     * @param mixed $args
-     * @return array
-     */
-    protected function requiredRule($value, $attr, $args)
+    protected function vRequired($v, $value, $type)
     {
     {
-        $f = function () use ($value) {
-            if (is_string($value)) {
-                $this->curType = self::T_STRING;
-                return isset($value{0});
-            } elseif (is_array($value)) {
-                $this->curType = self::T_ARRAY;
-                return ! empty($value);
-            } else {
-                return null !== $value;
+        if (is_string($value)) {
+            if (strlen(trim($value)) > 0) {
+                return [$value, $v::T_STRING, false];
             }
             }
-        };
-        if ($f()) {
-            if (is_numeric($value)) {
-                if (is_int(0 + $value)) {
-                    $this->curType = self::T_INT;
-                } else {
-                    $this->curType = self::T_NUMERIC;
-                }
+        } elseif (is_array($value)) {
+            if (! empty($value)) {
+                return [$value, $v::T_ARRAY, false];
+            }
+        } elseif (null !== $value) {
+            if (is_int($value)) {
+                $type = $v::T_INT;
+            } elseif (is_numeric($value)) {
+                $type = $v::T_NUMERIC;
+            }
+            return [$value, $type, false];
+        }
+        return [null, $type, 'The :alias is required'];
+    }
+
+    protected function vRequiredWith($v, $value, $type, $attr)
+    {
+        foreach (explode(',', $attr) as $field) {
+            if (null !== $v->$field) {
+                return $this->vRequired($v, $value, $type);
             }
             }
-            return [$value, false];
+        }
+        list(, , $error) = $this->vRequired($v, $value, $type);
+        if (false === $error) {
+            return [null, $type, 'The :alias is not required'];
         } else {
         } else {
-            return [$attr, 'The :alias is required'];
+            return [$value, $type, false];
         }
         }
     }
     }
 
 
-    protected function stringRule($value, $attr)
+    protected function vString($v, $value, $type, $attr)
     {
     {
-        if (is_string($value)) {
-            $this->curType = self::T_STRING;
-            return [$value, false];
+        if (null === $value) {
+            return [null, $type, false];
+        } elseif (is_string($value)) {
+            foreach(explode(',', $attr) as $action) {
+                switch ($action) {
+                    case 'trim':
+                        $value = trim($value);
+                        break;
+                    case 'lower':
+                        $value = mb_strtolower($value, 'UTF-8');
+                        break;
+                }
+            }
+            return [$value, $v::T_STRING, false];
         } else {
         } else {
-            return [$attr, 'The :alias must be string'];
+            return [null, $type, 'The :alias must be string'];
         }
         }
     }
     }
 
 
-    protected function numericRule($value, $attr)
+    protected function vNumeric($v, $value, $type)
     {
     {
-        if (is_numeric($value)) {
-            $this->curType = self::T_NUMERIC;
-            return [0 + $value, false];
+        if (null === $value) {
+            return [null, $type, false];
+        } elseif (is_numeric($value)) {
+            return [0 + $value, $v::T_NUMERIC, false];
         } else {
         } else {
-            return [$attr, 'The :alias must be numeric'];
+            return [null, $type, 'The :alias must be numeric'];
         }
         }
     }
     }
 
 
-    protected function intRule($value, $attr)
+    protected function vInteger($v, $value, $type)
     {
     {
-        if (is_numeric($value) && is_int(0 + $value)) {
-            $this->curType = self::T_INT;
-            return [(int) $value, false];
+        if (null === $value) {
+            return [null, $type, false];
+        } elseif (is_numeric($value) && is_int(0 + $value)) {
+            return [(int) $value, $v::T_INT, false];
         } else {
         } else {
-            return [$attr, 'The :alias must be integer'];
+            return [null, $type, 'The :alias must be integer'];
         }
         }
     }
     }
 
 
-    protected function arrayRule($value, $attr)
+    protected function vArray($v, $value, $type)
     {
     {
-        if (is_array($value)) {
-            $this->curType = self::T_ARRAY;
-            return [$value, false];
+        if (null === $value) {
+            return [null, $type, false];
+        } elseif (is_array($value)) {
+            return [$value, $v::T_ARRAY, false];
         } else {
         } else {
-            return [$attr, 'The :alias must be array'];
+            return [null, $type, 'The :alias must be array'];
         }
         }
     }
     }
 
 
-    protected function minRule($value, $attr)
+    protected function vMin($v, $value, $type, $attr)
     {
     {
-        switch ($this->curType) {
+        if (null === $value) {
+            return [null, $type, false];
+        }
+        switch ($type) {
             case self::T_STRING:
             case self::T_STRING:
-                if (mb_strlen($value) < $attr) {
-                    return [$value, 'The :alias minimum is :attr characters'];
+                if (mb_strlen($value, 'UTF-8') < $attr) {
+                    return [$value, $type, 'The :alias minimum is :attr characters'];
                 }
                 }
                 break;
                 break;
             case self::T_NUMERIC:
             case self::T_NUMERIC:
             case self::T_INT:
             case self::T_INT:
                 if ($value < $attr) {
                 if ($value < $attr) {
-                    return [$value, 'The :alias minimum is :attr'];
+                    return [$value, $type, 'The :alias minimum is :attr'];
                 }
                 }
                 break;
                 break;
             case self::T_ARRAY:
             case self::T_ARRAY:
                 if (count($value) < $attr) {
                 if (count($value) < $attr) {
-                    return [$value, 'The :alias minimum is :attr elements'];
+                    return [$value, $type, 'The :alias minimum is :attr elements'];
                 }
                 }
                 break;
                 break;
             default:
             default:
-                return ['', 'The :alias minimum is :attr'];
+                return [null, $type, 'The :alias minimum is :attr'];
                 break;
                 break;
         }
         }
-        return [$value, false];
+        return [$value, $type, false];
     }
     }
 
 
-    protected function maxRule($value, $attr)
+    protected function vMax($v, $value, $type, $attr)
     {
     {
-        switch ($this->curType) {
+        if (null === $value) {
+            return [null, $type, false];
+        }
+        switch ($type) {
             case self::T_STRING:
             case self::T_STRING:
-                if (mb_strlen($value) > $attr) {
-                    return [$value, 'The :alias maximum is :attr characters'];
+                if (mb_strlen($value, 'UTF-8') > $attr) {
+                    return [$value, $type, 'The :alias maximum is :attr characters'];
                 }
                 }
                 break;
                 break;
             case self::T_NUMERIC:
             case self::T_NUMERIC:
             case self::T_INT:
             case self::T_INT:
                 if ($value > $attr) {
                 if ($value > $attr) {
-                    return [$value, 'The :alias maximum is :attr'];
+                    return [$value, $type, 'The :alias maximum is :attr'];
                 }
                 }
                 break;
                 break;
             case self::T_ARRAY:
             case self::T_ARRAY:
                 if (count($value) > $attr) {
                 if (count($value) > $attr) {
-                    return [$value, 'The :alias maximum is :attr elements'];
+                    return [$value, $type, 'The :alias maximum is :attr elements'];
                 }
                 }
                 break;
                 break;
             default:
             default:
-                return ['', 'The :alias maximum is :attr'];
+                return [null, $type, 'The :alias maximum is :attr'];
                 break;
                 break;
         }
         }
-        return [$value, false];
+        return [$value, $type, false];
     }
     }
 
 
-    protected function tokenRule($value, $attr, $args)
+    protected function vToken($v, $value, $type, $attr, $args)
     {
     {
         if (! is_array($args)) {
         if (! is_array($args)) {
             $args = [];
             $args = [];
         }
         }
-        if (is_string($value) && $this->c->Csrf->verify($value, $attr, $args)) {
-            return [$value, false];
+        $value = (string) $value;
+        if ($this->c->Csrf->verify($value, $attr, $args)) {
+            return [$value, $type, false];
         } else {
         } else {
-            return ['', ['Bad token', 'e']];
+            return [$value, $type, ['Bad token', 'e']];
         }
         }
     }
     }
 
 
-    protected function checkboxRule($value, $attr)
+    protected function vCheckbox($v, $value)
     {
     {
-        return [! empty($value), false]; //????
+        return [! empty($value), $v::T_BOOLEAN, false];
     }
     }
 
 
-    protected function refererRule($value, $attr, $args)
+    protected function vReferer($v, $value, $type, $attr, $args)
     {
     {
         if (! is_array($args)) {
         if (! is_array($args)) {
             $args = [];
             $args = [];
         }
         }
-        return [$this->c->Router->validate($value, $attr), false];
+        return [$this->c->Router->validate($value, $attr, $args), $type, false];
     }
     }
 
 
-    protected function emailRule($value, $attr)
+    protected function vEmail($v, $value, $type)
     {
     {
-        if ($this->c->Mail->valid($value)) {
-            return [$value, false];
+        if (null === $value) {
+            return [$value, $type, false];
+        } elseif ($this->c->Mail->valid($value)) {
+            return [$value, $type, false];
         } else {
         } else {
             if (! is_string($value)) {
             if (! is_string($value)) {
                 $value = (string) $value;
                 $value = (string) $value;
             }
             }
-            return [$value, 'The :alias is not valid email'];
+            return [$value, $type, 'The :alias is not valid email'];
         }
         }
     }
     }
 
 
-    protected function sameRule($value, $attr)
+    protected function vSame($v, $value, $type, $attr)
     {
     {
-        if (isset($this->data[$attr]) && $value === $this->data[$attr]) {
-            return [$value, false];
+        if (! $v->getStatus($attr) || $value === $v->$attr) {
+            return [$value, $type, false];
         } else {
         } else {
-            return [$value, 'The :alias must be same with original'];
+            return [null, $type, 'The :alias must be same with original'];
         }
         }
     }
     }
+
+    protected function vRegex($v, $value, $type, $attr)
+    {
+        if (null === $value) {
+            return [$value, $type, false];
+        } elseif ($type === $v::T_STRING && preg_match($attr, $value)) {
+            return [$value, $type, false];
+        } else {
+            return [null, $type, 'The :alias is not valid format'];
+        }
+    }
+
+    protected function vPassword($v, $value, $type)
+    {
+        return $this->vRegex($v, $value, $type, '%^(?=.*\p{N})(?=.*\p{Lu})(?=.*\p{Ll})(?=.*[^\p{N}\p{L}])%u');
+    }
+
+    protected function vLogin($v, $value, $type)
+    {
+        return $this->vRegex($v, $value, $type, '%^\p{L}[\p{L}\p{N}\x20\._-]+$%uD');
+    }
 }
 }

+ 12 - 13
app/bootstrap.php

@@ -34,8 +34,9 @@ if (!defined('PUN_SEARCH_MAX_WORD'))
 if (!defined('FORUM_MAX_COOKIE_SIZE'))
 if (!defined('FORUM_MAX_COOKIE_SIZE'))
 	define('FORUM_MAX_COOKIE_SIZE', 4048);
 	define('FORUM_MAX_COOKIE_SIZE', 4048);
 
 
-$loader = require __DIR__ . '/../vendor/autoload.php';
-$loader->setPsr4('ForkBB\\', __DIR__ . '/');
+//$loader =
+require __DIR__ . '/../vendor/autoload.php';
+//$loader->setPsr4('ForkBB\\', __DIR__ . '/');
 
 
 if (file_exists(__DIR__ . '/config/main.php')) {
 if (file_exists(__DIR__ . '/config/main.php')) {
     $container = new Container(include __DIR__ . '/config/main.php');
     $container = new Container(include __DIR__ . '/config/main.php');
@@ -64,15 +65,13 @@ while (! $page instanceof Page && $cur = array_pop($controllers)) {
     $page = $container->$cur;
     $page = $container->$cur;
 }
 }
 
 
-if ($page instanceof Page) { //????
-    if ($page->getDataForOnline(true)) {
-        $container->Online->handle($page);
-    }
-    $tpl = $container->View->setPage($page)->outputPage();
-    if (defined('PUN_DEBUG')) {
-        $debug = $container->Debug->debug();
-        $debug = $container->View->setPage($debug)->outputPage();
-        $tpl = str_replace('<!-- debuginfo -->', $debug, $tpl);
-    }
-    exit($tpl);
+if ($page->getDataForOnline(true)) {
+    $container->Online->handle($page);
 }
 }
+$tpl = $container->View->setPage($page)->outputPage();
+if (defined('PUN_DEBUG')) {
+    $debug = $container->Debug->debug();
+    $debug = $container->View->setPage($debug)->outputPage();
+    $tpl = str_replace('<!-- debuginfo -->', $debug, $tpl);
+}
+exit($tpl);

+ 30 - 3
app/lang/English/login.po → app/lang/English/auth.po

@@ -12,6 +12,15 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Language: en\n"
 "Language: en\n"
 
 
+msgid "Sign in"
+msgstr "Sign in"
+
+msgid "Banned email"
+msgstr "The email address you entered is banned in this forum."
+
+msgid "Account is not activated"
+msgstr "Account is not activated."
+
 msgid "Wrong user/pass"
 msgid "Wrong user/pass"
 msgstr "Wrong username and/or password."
 msgstr "Wrong username and/or password."
 
 
@@ -39,11 +48,29 @@ msgstr "Remember me"
 msgid "Forget mail"
 msgid "Forget mail"
 msgstr "An email has been sent to the specified address with instructions on how to change your password. If it does not arrive you can contact the forum administrator at <a href=\"mailto:%1$s\">%1$s</a>."
 msgstr "An email has been sent to the specified address with instructions on how to change your password. If it does not arrive you can contact the forum administrator at <a href=\"mailto:%1$s\">%1$s</a>."
 
 
-msgid "Error mail"
-msgstr "When sending email there was an error. Try later or contact the forum administrator at <a href=\"mailto:%1$s\">%1$s</a>."
-
 msgid "Email flood"
 msgid "Email flood"
 msgstr "This account has already requested a password reset in the past hour. Please wait %s minutes before trying again."
 msgstr "This account has already requested a password reset in the past hour. Please wait %s minutes before trying again."
 
 
 msgid "Pass not match"
 msgid "Pass not match"
 msgstr "Passwords do not match."
 msgstr "Passwords do not match."
+
+msgid "Change pass"
+msgstr "Change password"
+
+msgid "Change password"
+msgstr "Change password"
+
+msgid "New pass"
+msgstr "New password"
+
+msgid "Confirm new pass"
+msgstr "Confirm new password"
+
+msgid "Pass format"
+msgstr "Password must contain the digit, uppercase and lowercase letters, symbol different from the digits and letters."
+
+msgid "Pass info"
+msgstr "Passwords must be at least 8 characters long. Passwords are case sensitive."
+
+msgid "Pass updated"
+msgstr "Your password has been updated. You can now login with your new password."

+ 7 - 1
app/lang/English/common.po

@@ -24,9 +24,15 @@ msgstr "."
 msgid "lang_thousands_sep"
 msgid "lang_thousands_sep"
 msgstr ","
 msgstr ","
 
 
+msgid "Forum rules"
+msgstr "Forum rules"
+
 msgid "Redirecting to index"
 msgid "Redirecting to index"
 msgstr "Redirecting to Index page."
 msgstr "Redirecting to Index page."
 
 
+msgid "Error mail"
+msgstr "When sending email there was an error. Please try again later or contact the forum administrator at <a href=\"mailto:%1$s\">%1$s</a>."
+
 msgid "Bad token"
 msgid "Bad token"
 msgstr "Bad token."
 msgstr "Bad token."
 
 
@@ -451,7 +457,7 @@ msgid "New reports"
 msgstr "There are new reports"
 msgstr "There are new reports"
 
 
 msgid "Maintenance mode enabled"
 msgid "Maintenance mode enabled"
-msgstr "Maintenance mode is enabled!"
+msgstr "<a href=\"%s\"Maintenance mode is enabled!</a>"
 
 
 msgid "Size unit B"
 msgid "Size unit B"
 msgstr "%s B"
 msgstr "%s B"

+ 3 - 3
app/lang/English/mail/new_user.tpl

@@ -1,12 +1,12 @@
 Subject: Alert - New registration
 Subject: Alert - New registration
 
 
-User '<username>' registered in the forums at <base_url>
+User '<username>' registered in the forums at <fRootLink>
 
 
-User profile: <profile_url>
+User profile: <userLink>
 
 
 To administer this account, please visit the following page:
 To administer this account, please visit the following page:
 <admin_url>
 <admin_url>
 
 
 --
 --
-<board_mailer> Mailer
+<fMailer> Mailer
 (Do not reply to this message)
 (Do not reply to this message)

+ 3 - 5
app/lang/English/mail/password_reset.tpl

@@ -2,13 +2,11 @@ Subject: New password requested
 
 
 Hello <username>,
 Hello <username>,
 
 
-You have requested to have a new password assigned to your account in the discussion forum at <base_url>. If you didn't request this or if you don't want to change your password you should just ignore this message. Only if you visit the activation page below will your password be changed.
-
-Your new password is: <new_password>
+You have requested to have a new password assigned to your account in the discussion forum at <fRootLink>
 
 
 To change your password, please visit the following page:
 To change your password, please visit the following page:
-<activation_url>
+<link>
 
 
 --
 --
-<board_mailer> Mailer
+<fMailer> Mailer
 (Do not reply to this message)
 (Do not reply to this message)

+ 5 - 6
app/lang/English/mail/welcome.tpl

@@ -1,12 +1,11 @@
-Subject: Welcome to <board_title>!
+Subject: Welcome to <fTitle>!
 
 
-Thank you for registering in the forums at <base_url>. Your account details are:
+Thank you for registering in the forums at <fRootLink>.
 
 
-Username: <username>
-Password: <password>
+Your username: <username>
 
 
-Login at <login_url> to activate the account.
+Go <link> to activate your account.
 
 
 --
 --
-<board_mailer> Mailer
+<fMailer> Mailer
 (Do not reply to this message)
 (Do not reply to this message)

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

@@ -78,6 +78,9 @@ msgstr "New password"
 msgid "Confirm new pass"
 msgid "Confirm new pass"
 msgstr "Confirm new password"
 msgstr "Confirm new password"
 
 
+msgid "Pass format"
+msgstr "Password must contain the digit, uppercase and lowercase letters, symbol different from the digits and letters."
+
 msgid "Pass info"
 msgid "Pass info"
 msgstr "Passwords must be at least 8 characters long. Passwords are case sensitive."
 msgstr "Passwords must be at least 8 characters long. Passwords are case sensitive."
 
 

+ 70 - 0
app/lang/English/register.po

@@ -0,0 +1,70 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "Sign up"
+msgstr "Sign up"
+
+msgid "If no rules"
+msgstr "Treat other people the way you want to be treated yourself."
+
+msgid "Bad agree"
+msgstr "ID forms obsolete. Please begin <a href=\"%s\">registration</a> again."
+
+msgid "Reg cancel redirect"
+msgstr "No you agree with the forum rules. Registration cancelled. Redirecting …"
+
+msgid "Registration flood"
+msgstr "A new user was registered with the same IP address as you within the last hour. To prevent registration flooding, at least an hour has to pass between registrations from the same IP. Sorry for the inconvenience."
+
+msgid "Agree"
+msgstr "Agree"
+
+msgid "Username guest"
+msgstr "The username guest is reserved. Please choose another username."
+
+msgid "Username censor"
+msgstr "The username you entered contains one or more censored words. Please choose another username."
+
+msgid "Banned username"
+msgstr "The username you entered is banned in this forum. Please choose another username."
+
+msgid "Username not unique"
+msgstr "The username you entered is not unique. Please choose another username."
+
+msgid "Banned email"
+msgstr "The email address you entered is banned in this forum. Please choose another email address."
+
+msgid "Dupe email"
+msgstr "Someone else is already registered with that email address. Please choose another email address."
+
+msgid "Reg email"
+msgstr "Thank you for registering. A letter with a link to activate your account was sent to the specified address. If it doesn't arrive you can contact the forum administrator at <a href=\"mailto:%1$s\">%1$s</a>."
+
+msgid "Reg complete"
+msgstr "Registration complete. You can log in the forums."
+
+msgid "Email info"
+msgstr "Please enter a valid email address. It will be used to activate your account."
+
+msgid "Pass format"
+msgstr "Password must contain the digit, uppercase and lowercase letters, symbol different from the digits and letters."
+
+msgid "Pass info"
+msgstr "Passwords must be at least 8 characters long. Passwords are case sensitive."
+
+msgid "Login format"
+msgstr "The username must begin with a letter. May contain letters, numbers, spaces, dots, dashes and underscores."
+
+msgid "Error welcom mail"
+msgstr "When sending email there was an error. Please use the password reset form or contact the forum administrator at <a href=\"mailto:%1$s\">%1$s</a>."

+ 30 - 3
app/lang/Russian/login.po → app/lang/Russian/auth.po

@@ -12,6 +12,15 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Language: ru\n"
 "Language: ru\n"
 
 
+msgid "Sign in"
+msgstr "Войти"
+
+msgid "Banned email"
+msgstr "Введенный почтовый адрес заблокирован."
+
+msgid "Account is not activated"
+msgstr "Аккаунт не активирован."
+
 msgid "Wrong user/pass"
 msgid "Wrong user/pass"
 msgstr "Неверное имя и/или пароль. Имя и пароль чувствительны к регистру!"
 msgstr "Неверное имя и/или пароль. Имя и пароль чувствительны к регистру!"
 
 
@@ -39,11 +48,29 @@ msgstr "Запомнить меня"
 msgid "Forget mail"
 msgid "Forget mail"
 msgstr "Письмо с инструкцией по изменению пароля было выслано на указанный вами почтовый адрес. Если вы не получите его, свяжитесь с администрацией форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."
 msgstr "Письмо с инструкцией по изменению пароля было выслано на указанный вами почтовый адрес. Если вы не получите его, свяжитесь с администрацией форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."
 
 
-msgid "Error mail"
-msgstr "При отправки письма возникла ошибка. Попробуйте позже или свяжитесь с администратором форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."
-
 msgid "Email flood"
 msgid "Email flood"
 msgstr "Для этой учетной записи недавно уже запрашивали сброс пароля. Пожалуйста, подождите %s минут, прежде чем повторить попытку."
 msgstr "Для этой учетной записи недавно уже запрашивали сброс пароля. Пожалуйста, подождите %s минут, прежде чем повторить попытку."
 
 
 msgid "Pass not match"
 msgid "Pass not match"
 msgstr "Пароли не совпадают."
 msgstr "Пароли не совпадают."
+
+msgid "Change pass"
+msgstr "Смена пароля"
+
+msgid "Change password"
+msgstr "Сменить пароль"
+
+msgid "New pass"
+msgstr "Новый пароль"
+
+msgid "Confirm new pass"
+msgstr "Ещё раз"
+
+msgid "Pass format"
+msgstr "Пароль должен содержать цифру, строчную и прописную буквы, символ отличающийся от цифр и букв."
+
+msgid "Pass info"
+msgstr "Пароль должен состоять минимум из 8 символов. Пароль чувствителен к регистру вводимых букв."
+
+msgid "Pass updated"
+msgstr "Ваш пароль изменён. Вы можете войти на форум с новым паролем."

+ 7 - 1
app/lang/Russian/common.po

@@ -24,9 +24,15 @@ msgstr "."
 msgid "lang_thousands_sep"
 msgid "lang_thousands_sep"
 msgstr ","
 msgstr ","
 
 
+msgid "Forum rules"
+msgstr "Правила форума"
+
 msgid "Redirecting to index"
 msgid "Redirecting to index"
 msgstr "Перенаправление на главную страницу форума."
 msgstr "Перенаправление на главную страницу форума."
 
 
+msgid "Error mail"
+msgstr "При отправке письма возникла ошибка. Пожалуйста, повторите попытку позже или свяжитесь с администратором форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."
+
 msgid "Bad request"
 msgid "Bad request"
 msgstr "Неверный запрос. Ссылка, по которой вы перешли, является неверной или просроченной."
 msgstr "Неверный запрос. Ссылка, по которой вы перешли, является неверной или просроченной."
 
 
@@ -451,7 +457,7 @@ msgid "New reports"
 msgstr "Есть новые сигналы"
 msgstr "Есть новые сигналы"
 
 
 msgid "Maintenance mode enabled"
 msgid "Maintenance mode enabled"
-msgstr "Включен режим обслуживания!"
+msgstr "<a href=\"%s\">Включен режим обслуживания!</a>"
 
 
 msgid "Size unit B"
 msgid "Size unit B"
 msgstr "%s байт"
 msgstr "%s байт"

+ 4 - 4
app/lang/Russian/mail/new_user.tpl

@@ -1,12 +1,12 @@
 Subject: Внимание - Новая регистрация
 Subject: Внимание - Новая регистрация
 
 
-Пользователь '<username>' зарегистрировался на форуме по адресу <base_url>
+Пользователь '<username>' зарегистрировался на форуме по адресу <fRootLink>
 
 
-Адрес профиля пользователя: <profile_url>
+Адрес профиля пользователя: <userLink>
 
 
 Для управления этой учетной записью, пожалуйста, посетите следующую страницу:
 Для управления этой учетной записью, пожалуйста, посетите следующую страницу:
 <admin_url>
 <admin_url>
 
 
 --
 --
-Отправитель <board_mailer>
-(Не отвечайте на это сообщение)
+Отправитель <fMailer>
+(Не отвечайте на это сообщение)

+ 6 - 8
app/lang/Russian/mail/password_reset.tpl

@@ -1,14 +1,12 @@
 Subject: Запрос на смену пароля
 Subject: Запрос на смену пароля
 
 
-Здравствуйте <username>,
+Здравствуйте, <username>.
 
 
-Кто-то, возможно вы, сделал запрос на смену пароля аккаунта на форуме <base_url>. Если это не вы или если вы не хотите менять пароль, просто проигнорируйте это письмо. Ваш пароль изменится только если вы посетите активационную ссылку.
+Кто-то, возможно вы, сделал запрос на смену пароля аккаунта на форуме <fRootLink>
 
 
-Ваш новый пароль: <new_password>
-
-Чтобы установить этот пароль, пожалуйста пройдите по этой ссылке:
-<activation_url>
+Чтобы сменить пароль, вам нужно перейти по ссылке:
+<link>
 
 
 --
 --
-Отправитель <board_mailer>
-(Не отвечайте на это сообщение)
+Отправитель <fMailer>
+(Не отвечайте на это сообщение)

+ 5 - 6
app/lang/Russian/mail/welcome.tpl

@@ -1,12 +1,11 @@
-Subject: Добро пожаловать на <board_title>!
+Subject: Добро пожаловать на <fTitle>!
 
 
-Добро пожаловать на форум по адресу <base_url>. Детали вашего аккаунта:
+Добро пожаловать на форум по адресу <fRootLink>.
 
 
-Пользователь: <username>
-Пароль: <password>
+Ваше имя пользователя: <username>
 
 
-Залогиньтесь на странице <login_url> чтобы активировать аккаунт.
+Перейдите по ссылке <link> чтобы активировать аккаунт.
 
 
 --
 --
-Отправитель <board_mailer>
+Отправитель <fMailer>
 (Не отвечайте на это сообщение)
 (Не отвечайте на это сообщение)

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

@@ -78,6 +78,9 @@ msgstr "Новый пароль"
 msgid "Confirm new pass"
 msgid "Confirm new pass"
 msgstr "Ещё раз"
 msgstr "Ещё раз"
 
 
+msgid "Pass format"
+msgstr "Пароль должен содержать цифру, строчную и прописную буквы, символ отличающийся от цифр и букв."
+
 msgid "Pass info"
 msgid "Pass info"
 msgstr "Пароль должен состоять минимум из 8 символов. Пароль чувствителен к регистру вводимых букв."
 msgstr "Пароль должен состоять минимум из 8 символов. Пароль чувствителен к регистру вводимых букв."
 
 

+ 70 - 0
app/lang/Russian/register.po

@@ -0,0 +1,70 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "Sign up"
+msgstr "Зарегистрироваться"
+
+msgid "If no rules"
+msgstr "Относитесь к другим так, как хотите, чтобы относились к вам."
+
+msgid "Bad agree"
+msgstr "Идентификатор формы устарел. Пожалуйста, начните <a href=\"%s\">регистрацию</a> заново."
+
+msgid "Reg cancel redirect"
+msgstr "Нет вашего согласия с правилами форума. Регистрация отменена. Переадресация &hellip;"
+
+msgid "Registration flood"
+msgstr "Недавно с вашего IP адреса был зарегистрирован новый пользователь. Должно пройти не менее часа до следующей регистрации с этого IP адреса. Извините за неудобства."
+
+msgid "Agree"
+msgstr "Принимаю правила"
+
+msgid "Username guest"
+msgstr "Гость - зарезервированное имя. Пожалуйста, выберите другое имя."
+
+msgid "Username censor"
+msgstr "Выбранное имя пользователя содержит запрещенные слова. Пожалуйста, выберите другое имя."
+
+msgid "Banned username"
+msgstr "Введенное имя пользователя заблокировано. Пожалуйста, выберите другое имя."
+
+msgid "Username not unique"
+msgstr "Выбранное имя пользователя не уникально. Пожалуйста, выберите другое имя."
+
+msgid "Banned email"
+msgstr "Введенный почтовый адрес заблокирован. Пожалуйста, выберите другой адрес."
+
+msgid "Dupe email"
+msgstr "Введенный почтовый адрес уже кем-то используется. Пожалуйста, выберите другой адрес."
+
+msgid "Reg email"
+msgstr "Спасибо за регистрацию. Письмо с ссылкой для активации аккаунта было отправлено на указанный почтовый адрес. Если оно не дойдет, свяжитесь с администратором форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."
+
+msgid "Reg complete"
+msgstr "Регистрация завершена. Вы можете войти на форум."
+
+msgid "Email info"
+msgstr "Укажите действующий почтовый адрес. Он будет использован для активации аккаунта."
+
+msgid "Pass format"
+msgstr "Пароль должен содержать цифру, строчную и прописную буквы, символ отличающийся от цифр и букв."
+
+msgid "Pass info"
+msgstr "Пароль должен состоять минимум из 8 символов. Пароль чувствителен к регистру вводимых букв."
+
+msgid "Login format"
+msgstr "Имя пользователя должно начинаться с буквы. Может содержать буквы, цифры, пробел, точку, дефис и знак подчеркивания."
+
+msgid "Error welcom mail"
+msgstr "При отправке письма возникла ошибка. Пожалуйста, воспользуйтесь формой восстановления пароля или свяжитесь с администратором форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."

+ 13 - 7
app/templates/change_password.tpl

@@ -1,15 +1,21 @@
 @extends('layouts/main')
 @extends('layouts/main')
     <section class="f-main f-login">
     <section class="f-main f-login">
-      <div class="f-wdiv">
+      <div class="f-lrdiv">
         <h2>{!! __('Change pass') !!}</h2>
         <h2>{!! __('Change pass') !!}</h2>
         <form class="f-form" method="post" action="{!! $formAction !!}">
         <form class="f-form" method="post" action="{!! $formAction !!}">
           <input type="hidden" name="token" value="{!! $formToken !!}">
           <input type="hidden" name="token" value="{!! $formToken !!}">
-          <label class="f-child1" for="id-password">{!! __('New pass') !!}</label>
-          <input required id="id-password" type="password" name="password" pattern=".{8,}" autofocus="autofocus" tabindex="1">
-          <label class="f-child1" for="id-password2">{!! __('Confirm new pass') !!}</label>
-          <input required id="id-password2" type="password" name="password2" pattern=".{8,}" tabindex="2">
-          <label class="f-child2">{!! __('Pass info') !!}</label>
-          <input class="f-btn" type="submit" name="login" value="{!! __('Save') !!}" tabindex="3">
+          <div>
+            <label class="f-child1 f-req" for="id-password">{!! __('New pass') !!}</label>
+            <input required class="f-ctrl" id="id-password" type="password" name="password" pattern=".{8,}" autofocus="autofocus" tabindex="1">
+          </div>
+          <div>
+            <label class="f-child1 f-req" for="id-password2">{!! __('Confirm new pass') !!}</label>
+            <input required class="f-ctrl" id="id-password2" type="password" name="password2" pattern=".{8,}" tabindex="2">
+            <label class="f-child4">{!! __('Pass format') !!} {!! __('Pass info') !!}</label>
+          </div>
+          <div>
+            <input class="f-btn" type="submit" name="login" value="{!! __('Change password') !!}" tabindex="3">
+          </div>
         </form>
         </form>
       </div>
       </div>
     </section>
     </section>

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

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
-<html>
+<html lang="{!! $fLang !!}" dir="{!! $fDirection !!}">
 <head>
 <head>
   <meta charset="utf-8">
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">

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

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
-<html>
+<html lang="{!! $fLang !!} dir="{!! $fDirection !!}">
 <head>
 <head>
   <meta charset="utf-8">
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">

+ 17 - 9
app/templates/login.tpl

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

+ 9 - 5
app/templates/password_reset.tpl

@@ -1,13 +1,17 @@
 @extends('layouts/main')
 @extends('layouts/main')
     <section class="f-main f-login">
     <section class="f-main f-login">
-      <div class="f-wdiv">
+      <div class="f-lrdiv">
         <h2>{!! __('Password reset') !!}</h2>
         <h2>{!! __('Password reset') !!}</h2>
         <form class="f-form" method="post" action="{!! $formAction !!}">
         <form class="f-form" method="post" action="{!! $formAction !!}">
           <input type="hidden" name="token" value="{!! $formToken !!}">
           <input type="hidden" name="token" value="{!! $formToken !!}">
-          <label class="f-child1" for="id-email">{!! __('Email') !!}</label>
-          <input required id="id-email" type="text" name="email" value="{{ $email }}" maxlength="80" autofocus="autofocus" spellcheck="false" tabindex="1">
-          <label class="f-child2">{!! __('Password reset info') !!}</label>
-          <input class="f-btn" type="submit" name="submit" value="{!! __('Submit') !!}" tabindex="2">
+          <div>
+            <label class="f-child1 f-req" for="id-email">{!! __('Email') !!}</label>
+            <input required class="f-ctrl" id="id-email" type="text" name="email" value="{{ $email }}" maxlength="80" pattern=".+@.+" autofocus="autofocus" spellcheck="false" tabindex="1">
+            <label class="f-child4">{!! __('Password reset info') !!}</label>
+          </div>
+          <div>
+            <input class="f-btn" type="submit" name="submit" value="{!! __('Send email') !!}" tabindex="2">
+          </div>
         </form>
         </form>
       </div>
       </div>
     </section>
     </section>

+ 29 - 0
app/templates/register.tpl

@@ -0,0 +1,29 @@
+@extends('layouts/main')
+    <section class="f-main f-register">
+      <div class="f-lrdiv">
+        <h2>{!! __('Register') !!}</h2>
+        <form class="f-form" method="post" action="{!! $formAction !!}">
+          <input type="hidden" name="token" value="{!! $formToken !!}">
+          <input type="hidden" name="agree" value="{!! $agree !!}">
+          <input type="hidden" name="on" value="{!! $on !!}">
+          <div>
+            <label class="f-child1 f-req" for="id-email">{!! __('Email') !!}</label>
+            <input required class="f-ctrl" id="id-email" type="text" name="email" value="{{ $email }}" maxlength="80" pattern=".+@.+" autofocus="autofocus" spellcheck="false" tabindex="1">
+            <label class="f-child4 f-fhint">{!! __('Email info') !!}</label>
+          </div>
+          <div>
+            <label class="f-child1 f-req" for="id-username">{!! __('Username') !!}</label>
+            <input required class="f-ctrl" id="id-username" type="text" name="username" value="{{ $username }}" maxlength="25" pattern=".{2,25}" spellcheck="false" tabindex="2">
+            <label class="f-child4 f-fhint">{!! __('Login format') !!}</label>
+          </div>
+          <div>
+            <label class="f-child1 f-req" for="id-password">{!! __('Password') !!}</label>
+            <input required class="f-ctrl" id="id-password" type="password" name="password" pattern=".{8,}" tabindex="3">
+            <label class="f-child4 f-fhint">{!! __('Pass format') !!} {!! __('Pass info') !!}</label>
+          </div>
+          <div>
+           <input class="f-btn" type="submit" name="register" value="{!! __('Sign up') !!}" tabindex="5">
+          </div>
+        </form>
+      </div>
+    </section>

+ 15 - 2
app/templates/rules.tpl

@@ -1,5 +1,18 @@
 @extends('layouts/main')
 @extends('layouts/main')
     <section class="f-main f-rules">
     <section class="f-main f-rules">
-      <h2>{!! __('Forum rules') !!}</h2>
-      <div>{!! $Rules !!}</div>
+      <h2>{!! $title !!}</h2>
+      <div id="id-rules">{!! $rules !!}</div>
+@if($formAction)
+      <div class="f-lrdiv">
+        <form class="f-form" method="post" action="{!! $formAction !!}">
+          <input type="hidden" name="token" value="{!! $formToken !!}">
+          <div>
+            <label class="f-child2"><input type="checkbox" name="agree" value="{!! $formHash !!}" tabindex="1">{!! __('Agree') !!}</label>
+          </div>
+          <div>
+            <input class="f-btn" type="submit" name="register" value="{!! __('Register') !!}" tabindex="2">
+          </div>
+        </form>
+      </div>
+@endif
     </section>
     </section>

+ 5 - 0
composer.json

@@ -11,6 +11,11 @@
             "homepage": "https://github.com/MioVisman"
             "homepage": "https://github.com/MioVisman"
         }
         }
     ],
     ],
+    "autoload": {
+        "psr-4": {
+            "ForkBB\\": "app/"
+        }
+    },
     "require": {
     "require": {
         "php": ">=5.6.0",
         "php": ">=5.6.0",
         "artoodetoo/dirk": "dev-master"
         "artoodetoo/dirk": "dev-master"

+ 2 - 2
composer.lock

@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "hash": "6c899de021c8c6537d723a4f7d26f06d",
-    "content-hash": "b734e8bdbede86eb0dbd047bb31d3a03",
+    "hash": "ad22a23d7b225fa300553d8d0dcdaeb9",
+    "content-hash": "2eea8744cdbc34c8408e6d137176a8df",
     "packages": [
     "packages": [
         {
         {
             "name": "artoodetoo/dirk",
             "name": "artoodetoo/dirk",

+ 1 - 0
db_update.php

@@ -669,6 +669,7 @@ switch ($stage)
 
 
             $db->alter_field('users', 'password', 'VARCHAR(255)', false, '') or error('Unable to alter password field', __FILE__, __LINE__, $db->error());
             $db->alter_field('users', 'password', 'VARCHAR(255)', false, '') or error('Unable to alter password field', __FILE__, __LINE__, $db->error());
             $db->add_field('user', 'u_mark_all_read', 'INT(10) UNSIGNED', true) or error('Unable to add u_mark_all_read field', __FILE__, __LINE__, $db->error());
             $db->add_field('user', 'u_mark_all_read', 'INT(10) UNSIGNED', true) or error('Unable to add u_mark_all_read field', __FILE__, __LINE__, $db->error());
+            $db->add_field('user', 'email_confirmed', 'TINYINT(1)', false, 0, 'email') or error('Unable to add email_confirmed field', __FILE__, __LINE__, $db->error());
 
 
             $db->add_field('online', 'o_position', 'VARCHAR(100)', false, '') or error('Unable to add o_position field', __FILE__, __LINE__, $db->error());
             $db->add_field('online', 'o_position', 'VARCHAR(100)', false, '') or error('Unable to add o_position field', __FILE__, __LINE__, $db->error());
             $db->add_index('online', 'o_position_idx', array('o_position')) or error('Unable to add o_position_idx index', __FILE__, __LINE__, $db->error());
             $db->add_index('online', 'o_position_idx', array('o_position')) or error('Unable to add o_position_idx index', __FILE__, __LINE__, $db->error());

+ 2 - 6
include/functions.php

@@ -706,15 +706,11 @@ function delete_post($post_id, $topic_id)
 function censor_words($text)
 function censor_words($text)
 {
 {
     global $container;
     global $container;
-	static $search_for, $replace_with;
 
 
-	// If not already built in a previous call, build an array of censor words and their replacement text
-	if (!isset($search_for)) {
-        list($search_for, $replace_with) = $container->get('censoring');
-	}
+    list($search_for, $replace_with) = $container->censoring;
 
 
 	if (!empty($search_for)) {
 	if (!empty($search_for)) {
-		$text = substr(ucp_preg_replace($search_for, $replace_with, ' '.$text.' '), 1, -1);
+		$text = preg_replace($search_for, $replace_with, $text);
     }
     }
 
 
 	return $text;
 	return $text;

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

@@ -194,6 +194,10 @@ select {
   content: ", ";
   content: ", ";
 }
 }
 
 
+.f-req:after {
+  content: " *";
+  color: red;
+}
 
 
 /* Меню */
 /* Меню */
 
 
@@ -510,7 +514,7 @@ select {
   padding: 0.625rem;
   padding: 0.625rem;
 }
 }
 
 
-.f-rules > div {
+#id-rules {
   padding: 0.625rem;
   padding: 0.625rem;
   border: 0.0625rem solid #AA7939;
   border: 0.0625rem solid #AA7939;
   background-color: #F8F4E3;
   background-color: #F8F4E3;
@@ -992,26 +996,27 @@ li + li .f-btn {
   }
   }
 }
 }
 
 
-/*********/
-/* Логин */
-/*********/
-.f-login .f-wdiv {
+/*********************/
+/* Логин/Регистрация */
+/*********************/
+.f-lrdiv {
   margin: 1rem auto;
   margin: 1rem auto;
   max-width: 20rem;
   max-width: 20rem;
   border: 0.0625rem solid #AA7939;
   border: 0.0625rem solid #AA7939;
   border-radius: 0.1875rem;
   border-radius: 0.1875rem;
 }
 }
 
 
-.f-login h2 {
+.f-lrdiv h2 {
   padding: 0.625rem;
   padding: 0.625rem;
   text-align: center;
   text-align: center;
 }
 }
 
 
-.f-login .f-form {
+.f-lrdiv .f-form {
   padding: 0.625rem;
   padding: 0.625rem;
 }
 }
 
 
-.f-login .f-form > input {
+.f-lrdiv .f-ctrl,
+.f-lrdiv .f-btn {
   padding: 0.5rem;
   padding: 0.5rem;
   font-size: 1rem;
   font-size: 1rem;
   display: block;
   display: block;
@@ -1020,7 +1025,10 @@ li + li .f-btn {
   box-sizing: border-box;
   box-sizing: border-box;
 }
 }
 
 
-.f-login .f-form > label {
+.f-lrdiv .f-child1,
+.f-lrdiv .f-child2,
+.f-lrdiv .f-child3,
+.f-lrdiv .f-child4 {
   display: block;
   display: block;
   width: 100%;
   width: 100%;
 }
 }
@@ -1031,28 +1039,53 @@ li + li .f-btn {
   font-weight: normal;
   font-weight: normal;
 }
 }
 
 
-.f-login .f-child1 {
+.f-lrdiv .f-child1 {
   font-weight: bold;
   font-weight: bold;
 }
 }
 
 
-.f-login .f-child1:after {
-  content: ":";
-}
-
-.f-login .f-child2 {
+.f-lrdiv .f-child2 {
   font-size: 0.875rem;
   font-size: 0.875rem;
 }
 }
 
 
-.f-login .f-child2 > input {
+.f-lrdiv .f-child2 > input {
   margin: 0 0.625rem 0 0;
   margin: 0 0.625rem 0 0;
 }
 }
 
 
-.f-login .f-btn {
+.f-lrdiv .f-btn {
   margin: 1rem 0 0.375rem 0;
   margin: 1rem 0 0.375rem 0;
 }
 }
 
 
-.f-login .f-child3 {
+.f-lrdiv .f-child3 {
   padding: 0.625rem;
   padding: 0.625rem;
   text-align: center;
   text-align: center;
   font-size: 0.875rem;
   font-size: 0.875rem;
 }
 }
+
+.f-lrdiv .f-child4 {
+  font-size: 0.8125rem;
+  text-align: justify;
+}
+
+.f-ctrl {
+  border: 0.0625rem solid #AA7939;
+}
+
+.f-ctrl:focus {
+  box-shadow: inset 0px 0px 0.25rem 0 rgba(170,121,57,0.5), 0px 0px 0.25rem 0 rgba(170,121,57,0.5);
+}
+
+.f-ctrl + .f-fhint {
+  overflow: hidden;
+  max-height: 0;
+  -webkit-transition: max-height 0.5s, margin 0.5s;
+  -webkit-transition-delay: 0.2s;
+  transition: max-height 0.5s, margin 0.5s;
+  transition-delay: 0.2s;
+}
+
+.f-ctrl:focus + .f-fhint,
+.f-ctrl:active + .f-fhint {
+  margin-top: -0.375rem;
+  margin-bottom: 1rem;
+  max-height: 1000rem;
+}

+ 1 - 0
vendor/composer/autoload_psr4.php

@@ -7,4 +7,5 @@ $baseDir = dirname($vendorDir);
 
 
 return array(
 return array(
     'R2\\Templating\\' => array($vendorDir . '/artoodetoo/dirk/src'),
     'R2\\Templating\\' => array($vendorDir . '/artoodetoo/dirk/src'),
+    'ForkBB\\' => array($baseDir . '/app'),
 );
 );

+ 8 - 0
vendor/composer/autoload_static.php

@@ -11,6 +11,10 @@ class ComposerStaticInit90ad93c7251d4f60daa9e545879c49e7
         array (
         array (
             'R2\\Templating\\' => 14,
             'R2\\Templating\\' => 14,
         ),
         ),
+        'F' => 
+        array (
+            'ForkBB\\' => 7,
+        ),
     );
     );
 
 
     public static $prefixDirsPsr4 = array (
     public static $prefixDirsPsr4 = array (
@@ -18,6 +22,10 @@ class ComposerStaticInit90ad93c7251d4f60daa9e545879c49e7
         array (
         array (
             0 => __DIR__ . '/..' . '/artoodetoo/dirk/src',
             0 => __DIR__ . '/..' . '/artoodetoo/dirk/src',
         ),
         ),
+        'ForkBB\\' => 
+        array (
+            0 => __DIR__ . '/../..' . '/app',
+        ),
     );
     );
 
 
     public static function getInitializer(ClassLoader $loader)
     public static function getInitializer(ClassLoader $loader)