Visman 8 år sedan
förälder
incheckning
ffdcefb076

+ 2 - 3
app/Controllers/Routing.php

@@ -40,8 +40,8 @@ class Routing
             $r->add('GET', '/login/forget', 'Auth:forget', 'Forget');
             $r->add('POST', '/login/forget', 'Auth:forgetPost');
             // смена пароля
-            $r->add('GET', '/login/{email}/{key}', 'Auth:changePass', 'ChangePassword');
-            $r->add('POST', '/login/{email}/{key}', 'Auth:changePassPost');
+            $r->add('GET', '/login/{email}/{key}/{hash}', 'Auth:changePass', 'ChangePassword');
+            $r->add('POST', '/login/{email}/{key}/{hash}', 'Auth:changePassPost');
 
             // регистрация
             if ($config['o_regs_allow'] == '1') {
@@ -120,5 +120,4 @@ class Routing
         }
         return $page;
     }
-
 }

+ 1 - 3
app/Core/Router.php

@@ -59,7 +59,7 @@ class Router
 
     /**
      * Конструктор
-     * @param string $prefix
+     * @param string $base
      */
     public function __construct($base = '')
     {
@@ -183,8 +183,6 @@ class Router
                 return [self::OK, $data[0], $args];
             }
         }
-
-
         if (empty($allowed)) {
             return [self::NOT_FOUND];
         } else {

+ 12 - 18
app/Core/Secury.php

@@ -16,9 +16,7 @@ class Secury
 
     /**
      * Конструктор
-     *
      * @param array $hmac
-     *
      * @throws \InvalidArgumentException
      * @throws \UnexpectedValueException
      */
@@ -35,12 +33,20 @@ class Secury
 
     /**
      * Обертка для hash_hmac
-     *
+     * @param string $data
+     * @return string
+     */
+    public function hash($data)
+    {
+        return $this->hmac($data, md5(__DIR__));
+    }
+
+    /**
+     * Обертка для hash_hmac
      * @param string $data
      * @param string $key
-     *
-     * @throws \InvalidArgumentException
      * @return string
+     * @throws \InvalidArgumentException
      */
     public function hmac($data, $key)
     {
@@ -52,11 +58,9 @@ class Secury
 
     /**
      * Возвращает случайный набор байтов заданной длины
-     *
      * @param int $len
-     *
-     * @throws \RuntimeException
      * @return string
+     * @throws \RuntimeException
      */
     public function randomKey($len)
     {
@@ -76,15 +80,12 @@ class Secury
         if (strlen($key) < $len) {
             throw new RuntimeException('Could not gather sufficient random data');
         }
-
     	return $key;
     }
 
     /**
      * Возвращает случайную строку заданной длины состоящую из символов 0-9 и a-f
-     *
      * @param int $len
-     *
      * @return string
      */
     public function randomHash($len)
@@ -95,9 +96,7 @@ class Secury
     /**
      * Возвращает случайную строку заданной длины состоящую из цифр, латиницы,
      * знака минус и символа подчеркивания
-     *
      * @param int $len
-     *
      * @return string
      */
     public function randomPass($len)
@@ -108,15 +107,12 @@ class Secury
         for ($i = 0; $i < $len; ++$i) {
             $result .= substr($chars, (ord($key[$i]) % strlen($chars)), 1);
         }
-
         return $result;
     }
 
     /**
      * Replacing invalid UTF-8 characters and remove control characters
-     *
      * @param string|array $data
-     *
      * @return string|array
      */
     public function replInvalidChars($data)
@@ -124,13 +120,11 @@ class Secury
         if (is_array($data)) {
             return array_map([$this, 'replInvalidChars'], $data);
         }
-
         // Replacing invalid UTF-8 characters
         // slow, small memory
         //$data = mb_convert_encoding((string) $data, 'UTF-8', 'UTF-8');
         // fast, large memory
         $data = htmlspecialchars_decode(htmlspecialchars((string) $data, ENT_SUBSTITUTE, 'UTF-8'));
-
         // Remove control characters
         return preg_replace('%[\x00-\x08\x0B-\x0C\x0E-\x1F]%', '', $data);
     }

+ 1 - 3
app/Models/Csrf.php

@@ -19,7 +19,6 @@ class Csrf
 
     /**
      * Конструктор
-     *
      * @param Secury $secury
      * @param User $user
      */
@@ -51,7 +50,7 @@ class Csrf
      * @param array $args
      * @return bool
      */
-    public function check($token, $marker, array $args = [])
+    public function verify($token, $marker, array $args = [])
     {
         return is_string($token)
             && preg_match('%f(\d+)$%D', $token, $matches)
@@ -59,5 +58,4 @@ class Csrf
             && $matches[1] + 1800 > time()
             && hash_equals($this->create($marker, $args, $matches[1]), $token);
     }
-
 }

+ 1 - 1
app/Models/Online.php

@@ -37,7 +37,7 @@ class Online
     protected $user;
 
     /**
-     * Контролер
+     * Конструктор
      * @param array $config
      * @param DB $db
      * @param User $user

+ 82 - 77
app/Models/Pages/Auth.php

@@ -33,7 +33,7 @@ class Auth extends Page
     {
         $this->c->get('Lang')->load('login');
 
-        if ($this->c->get('Csrf')->check($args['token'], 'Logout', $args)) {
+        if ($this->c->get('Csrf')->verify($args['token'], 'Logout', $args)) {
             $user = $this->c->get('user');
 
             $this->c->get('UserCookie')->deleteUserCookie();
@@ -43,7 +43,7 @@ class Auth extends Page
             return $this->c->get('Redirect')->setPage('Index')->setMessage(__('Logout redirect'));
         }
 
-        return $this->c->get('Redirect')->setPage('Index')->setMessage(__('Bad request'));
+        return $this->c->get('Redirect')->setPage('Index')->setMessage(__('Bad token'));
     }
 
     /**
@@ -89,36 +89,33 @@ class Auth extends Page
     {
         $this->c->get('Lang')->load('login');
 
-        $username = $this->c->get('Request')->postStr('username', '');
-        $password = $this->c->get('Request')->postStr('password', '');
-        $token = $this->c->get('Request')->postStr('token');
-        $save = $this->c->get('Request')->postStr('save');
+        $v = $this->c->get('Validator');
+        $v->setRules([
+            'token'    => 'token:Login',
+            'redirect' => 'referer:Index',
+            'username' => ['required|string|min:2|max:25', __('Username')],
+            'password' => ['required|string', __('Password')],
+            'save'     => 'checkbox',
+        ]);
 
-        $redirect = $this->c->get('Request')->postStr('redirect', '');
-        $redirect = $this->c->get('Router')->validate($redirect, 'Index');
+        $ok = $v->validation($_POST);
+        $data = $v->getData();
+        $this->iswev = $v->getErrors();
 
-        $args = [
-            '_username' => $username,
-            '_redirect' => $redirect,
-            '_save' => $save,
-        ];
-
-        if (! $this->c->get('Csrf')->check($token, 'Login')) {
-            $this->iswev['e'][] = __('Bad token');
-            return $this->login($args);
-        }
-
-        if (empty($username) || empty($password)) {
+        if ($ok && ! $this->loginProcess($data['username'], $data['password'], $data['save'])) {
             $this->iswev['v'][] = __('Wrong user/pass');
-            return $this->login($args);
+            $ok = false;
         }
 
-        if (! $this->loginProcess($username, $password, ! empty($save))) {
-            $this->iswev['v'][] = __('Wrong user/pass');
-            return $this->login($args);
+        if ($ok) {
+            return $this->c->get('Redirect')->setUrl($data['redirect'])->setMessage(__('Login redirect'));
+        } else {
+            return $this->login([
+                '_username' => $data['username'],
+                '_redirect' => $data['redirect'],
+                '_save'     => $data['save'],
+            ]);
         }
-
-        return $this->c->get('Redirect')->setUrl($redirect)->setMessage(__('Login redirect'));
     }
 
     /**
@@ -196,7 +193,7 @@ class Auth extends Page
         }
 
         $this->titles = [
-            __('Request pass'),
+            __('Password reset'),
         ];
         $this->data = [
             'email' => $args['_email'],
@@ -215,43 +212,43 @@ class Auth extends Page
     {
         $this->c->get('Lang')->load('login');
 
-        $token = $this->c->get('Request')->postStr('token');
-        $email = $this->c->get('Request')->postStr('email');
+        $v = $this->c->get('Validator');
+        $v->setRules([
+            'token' => 'token:Forget',
+            'email' => 'required|email',
+        ])->setMessages([
+            'email' => __('Invalid email'),
+        ]);
 
-        $args = [
-            '_email' => $email,
-        ];
+        $ok = $v->validation($_POST);
+        $data = $v->getData();
+        $this->iswev = $v->getErrors();
 
-        if (! $this->c->get('Csrf')->check($token, 'Forget')) {
-            $this->iswev['e'][] = __('Bad token');
-            return $this->forget($args);
-        }
-
-        $mail = $this->c->get('Mail');
-        if (! $mail->valid($email)) {
+        if ($ok && ($user = $this->c->get('UserMapper')->getUser($data['email'], 'email')) === null) {
             $this->iswev['v'][] = __('Invalid email');
-            return $this->forget($args);
+            $ok = false;
         }
-
-        $user = $this->c->get('UserMapper')->getUser($email, 'email');
-        if (null == $user) {
-            $this->iswev['v'][] = __('Invalid email');
-            return $this->forget($args);
+        if ($ok && ! empty($user->lastEmailSent) && time() - $user->lastEmailSent < 3600) {
+            $this->iswev['e'][] = __('Email flood', (int) (($user->lastEmailSent + 3600 - time()) / 60));
+            $ok = false;
         }
 
-        if (! empty($user->lastEmailSent) && time() - $user->lastEmailSent < 3600) {
-            $this->iswev['e'][] = __('Email flood', (int) (($user->lastEmailSent + 3600 - time()) / 60));
-            return $this->forget($args);
+        if (! $ok) {
+            return $this->forget([
+                '_email' => $data['email'],
+            ]);
         }
 
+        $mail = $this->c->get('Mail');
         $mail->setFolder($this->c->getParameter('DIR_LANG'))
             ->setLanguage($user->language);
 
         $key = 'p' . $this->c->get('Secury')->randomPass(75);
-        $link = $this->c->get('Router')->link('ChangePassword', ['email' => $email, 'key' => $key]);
-        $data = ['key' => $key, 'link' => $link];
+        $hash = $this->c->get('Secury')->hash($data['email'] . $key);
+        $link = $this->c->get('Router')->link('ChangePassword', ['email' => $data['email'], 'key' => $key, 'hash' => $hash]);
+        $tplData = ['link' => $link];
 
-        if ($mail->send($email, 'change_password.tpl', $data)) {
+        if ($mail->send($data['email'], 'change_password.tpl', $tplData)) {
             $this->c->get('UserMapper')->updateUser($user->id, ['activate_string' => $key, 'last_email_sent' => time()]);
             return $this->c->get('Message')->message(__('Forget mail', $this->config['o_admin_email']), false, 200);
         } else {
@@ -269,14 +266,19 @@ class Auth extends Page
         $this->nameTpl = 'login/password';
         $this->onlinePos = 'password';
 
-        // что-то пошло не так
-        if (! $this->c->get('Mail')->valid($args['email'])
-            || ($user = $this->c->get('UserMapper')->getUser($args['email'], 'email')) === null
-            || empty($user->activateString)
-            || $user->activateString{0} !== 'p'
-            || ! hash_equals($user->activateString, $args['key'])
-        ) {
-            return $this->c->get('Message')->message(__('Bad request'), false);
+        if (isset($args['_ok'])) {
+            unset($args['_ok']);
+        } else {
+            // что-то пошло не так
+            if (! hash_equals($args['hash'], $this->c->get('Secury')->hash($args['email'] . $args['key']))
+                || ! $this->c->get('Mail')->valid($args['email'])
+                || ($user = $this->c->get('UserMapper')->getUser($args['email'], 'email')) === null
+                || empty($user->activateString)
+                || $user->activateString{0} !== 'p'
+                || ! hash_equals($user->activateString, $args['key'])
+            ) {
+                return $this->c->get('Message')->message(__('Bad request'), false);
+            }
         }
 
         $this->c->get('Lang')->load('login');
@@ -300,14 +302,11 @@ class Auth extends Page
      */
     public function changePassPost(array $args)
     {
-        $token = $this->c->get('Request')->postStr('token');
-        $password = $this->c->get('Request')->postStr('password', '');
-        $password2 = $this->c->get('Request')->postStr('password2', '');
-
         $this->c->get('Lang')->load('login');
 
         // что-то пошло не так
-        if (! $this->c->get('Mail')->valid($args['email'])
+        if (! hash_equals($args['hash'], $this->c->get('Secury')->hash($args['email'] . $args['key']))
+            || ! $this->c->get('Mail')->valid($args['email'])
             || ($user = $this->c->get('UserMapper')->getUser($args['email'], 'email')) === null
             || empty($user->activateString)
             || $user->activateString{0} !== 'p'
@@ -316,23 +315,29 @@ class Auth extends Page
             return $this->c->get('Message')->message(__('Bad request'), false);
         }
 
-        if (! $this->c->get('Csrf')->check($token, 'ChangePassword', $args)) {
-            $this->iswev['e'][] = __('Bad token');
-            return $this->changePass($args);
-        }
-        if (mb_strlen($password) < 6) {
-            $this->iswev['v'][] = __('Pass too short');
-            return $this->changePass($args);
-        }
-        if ($password !== $password2) {
-            $this->iswev['v'][] = __('Pass not match');
+        $this->c->get('Lang')->load('profile');
+
+        $v = $this->c->get('Validator');
+        $v->setRules([
+            'token'     => 'token:ChangePassword',
+            'password'  => ['required|string|min:8', __('New pass')],
+            'password2' => 'required|same:password',
+        ])->setArguments([
+            'token' => $args,
+        ])->setMessages([
+            'password2' => __('Pass not match'),
+        ]);
+
+        if (! $v->validation($_POST)) {
+            $this->iswev = $v->getErrors();
+            $args['_ok'] = true;
             return $this->changePass($args);
         }
+        $data = $v->getData();
 
-        $this->c->get('UserMapper')->updateUser($user->id, ['password' => password_hash($password, PASSWORD_DEFAULT), 'activate_string' => null]);
+        $this->c->get('UserMapper')->updateUser($user->id, ['password' => password_hash($data['password'], PASSWORD_DEFAULT), 'activate_string' => null]);
 
-        $this->c->get('Lang')->load('profile');
         $this->iswev['s'][] = __('Pass updated');
-        return $this->login([]);
+        return $this->login(['_redirect' => $this->c->get('Router')->link('Index')]);
     }
 }

+ 423 - 0
app/Models/Validator.php

@@ -0,0 +1,423 @@
+<?php
+
+namespace ForkBB\Models;
+
+use R2\DependencyInjection\ContainerInterface;
+use RuntimeException;
+
+class Validator
+{
+    const T_UNKNOWN = 0;
+    const T_STRING = 1;
+    const T_NUMERIC = 2;
+    const T_INT = 3;
+    const T_ARRAY = 4;
+
+    /**
+     * Контейнер
+     * @var ContainerInterface
+     */
+    protected $c;
+
+    /**
+     * @var array
+     */
+    protected $rules;
+
+    /**
+     * @var array
+     */
+    protected $data;
+
+    /**
+     * @var array
+     */
+    protected $arguments;
+
+    /**
+     * @var array
+     */
+    protected $messages;
+
+    /**
+     * @var array
+     */
+    protected $aliases;
+
+    /**
+     * @var array
+     */
+    protected $errors;
+
+    /**
+     * Тип текущего поля при валидации
+     * @var int
+     */
+    protected $curType;
+
+    /**
+     * Конструктор
+     * @param ContainerInterface $container
+     */
+    public function __construct(ContainerInterface $container)
+    {
+        $this->c = $container;
+    }
+
+    /**
+     * Установка правил проверки
+     * @param array $list
+     * @return Validator
+     * @throws RuntimeException
+     */
+    public function setRules(array $list)
+    {
+        $this->rules = [];
+        $this->data = [];
+        $this->alias = [];
+        $this->errors = [];
+        $this->arguments = [];
+        foreach ($list as $field => $raw) {
+            $rules = [];
+            // псевдоним содержится в списке правил
+            if (is_array($raw)) {
+                $this->aliases[$field] = $raw[1];
+                $raw = $raw[0];
+            }
+            // перебор правил для текущего поля
+            $rawRules = explode('|', $raw);
+            foreach ($rawRules as $rule) {
+                 $tmp = explode(':', $rule, 2);
+                 if (! method_exists($this, $tmp[0] . 'Rule')) {
+                     throw new RuntimeException('Rule not found');
+                 }
+                 $rules[$tmp[0]] = isset($tmp[1]) ? $tmp[1] : '';
+            }
+            $this->rules[$field] = $rules;
+        }
+        return $this;
+    }
+
+    /**
+     * Установка дополнительных аргументов для конкретных "имя поля"."имя правила".
+     * @param array $arguments
+     * @return Validator
+     */
+    public function setArguments(array $arguments)
+    {
+        $this->arguments = $arguments;
+        return $this;
+    }
+
+    /**
+     * Установка сообщений для конкретных "имя поля"."имя правила".
+     * @param array $messages
+     * @return Validator
+     */
+    public function setMessages(array $messages)
+    {
+        $this->messages = $messages;
+        return $this;
+    }
+
+    /**
+     * Установка псевдонимов имен полей для сообщений об ошибках
+     * @param array $aliases
+     * @return Validator
+     */
+    public function setAliases(array $aliases)
+    {
+        $this->aliases = $aliases;
+        return $this;
+    }
+
+    /**
+     * Проверка данных
+     * Удачная проверка возвращает true
+     * @param array $raw
+     * @return bool
+     * @throws \RuntimeException
+     */
+    public function validation(array $raw)
+    {
+        if (empty($this->rules)) {
+            throw new RuntimeException('Rules not found');
+        }
+        $ok = true;
+        $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->get('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;
+                }
+            }
+            $ok = $this->error($error, $field, $rule, $attr, $ok);
+            $this->data[$field] = $value;
+        }
+        return $ok;
+    }
+
+    /**
+     * Получение дополнительных аргументов
+     * @param string $field
+     * @param string $field
+     * @return mixed
+     */
+    protected function getArguments($field, $rule)
+    {
+        if (isset($this->arguments[$field . '.'. $rule])) {
+            return $this->arguments[$field . '.'. $rule];
+        } elseif (isset($this->arguments[$field])) {
+            return $this->arguments[$field];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Обработка ошибки
+     * @param mixed $error
+     * @param string $field
+     * @param string $rule
+     * @param string $attr
+     * @param bool $ok
+     * return bool
+     */
+    protected function error($error, $field, $rule, $attr, $ok)
+    {
+        if (is_bool($error)) {
+            return $ok;
+        }
+        // псевдоним имени поля
+        $alias = isset($this->aliases[$field]) ? $this->aliases[$field] : $field;
+        // текст ошибки
+        if (isset($this->messages[$field . '.' . $rule])) {
+            $error = $this->messages[$field . '.' . $rule];
+        } elseif (isset($this->messages[$field])) {
+            $error = $this->messages[$field];
+        }
+        $type = 'v';
+        // ошибка содержит тип
+        if (is_array($error)) {
+            $type = $error[1];
+            $error = $error[0];
+        }
+        $this->errors[$type][] = __($error, [':alias' => $alias, ':attr' => $attr]);
+        return false;
+    }
+
+    /**
+     * Возвращает проверенные данные
+     * Поля с ошибками содержат значения по умолчанию или значения с ошибками
+     * @return array
+     * @throws \RuntimeException
+     */
+    public function getData()
+    {
+        if (empty($this->data)) {
+            throw new RuntimeException('Data not found');
+        }
+        return $this->data;
+    }
+
+    /**
+     * Возращает массив ошибок
+     * @return array
+     */
+    public function getErrors()
+    {
+        return $this->errors;
+    }
+
+    /**
+     * Правило "required"
+     * @param mixed $value
+     * @param string $attrs
+     * @param mixed $args
+     * @return array
+     */
+    protected function requiredRule($value, $attr, $args)
+    {
+        $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 ($f()) {
+            if (is_numeric($value)) {
+                if (is_int(0 + $value)) {
+                    $this->curType = self::T_INT;
+                } else {
+                    $this->curType = self::T_NUMERIC;
+                }
+            }
+            return [$value, false];
+        } else {
+            return [$attr, 'The :alias is required'];
+        }
+    }
+
+    protected function stringRule($value, $attr)
+    {
+        if (is_string($value)) {
+            $this->curType = self::T_STRING;
+            return [$value, false];
+        } else {
+            return [$attr, 'The :alias must be string'];
+        }
+    }
+
+    protected function numericRule($value, $attr)
+    {
+        if (is_numeric($value)) {
+            $this->curType = self::T_NUMERIC;
+            return [0 + $value, false];
+        } else {
+            return [$attr, 'The :alias must be numeric'];
+        }
+    }
+
+    protected function intRule($value, $attr)
+    {
+        if (is_numeric($value) && is_int(0 + $value)) {
+            $this->curType = self::T_INT;
+            return [(int) $value, false];
+        } else {
+            return [$attr, 'The :alias must be integer'];
+        }
+    }
+
+    protected function arrayRule($value, $attr)
+    {
+        if (is_array($value)) {
+            $this->curType = self::T_ARRAY;
+            return [$value, false];
+        } else {
+            return [$attr, 'The :alias must be array'];
+        }
+    }
+
+    protected function minRule($value, $attr)
+    {
+        switch ($this->curType) {
+            case self::T_STRING:
+                if (mb_strlen($value) < $attr) {
+                    return [$value, 'The :alias minimum is :attr characters'];
+                }
+                break;
+            case self::T_NUMERIC:
+            case self::T_INT:
+                if ($value < $attr) {
+                    return [$value, 'The :alias minimum is :attr'];
+                }
+                break;
+            case self::T_ARRAY:
+                if (count($value) < $attr) {
+                    return [$value, 'The :alias minimum is :attr elements'];
+                }
+                break;
+            default:
+                return ['', 'The :alias minimum is :attr'];
+                break;
+        }
+        return [$value, false];
+    }
+
+    protected function maxRule($value, $attr)
+    {
+        switch ($this->curType) {
+            case self::T_STRING:
+                if (mb_strlen($value) > $attr) {
+                    return [$value, 'The :alias maximum is :attr characters'];
+                }
+                break;
+            case self::T_NUMERIC:
+            case self::T_INT:
+                if ($value > $attr) {
+                    return [$value, 'The :alias maximum is :attr'];
+                }
+                break;
+            case self::T_ARRAY:
+                if (count($value) > $attr) {
+                    return [$value, 'The :alias maximum is :attr elements'];
+                }
+                break;
+            default:
+                return ['', 'The :alias maximum is :attr'];
+                break;
+        }
+        return [$value, false];
+    }
+
+    protected function tokenRule($value, $attr, $args)
+    {
+        if (! is_array($args)) {
+            $args = [];
+        }
+        if (is_string($value) && $this->c->get('Csrf')->verify($value, $attr, $args)) {
+            return [$value, false];
+        } else {
+            return ['', ['Bad token', 'e']];
+        }
+    }
+
+    protected function checkboxRule($value, $attr)
+    {
+        return [! empty($value), false]; //????
+    }
+
+    protected function refererRule($value, $attr, $args)
+    {
+        if (! is_array($args)) {
+            $args = [];
+        }
+        return [$this->c->get('Router')->validate($value, $attr), false];
+    }
+
+    protected function emailRule($value, $attr)
+    {
+        if ($this->c->get('Mail')->valid($value)) {
+            return [$value, false];
+        } else {
+            if (! is_string($value)) {
+                $value = (string) $value;
+            }
+            return [$value, 'The :alias is not valid email'];
+        }
+    }
+
+    protected function sameRule($value, $attr)
+    {
+        if (isset($this->data[$attr]) && $value === $this->data[$attr]) {
+            return [$value, false];
+        } else {
+            return [$value, 'The :alias must be same with original'];
+        }
+    }
+}

+ 4 - 7
app/lang/English/login.po

@@ -24,10 +24,10 @@ msgstr "Logged in successfully. Redirecting …"
 msgid "Logout redirect"
 msgstr "Logged out. Redirecting …"
 
-msgid "Request pass"
-msgstr "Request password"
+msgid "Password reset"
+msgstr "Password reset"
 
-msgid "Request pass info"
+msgid "Password reset info"
 msgstr "An email will be sent to the specified address with instructions on how to change your password."
 
 msgid "Not registered"
@@ -43,10 +43,7 @@ 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"
-msgstr "This account has already requested a password reset in the past hour. Please wait %s minutes before requesting a new password again."
-
-msgid "Pass too short"
-msgstr "Passwords must be at least 6 characters long."
+msgstr "This account has already requested a password reset in the past hour. Please wait %s minutes before trying again."
 
 msgid "Pass not match"
 msgstr "Passwords do not match."

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

@@ -79,7 +79,7 @@ msgid "Confirm new pass"
 msgstr "Confirm new password"
 
 msgid "Pass info"
-msgstr "Passwords must be at least 6 characters long. Passwords are case sensitive."
+msgstr "Passwords must be at least 8 characters long. Passwords are case sensitive."
 
 msgid "Email key bad"
 msgstr "The specified email activation key was incorrect or has expired. Please re-request change of email address. If that fails, contact the forum administrator at"

+ 5 - 8
app/lang/Russian/login.po

@@ -24,11 +24,11 @@ msgstr "Успешный вход. Переадресация &hellip;"
 msgid "Logout redirect"
 msgstr "Выход произведён. Переадресация &hellip;"
 
-msgid "Request pass"
-msgstr "Восстановление пароля"
+msgid "Password reset"
+msgstr "Сброс пароля"
 
-msgid "Request pass info"
-msgstr "Инструкция по смене пароля будет выслана на указанный адрес."
+msgid "Password reset info"
+msgstr "Инструкция по смене пароля будет выслана на указанный почтовый адрес."
 
 msgid "Not registered"
 msgstr "Ещё не зарегистрированы?"
@@ -43,10 +43,7 @@ msgid "Error mail"
 msgstr "При отправки письма возникла ошибка. Попробуйте позже или свяжитесь с администратором форума по адресу <a href=\"mailto:%1$s\">%1$s</a>."
 
 msgid "Email flood"
-msgstr "Для этой учетной записи недавно уже запрашивали новый пароль. Пожалуйста, подождите %s минут, прежде чем повторить попытку."
-
-msgid "Pass too short"
-msgstr "Пароль должен состоять минимум из 6 символов."
+msgstr "Для этой учетной записи недавно уже запрашивали сброс пароля. Пожалуйста, подождите %s минут, прежде чем повторить попытку."
 
 msgid "Pass not match"
 msgstr "Пароли не совпадают."

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

@@ -79,7 +79,7 @@ msgid "Confirm new pass"
 msgstr "Ещё раз"
 
 msgid "Pass info"
-msgstr "Пароль должен состоять минимум из 6 символов. Пароль чувствителен к регистру вводимых букв."
+msgstr "Пароль должен состоять минимум из 8 символов. Пароль чувствителен к регистру вводимых букв."
 
 msgid "Email key bad"
 msgstr "Указанный ключ активации почтового адреса неверен или истек срок его действия. Пожалуйста, повторно запросите смену почтового адреса. Если ничего не получится, то свяжитесь с администрацией; почтовый адрес для связи"

+ 2 - 2
app/templates/login/forget.tpl

@@ -1,12 +1,12 @@
 @extends('layouts/main')
     <section class="f-main f-login">
       <div class="f-wdiv">
-        <h2>{!! __('Request pass') !!}</h2>
+        <h2>{!! __('Password reset') !!}</h2>
         <form class="f-form" method="post" action="{!! $formAction !!}">
           <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">{!! __('Request pass info') !!}</label>
+          <label class="f-child2">{!! __('Password reset info') !!}</label>
           <input class="f-btn" type="submit" name="submit" value="{!! __('Submit') !!}" tabindex="2">
         </form>
       </div>

+ 2 - 2
app/templates/login/password.tpl

@@ -5,9 +5,9 @@
         <form class="f-form" method="post" action="{!! $formAction !!}">
           <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=".{6,}" autofocus="autofocus" tabindex="1">
+          <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=".{6,}" tabindex="2">
+          <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">
         </form>

+ 2 - 0
include/functions.php

@@ -2083,6 +2083,8 @@ function __($data, ...$args)
 
     if (empty($args)) {
         return $tr;
+    } elseif (is_array($args[0])) {
+        return strtr($tr, $args[0]);
     } else {
         return sprintf($tr, ...$args);
     }