Bläddra i källkod

Add AdminBans (beginning) + userRules

Visman 5 år sedan
förälder
incheckning
c21c15aae9

+ 6 - 0
app/Controllers/Routing.php

@@ -137,6 +137,12 @@ class Routing
             $r->add(['GET', 'POST'], '/admin/users/{action:\w+}/{ids:\d+(?:-\d+)*}[/{token}]', 'AdminUsersAction:view', 'AdminUsersAction');
 
             $r->add('GET',           '/admin/users/promote/{uid:[2-9]|[1-9]\d+}/{pid:[1-9]\d*}/{token}', 'AdminUsersPromote:promote', 'AdminUserPromote');
+
+            if ($this->c->userRules->banUsers) {
+                $r->add(['GET', 'POST'], '/admin/bans',                                 'AdminBans:view',   'AdminBans');
+                $r->add(['GET', 'POST'], '/admin/bans/new[/{ids:\d+(?:-\d+)*}]',        'AdminBans:newBan', 'AdminBansNew');
+                $r->add('GET',           '/admin/bans/result/{data}[/{page:[1-9]\d*}]', 'AdminBans:result', 'AdminBansResult');
+            }
         }
         // только админ
         if ($user->isAdmin) {

+ 1 - 1
app/Models/BanList/Delete.php

@@ -18,7 +18,7 @@ class Delete extends Method
     {
         if (! empty($ids)) {
             $vars = [
-                ':ids' => $ids
+                ':ids' => $ids,
             ];
             $sql = 'DELETE FROM ::bans WHERE id IN (?ai:ids)';
 

+ 113 - 0
app/Models/BanList/Filter.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace ForkBB\Models\BanList;
+
+use ForkBB\Models\Method;
+use InvalidArgumentException;
+use PDO;
+
+class Filter extends Method
+{
+    /**
+     * Получение списка id банов по условиям
+     *
+     * @param array $filters
+     * @param array $order
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return array
+     */
+    public function filter(array $filters, array $order = [])
+    {
+        $fields  = $this->c->dbMap->bans;
+        $orderBy = [];
+        $where   = [];
+
+        foreach ($order as $field => $dir) {
+            if (! isset($fields[$field])) {
+                throw new InvalidArgumentException("The '{$field}' field is not found");
+            }
+            if ('ASC' !== $dir && 'DESC' !== $dir) {
+                throw new InvalidArgumentException('The sort direction is not defined');
+            }
+            $orderBy[] = "b.{$field} {$dir}";
+        }
+        if (empty($orderBy)) {
+            $orderBy = 'b.id DESC';
+        } else {
+            $orderBy = \implode(', ', $orderBy);
+        }
+
+        $vars = [];
+
+        foreach ($filters as $field => $rule) {
+            if (! isset($fields[$field])) {
+                throw new InvalidArgumentException("The '{$field}' field is not found");
+            }
+            switch ($rule[0]) {
+                case 'LIKE':
+                    if (false !== \strpos($rule[1], '*')) {
+                        // кроме * есть другие символы
+                        if ('' != \trim($rule[1], '*')) {
+                            $where[] = "b.{$field} LIKE ?{$fields[$field]}";
+                            $vars[]  = \str_replace(['%', '*', '_'], ['\\%', '%', '\\_'], $rule[1]);
+                        }
+                        break;
+                    }
+                    $rule[0] = '=';
+                case '=':
+                case '!=':
+                    $where[] = "b.{$field}{$rule[0]}?{$fields[$field]}";
+                    $vars[]  = $rule[1];
+                    break;
+                case 'BETWEEN':
+                    // если и min, и max
+                    if (isset($rule[1], $rule[2])) {
+                        // min меньше max
+                        if ($rule[1] < $rule[2]) {
+                            $where[] = "b.{$field} BETWEEN ?{$fields[$field]} AND ?{$fields[$field]}";
+                            $vars[]  = $rule[1];
+                            $vars[]  = $rule[2];
+                        // min больше max O_o
+                        } elseif ($rule[1] > $rule[2]) {
+                            $where[] = "b.{$field} NOT BETWEEN ?{$fields[$field]} AND ?{$fields[$field]}";
+                            $vars[]  = $rule[1];
+                            $vars[]  = $rule[2];
+                        // min равен max :)
+                        } else {
+                            $where[] = "b.{$field}=?{$fields[$field]}";
+                            $vars[]  = $rule[1];
+                        }
+                    // есть только min
+                    } elseif (isset($rule[1])) {
+                        $where[] = "b.{$field}>=?{$fields[$field]}";
+                        $vars[]  = $rule[1];
+                    // есть только max
+                    } elseif (isset($rule[2])) {
+                        $where[] = "b.{$field}<=?{$fields[$field]}";
+                        $vars[]  = $rule[2];
+                    }
+                    break;
+                default:
+                    throw new InvalidArgumentException('The condition is not defined');
+            }
+        }
+
+        if (empty($where)) {
+            $sql = "SELECT b.id
+                    FROM ::bans AS b
+                    ORDER BY {$orderBy}";
+        } else {
+            $where = \implode(' AND ', $where);
+            $sql = "SELECT b.id
+                    FROM ::bans AS b
+                    WHERE {$where}
+                    ORDER BY {$orderBy}";
+        }
+
+        $ids = $this->c->DB->query($sql, $vars)->fetchAll(PDO::FETCH_COLUMN);
+
+        return $ids;
+    }
+}

+ 36 - 0
app/Models/BanList/GetList.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace ForkBB\Models\BanList;
+
+use ForkBB\Models\Method;
+
+class GetList extends Method
+{
+    /**
+     * Загружает список банов по массиву id
+     *
+     * @param array $ids
+     *
+     * @return array
+     */
+    public function getList(array $ids)
+    {
+        $vars = [
+            ':ids' => $ids,
+        ];
+        $sql = 'SELECT b.id, b.username, b.ip, b.email, b.message, b.expire, u.id as id_creator, u.username as name_creator
+                LEFT JOIN ::users AS u ON u.id=b.ban_creator
+                FROM ::bans AS b
+                WHERE id IN (?ai:ids)';
+
+        $this->c->DB->query($sql, $vars);
+
+        $list = \array_fill_keys($ids, false);
+
+        while ($row = $stmt->fetch()) {
+            $list[$row['id']] = $row;
+        }
+
+        return $list;
+    }
+}

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

@@ -53,8 +53,8 @@ class Admin extends Page
             'users' => [$r->link('AdminUsers'), \ForkBB\__('Users')],
         ];
 
-        if ($this->user->isAdmin || $this->user->g_mod_ban_users == '1') {
-            $nav['bans'] = ['admin_bans.php', \ForkBB\__('Bans')];
+        if ($this->c->userRules->banUsers) {
+            $nav['bans'] = [$r->link('AdminBans'), \ForkBB\__('Bans')];
         }
         if ($this->user->isAdmin || $this->c->config->o_report_method == '0' || $this->c->config->o_report_method == '2') {
             $nav['reports'] = ['admin_reports.php', \ForkBB\__('Reports')];

+ 756 - 0
app/Models/Pages/Admin/Bans.php

@@ -0,0 +1,756 @@
+<?php
+
+namespace ForkBB\Models\Pages\Admin;
+
+use ForkBB\Core\Container;
+use ForkBB\Core\Validator;
+use ForkBB\Models\Pages\Admin;
+use ForkBB\Models\User\Model as User;
+
+class Bans extends Admin
+{
+    /**
+     * Конструктор
+     *
+     * @param Container $container
+     */
+    public function __construct(Container $container)
+    {
+        parent::__construct($container);
+
+        $this->aIndex = 'bans';
+
+        $this->c->Lang->load('admin_bans');
+    }
+
+    /**
+     * Кодирует данные фильтра для url
+     *
+     * @param array $data
+     *
+     * @return string
+     */
+    protected function encodeData(array $data)
+    {
+        unset($data['token']);
+        $data = \base64_encode(\json_encode($data));
+        $hash = $this->c->Secury->hash($data);
+        return "{$data}:{$hash}";
+    }
+
+    /**
+     * Декодирует данные фильтра из url
+     *
+     * @param string $data
+     *
+     * @return mixed
+     */
+    protected function decodeData($data)
+    {
+        $data = \explode(':', $data);
+
+        if (2 !== \count($data)) {
+            return false;
+        }
+
+        if (! \hash_equals($data[1], $this->c->Secury->hash($data[0]))
+            || ! \is_array($data = \json_decode(\base64_decode($data[0], true), true))
+        ) {
+            return false;
+        }
+
+        return $data;
+    }
+
+    /**
+     * Подготавливает данные для шаблона
+     *
+     * @param array $args
+     * @param string $method
+     * @param array $data
+     *
+     * @return Page
+     */
+    public function view(array $args, $method, array $data = [])
+    {
+        $this->nameTpl        = 'admin/bans';
+        $this->formBanPage    = 'AdminBansNew';
+        $this->formBanHead    = \ForkBB\__('New ban head');
+        $this->formBanSubHead = \ForkBB\__('Add ban subhead');
+
+        if ('POST' === $method) {
+            $v = $this->c->Validator->reset()
+            ->addValidators([
+            ])->addRules([
+                'token'           => 'token:AdminBans',
+                'username'        => 'string|max:25',
+                'ip'              => 'string|max:40',
+                'email'           => 'string|max:80',
+                'message'         => 'string|max:255',
+                'expire_1'        => 'date',
+                'expire_2'        => 'date',
+                'order_by'        => 'required|string|in:id,username,ip,email,expire',
+                'direction'       => 'required|string|in:ASC,DESC',
+            ])->addAliases([
+                'username'        => 'Username label',
+                'ip'              => 'IP label',
+                'email'           => 'E-mail label',
+                'message'         => 'Message label',
+                'expire_1'        => 'Expire date label',
+                'expire_2'        => 'Expire date label',
+                'order_by'        => 'Order by label',
+#                        'direction'       => ,
+            ])->addArguments([
+            ])->addMessages([
+            ]);
+
+            if ($v->validation($_POST)) {
+                return $this->c->Redirect->page('AdminBansResult', ['data' => $this->encodeData($v->getData())]);
+            }
+
+            $this->fIswev = $v->getErrors();
+            $this->formSearch = $this->formSearch($v->getData());
+        } else {
+            $this->formSearch = $this->formSearch($data);
+            if (empty($data)) {
+                $this->formBan    = $this->formBan();
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Подготавливает массив данных для формы
+     *
+     * @param array $data
+     *
+     * @return array
+     */
+    protected function formSearch(array $data = [])
+    {
+        $form = [
+            'action' => $this->c->Router->link('AdminBans'),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('AdminBans'),
+            ],
+            'sets'   => [],
+            'btns'   => [
+                'search' => [
+                    'type'      => 'submit',
+                    'value'     => \ForkBB\__('Submit search'),
+                    'accesskey' => 's',
+                ],
+            ],
+        ];
+        $form['sets']['search-info'] = [
+            'info' => [
+                'info1' => [
+                    'type'  => '', //????
+                    'value' => \ForkBB\__('Ban search info'),
+                ],
+            ],
+        ];
+        $fields = [];
+        $fields['username'] = [
+            'type'      => 'text',
+            'maxlength' => 25,
+            'caption'   => \ForkBB\__('Username label'),
+            'value'     => isset($data['username']) ? $data['username'] : null,
+        ];
+        $fields['ip'] = [
+            'type'      => 'text',
+            'maxlength' => 40,
+            'caption'   => \ForkBB\__('IP label'),
+            'value'     => isset($data['ip']) ? $data['ip'] : null,
+        ];
+        $fields['email'] = [
+            'type'      => 'text',
+            'maxlength' => 80,
+            'caption'   => \ForkBB\__('E-mail label'),
+            'value'     => isset($data['email']) ? $data['email'] : null,
+        ];
+        $fields['message'] = [
+            'type'      => 'text',
+            'maxlength' => 255,
+            'caption'   => \ForkBB\__('Message label'),
+            'value'     => isset($data['message']) ? $data['message'] : null,
+        ];
+        $fields['between1'] = [
+            'class' => 'between',
+            'type'  => 'wrap',
+        ];
+        $fields['expire_1'] = [
+            'class'     => 'bstart',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['expire_1']) ? $data['expire_1'] : null,
+            'caption'   => \ForkBB\__('Expire date label'),
+        ];
+        $fields['expire_2'] = [
+            'class'     => 'bend',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['expire_2']) ? $data['expire_2'] : null,
+        ];
+        $fields[] = [
+            'type' => 'endwrap',
+        ];
+        $form['sets']['filters'] = [
+            'legend' => \ForkBB\__('Ban search subhead'),
+            'fields' => $fields,
+        ];
+
+        $fields = [];
+        $fields['between5'] = [
+            'class' => 'between',
+            'type'  => 'wrap',
+        ];
+        $fields['order_by'] = [
+            'class'   => 'bstart',
+            'type'    => 'select',
+            'options' => [
+                'id'       => \ForkBB\__('Order by id'),
+                'username' => \ForkBB\__('Order by username'),
+                'ip'       => \ForkBB\__('Order by ip'),
+                'email'    => \ForkBB\__('Order by e-mail'),
+                'expire'   => \ForkBB\__('Order by expire'),
+            ],
+            'value'   => isset($data['order_by']) ? $data['order_by'] : 'id',
+            'caption' => \ForkBB\__('Order by label'),
+        ];
+        $fields['direction'] = [
+            'class'   => 'bend',
+            'type'    => 'select',
+            'options' => [
+                'ASC'  => \ForkBB\__('Ascending'),
+                'DESC' => \ForkBB\__('Descending'),
+            ],
+            'value'   => isset($data['direction']) ? $data['direction'] : 'DESC',
+        ];
+        $fields[] = [
+            'type' => 'endwrap',
+        ];
+        $form['sets']['sorting'] = [
+            'legend' => \ForkBB\__('Search results legend'),
+            'fields' => $fields,
+        ];
+
+        return $form;
+    }
+
+    /**
+     * Подготавливает массив данных для формы
+     *
+     * @param array $data
+     * @param array $args
+     *
+     * @return array
+     */
+    protected function formBan(array $data = [], array $args = [])
+    {
+        $form = [
+            'action' => $this->c->Router->link($this->formBanPage, $args),
+            'hidden' => [
+                'token' => $this->c->Csrf->create($this->formBanPage, $args),
+            ],
+            'sets'   => [],
+            'btns'   => [
+                'submit' => [
+                    'type'      => 'submit',
+                    'value'     => \ForkBB\__('Submit'),
+                    'accesskey' => 's',
+                ],
+            ],
+        ];
+
+        if ($this->banCount < 2) {
+            $fields = [];
+            $fields['username'] = [
+                'type'      => 'text',
+                'maxlength' => 25,
+                'caption'   => \ForkBB\__('Username label'),
+                'info'      => \ForkBB\__('Username help'),
+                'value'     => isset($data['username']) ? $data['username'] : null,
+            ];
+            $fields['ip'] = [
+                'type'      => 'text',
+                'maxlength' => 255,
+                'caption'   => \ForkBB\__('IP label'),
+                'info'      => \ForkBB\__('IP help'),
+                'value'     => isset($data['ip']) ? $data['ip'] : null,
+            ];
+            $fields['email'] = [
+                'type'      => 'text',
+                'maxlength' => 80,
+                'caption'   => \ForkBB\__('E-mail label'),
+                'info'      => \ForkBB\__('E-mail help'),
+                'value'     => isset($data['email']) ? $data['email'] : null,
+            ];
+            $form['sets']['ban-attrs'] = [
+                'legend' => $this->formBanSubHead,
+                'fields' => $fields,
+            ];
+        }
+
+        $fields = [];
+        $fields['message'] = [
+            'type'      => 'text',
+            'maxlength' => 255,
+            'caption'   => \ForkBB\__('Ban message label'),
+            'info'      => \ForkBB\__('Ban message help'),
+            'value'     => isset($data['message']) ? $data['message'] : null,
+        ];
+        $fields['expire'] = [
+            'type'      => 'text',
+            'maxlength' => 100,
+            'caption'   => \ForkBB\__('Expire date label'),
+            'info'      => \ForkBB\__('Expire date help'),
+            'value'     => isset($data['expire']) ? $data['expire'] : null,
+        ];
+/*
+        $yn     = [1 => \ForkBB\__('Yes'), 0 => \ForkBB\__('No')];
+        $fields['o_default_dst'] = [
+            'type'      => 'radio',
+            'value'     => $config->o_default_dst,
+            'values'    => $yn,
+            'caption'   => \ForkBB\__('DST label'),
+            'info'      => \ForkBB\__('DST help'),
+        ];
+*/
+        $form['sets']['ban-exp'] = [
+            'legend' => \ForkBB\__('Message expiry subhead'),
+            'fields' => $fields,
+        ];
+
+        return $form;
+    }
+
+    /**
+     * Возвращает список id банов по фильтру
+     *
+     * @param array $data
+     *
+     * @return array
+     */
+    protected function forFilter(array $data)
+    {
+        $order = [
+            $data['order_by'] => $data['direction'],
+        ];
+        $filters = [];
+
+        foreach ($data as $field => $value) {
+            if ('order_by' === $field || 'direction' === $field) {
+                continue;
+            }
+
+            $key  = 1;
+            $type = '=';
+
+            if (\preg_match('%^(.+?)_(1|2)$%', $field, $matches)) {
+                $type  = 'BETWEEN';
+                $field = $matches[1];
+                $key   = $matches[2];
+
+                if (\is_string($value)) {
+                    $value = \strtotime($value . ' UTC');
+                }
+            } elseif (\is_string($value)) {
+                $type  = 'LIKE';
+            }
+
+            $filters[$field][0]    = $type;
+            $filters[$field][$key] = $value;
+        }
+
+        return $this->c->bans->filter($filters, $order);
+    }
+
+    /**
+     * Подготавливает данные для шаблона найденных банов
+     *
+     * @param array $args
+     * @param string $method
+     *
+     * @return Page
+     */
+    public function result(array $args, $method)
+    {
+        $data = $this->decodeData($args['data']);
+        if (false === $data) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        $idsN = $this->forFilter($data);
+
+        $number = \count($idsN);
+        if (0 == $number) {
+            $this->fIswev = ['i', \ForkBB\__('No bans found')];
+            return $this->view([], 'GET', $data);
+        }
+
+        $page  = isset($args['page']) ? (int) $args['page'] : 1;
+        $pages = (int) \ceil(($number ?: 1) / $this->c->config->o_disp_users);
+
+        if ($page > $pages) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        $startNum = ($page - 1) * $this->c->config->o_disp_users;
+        $idsN     = \array_slice($idsN, $startNum, $this->c->config->o_disp_users);
+        $banList  = $this->c->bans->list($idsN);
+
+        $this->nameTpl    = 'admin/bans_result';
+        $this->mainSuffix = '-one-column';
+        $this->aCrumbs[]  = [$this->c->Router->link('AdminBansResult', ['data' => $args['data']]), \ForkBB\__('Results head')];
+        $this->formResult = $this->form($banList, $startNum, $args);
+        $this->pagination = $this->c->Func->paginate($pages, $page, 'AdminBansResult', ['data' => $args['data']]);
+
+        return $this;
+    }
+
+    /**
+     * Создает массив данных для формы найденных по фильтру банов
+     *
+     * @param array $bans
+     * @param int $number
+     * @param array $args
+     *
+     * @return array
+     */
+    protected function form(array $bans, $number, array $args)
+    {
+        $form = [
+            'action' => $this->c->Router->link('AdminBansResult', $args),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('AdminBansResult', $args),
+            ],
+            'sets'   => [],
+            'btns'   => [],
+        ];
+/*
+        if ($this->rules->banUsers) {
+            $form['btns']['ban'] = [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Ban'),
+                'accesskey' => null,
+            ];
+        }
+        if ($this->rules->deleteUsers) {
+            $form['btns']['delete'] = [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Delete'),
+                'accesskey' => null,
+            ];
+        }
+        if ($this->rules->changeGroup) {
+            $form['btns']['change_group'] = [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Change group'),
+                'accesskey' => null,
+            ];
+        }
+
+        \array_unshift($users, $this->c->users->create(['id' => -1]));
+*/
+        foreach ($bans as $ban) {
+            if (! \is_array($ban)) {
+                continue; // ????
+            }
+
+            $fields = [];
+            $fields["l{$number}-wrap1"] = [
+                'class' => 'main-result',
+                'type'  => 'wrap',
+            ];
+            $fields["l{$number}-wrap2"] = [
+                'class' => 'user-result',
+                'type'  => 'wrap',
+            ];
+            $fields["l{$number}-username"] = [
+                'class'   => ['result', 'username'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results username head'),
+                'value'   => $ban['username'],
+            ];
+            $fields["l{$number}-email"] = [
+                'class'   => ['result', 'email', 'no-data'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results e-mail head'),
+                'value'   => $ban['email'],
+            ];
+            $fields[] = [
+                'type' => 'endwrap',
+            ];
+            $fields["l{$number}-ip"] = [
+                'class'   => ['result', 'ip'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results IP address head'),
+                'value'   => $ban['ip'],
+            ];
+            $fields["l{$number}-expire"] = [
+                'class'   => ['result', 'expire'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results expire head'),
+                'value'   => $ban['expire'], // ???? перевод в дату
+            ];
+            $fields["l{$number}-message"] = [
+                'class'   => ['result', 'message'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results message head'),
+                'value'   => $ban['message'],
+            ];
+            $fields["l{$number}-creator"] = [
+                'class'   => ['result', 'creator', 'no-data'],
+                'type'    => '1' == $this->c->user->g_view_users ? 'link' : 'str',
+                'caption' => \ForkBB\__('Results banned by head'),
+                'value'   => $ban['name_creator'],
+                'href'    => $this->c->Router->link('User', ['id' => $ban['id_creator'], 'name' => $ban['name_creator'],]), // ????
+            ];
+            $fields[] = [
+                'type' => 'endwrap',
+            ];
+/*
+            $key = $user->isGuest ? "guest{$number}" : "users[{$user->id}]";
+            $fields[$key] = [
+                'class'   => ['check'],
+                'caption' => \ForkBB\__('Select'),
+                'type'    => $user->isGuest ? 'str' : 'checkbox',
+                'value'   => $user->isGuest ? null : $user->id,
+                'checked' => false,
+            ];
+            $form['sets']["l{$number}"] = [
+                'class'  => 'result',
+                'legend' => -1 === $user->id ? null : $number,
+                'fields' => $fields,
+            ];
+*/
+            ++$number;
+        }
+
+        return $form;
+    }
+
+    /**
+     * Создает новый бан
+     *
+     * @param array $args
+     * @param string $method
+     *
+     * @return Page
+     */
+    public function newBan(array $args, $method)
+    {
+        $this->banCount = 0;
+
+        if (! empty($args['ids'])) {
+            $ids = \explode('-', $args['ids']);
+            foreach ($ids as &$id) {
+                if (! \preg_match('%^([2-9]|[1-9]\d+)$%D', $id)) {
+                    return $this->c->Message->message('Bad request');
+                }
+                $id = (int) $id;
+            }
+            unset($id);
+
+            $this->banCount = \count($ids);
+            $tmp   = $this->c->users->load(...$ids);
+
+            if (1 === $this->banCount && $tmp instanceof User) {
+                $userList = [$tmp];
+            } elseif (\is_array($tmp) && \count($tmp) === $this->banCount) {
+                $userList = $tmp;
+            } else {
+                return $this->c->Message->message('No user ID message');
+            }
+
+            foreach ($userList as $user) {
+                if ($user->isAdmin) {
+                    return $this->c->Message->message(\ForkBB\__('User is admin message', $user->username));
+                } elseif ($user->isAdmMod) {
+                    return $this->c->Message->message(\ForkBB\__('User is mod message', $user->username));
+                } elseif ($user->isGuest) { // ???? O_o
+                    return $this->c->Message->message('Cannot ban guest message');
+                }
+            }
+        }
+
+        $data = [];
+
+        $this->nameTpl        = 'admin/bans';
+        $this->formBanPage    = 'AdminBansNew';
+        $this->formBanHead    = \ForkBB\__('New ban head');
+        $this->formBanSubHead = \ForkBB\__('Add ban subhead');
+
+        if ('POST' === $method) {
+            $v = $this->c->Validator->reset()
+            ->addValidators([
+                'user_ban'        => [$this, 'vUserBan'],
+                'ip_ban'          => [$this, 'vIpBan'],
+                'email_ban'       => [$this, 'vEmailBan'],
+                'expire_ban'      => [$this, 'vExpireBan'],
+                'submit_ban'      => [$this, 'vSubmitBan'],
+            ])->addRules([
+                'token'           => 'token:' . $this->formBanPage,
+                'username'        => $this->banCount < 1 ? 'string|max:25|user_ban' : 'absent',
+                'ip'              => $this->banCount < 2 ? 'string:spaces|max:255|ip_ban' : 'absent',
+                'email'           => $this->banCount < 2 ? 'string|max:80|email_ban' : 'absent',
+                'message'         => 'string|max:255',
+                'expire'          => 'date|expire_ban',
+                'submit'          => 'required|submit_ban',
+            ])->addAliases([
+                'username'        => 'Username label',
+                'ip'              => 'IP label',
+                'email'           => 'E-mail label',
+                'message'         => 'Ban message label',
+                'expire'          => 'Expire date label',
+            ])->addArguments([
+                'token'           => $args,
+            ])->addMessages([
+            ]);
+
+            if ($v->validation($_POST)) {
+            }
+
+            $this->fIswev = $v->getErrors();
+            $data = $v->getData();
+        }
+
+        $this->aCrumbs[]      = [$this->c->Router->link($this->formBanPage, $args), $this->formBanSubHead];
+        $this->formBan        = $this->formBan($data, $args);
+
+        return $this;
+    }
+
+    /**
+     * Проверяет имя пользователя для бана
+     *
+     * @param Validator $v
+     * @param null|string $username
+     *
+     * @return null|string
+     */
+    public function vUserBan(Validator $v, $username)
+    {
+        if (empty($v->getErrors()) && '' != \trim($username)) {
+            $user = $this->c->users->create(['username' => $username, 'ciNameSearch' => true]);
+            $user = $this->c->users->load($user);
+
+            if (! $user instanceof User) { // ???? может ли вернутся несколько юзеров?
+                $v->addError('No user message');
+            } elseif ($user->isGuest) { // ???? O_o
+                $v->addError('Cannot ban guest message');
+            } elseif ($user->isAdmin) {
+                $v->addError(\ForkBB\__('User is admin message', $user->username));
+            } elseif ($user->isAdmMod) {
+                $v->addError(\ForkBB\__('User is mod message', $user->username));
+            }
+        }
+
+        return $username;
+    }
+
+    /**
+     * Проверяет ip для бана
+     *
+     * @param Validator $v
+     * @param null|string $ips
+     *
+     * @return null|string
+     */
+    public function vIpBan(Validator $v, $ips)
+    {
+        if ('' != \trim($ips)) {
+            $ending6   = ['', '::'];
+            $ending4   = ['.255', '.255.255', '.255.255.255'];
+            $addresses = \explode(' ', $ips);
+
+            foreach ($addresses as $address) {
+                if (\preg_match('%[:a-fA-F]|\d{4}%', $address)) {
+                    foreach ($ending6 as $ending) {
+                        if (false !== \filter_var($address . $ending, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
+                            continue 2;
+                        }
+                    }
+                } else {
+                    foreach ($ending4 as $ending) {
+                        if (false !== \filter_var($address . $ending, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
+                            continue 2;
+                        }
+                    }
+                }
+
+                $v->addError(\ForkBB\__('Invalid IP message (%s)', $address));
+                break;
+            }
+        }
+
+        return $ips;
+    }
+
+    /**
+     * Проверяет email для бана
+     *
+     * @param Validator $v
+     * @param null|string $email
+     *
+     * @return null|string
+     */
+    public function vEmailBan(Validator $v, $email)
+    {
+        if ('' != \trim($email)) {
+            $error = false;
+
+            if (false !== \strpos($email, '@') && false === $this->c->Mail->valid($email)) {
+                $error = true;
+            } elseif ('.' === $email[0] && false === $this->c->Mail->valid('test@sub' . $email)) {
+                $error = true;
+            } elseif (false === $this->c->Mail->valid('test@' . $email)) {
+                $error = true;
+            }
+
+            if ($error) {
+                $v->addError('Invalid e-mail message');
+            }
+        }
+
+        return $email;
+    }
+
+    /**
+     * Проверяет дату окончания для бана
+     *
+     * @param Validator $v
+     * @param null|string $expire
+     *
+     * @return null|string
+     */
+    public function vExpireBan(Validator $v, $expire)
+    {
+        if ('' != \trim($expire)) {
+            if (\strtotime($expire . ' UTC') - \time() < 86400) {
+                $v->addError('Invalid date message');
+            }
+        }
+
+        return $expire;
+    }
+
+    /**
+     * Проверяет, что форма не пуста
+     *
+     * @param Validator $v
+     * @param null|string $value
+     *
+     * @return null|string
+     */
+    public function vSubmitBan(Validator $v, $value)
+    {
+        if ('' == $v->username && '' == $v->ip && '' == $v->email) {
+            $v->addError('Must enter message');
+        }
+
+        return $value;
+    }
+}

+ 4 - 8
app/Models/Pages/Admin/Users.php

@@ -55,7 +55,7 @@ abstract class Users extends Admin
      */
     protected function decodeData($data)
     {
-        $data = \explode(':', $data, 2);
+        $data = \explode(':', $data);
 
         if (2 !== \count($data)) {
             return false;
@@ -98,10 +98,6 @@ abstract class Users extends Admin
             return false;
         }
 
-        if (! $this->rules instanceof Rules) {
-            $this->rules = $this->c->UsersRules->init();
-        }
-
         $userList = $this->c->users->load(...$selected);
         $result   = [];
         foreach ($userList as $user) {
@@ -111,7 +107,7 @@ abstract class Users extends Admin
 
             switch ($action) {
                 case self::ACTION_BAN:
-                    if (! $this->rules->canBanUser($user)) {
+                    if (! $this->c->userRules->canBanUser($user)) {
                         $this->fIswev = ['v', \ForkBB\__('You are not allowed to ban the %s', $user->username)];
                         if ($user->isAdmin) {
                             $this->fIswev = ['i', \ForkBB\__('No ban admins message')];
@@ -122,7 +118,7 @@ abstract class Users extends Admin
                     }
                     break;
                 case self::ACTION_DEL:
-                    if (! $this->rules->canDeleteUser($user)) {
+                    if (! $this->c->userRules->canDeleteUser($user)) {
                         $this->fIswev = ['v', \ForkBB\__('You are not allowed to delete the %s', $user->username)];
                         if ($user->isAdmin) {
                             $this->fIswev = ['i', \ForkBB\__('No delete admins message')];
@@ -131,7 +127,7 @@ abstract class Users extends Admin
                     }
                     break;
                 case self::ACTION_CHG:
-                    if (! $this->rules->canChangeGroup($user, $profile)) {
+                    if (! $this->c->userRules->canChangeGroup($user, $profile)) {
                         $this->fIswev = ['v', \ForkBB\__('You are not allowed to change group for %s', $user->username)];
                         if ($user->isAdmin) {
                             $this->fIswev = ['i', \ForkBB\__('No move admins message')];

+ 4 - 6
app/Models/Pages/Admin/Users/Action.php

@@ -46,24 +46,22 @@ class Action extends Users
             $profile = false;
         }
 
-        $this->rules = $this->c->UsersRules->init();
-
         $error = false;
         switch ($args['action']) {
             case self::ACTION_BAN:
-                if (! $this->rules->banUsers) {
+                if (! $this->c->userRules->banUsers) {
                     $error = true;
                 }
                 break;
             case self::ACTION_DEL:
-                if (! $this->rules->deleteUsers) {
+                if (! $this->c->userRules->deleteUsers) {
                     $error = true;
                 }
                 break;
             case self::ACTION_CHG:
-                if ($profile && ! $this->rules->canChangeGroup($this->c->users->load((int) $args['ids']), true)) {
+                if ($profile && ! $this->c->userRules->canChangeGroup($this->c->users->load((int) $args['ids']), true)) {
                     $error = true;
-                } elseif (! $profile && ! $this->rules->changeGroup) {
+                } elseif (! $profile && ! $this->c->userRules->changeGroup) {
                     $error = true;
                 }
                 break;

+ 10 - 12
app/Models/Pages/Admin/Users/Result.php

@@ -23,10 +23,8 @@ class Result extends Users
             return $this->c->Message->message('Bad request');
         }
 
-        $this->rules = $this->c->UsersRules->init();
-
         if (isset($data['ip'])) {
-            if (! $this->rules->viewIP) {
+            if (! $this->c->userRules->viewIP) {
                 return $this->c->Message->message('Bad request');
             }
 
@@ -59,9 +57,9 @@ class Result extends Users
                 'token'          => 'token:AdminUsersResult',
                 'users'          => 'required|array',
                 'users.*'        => 'required|integer|min:2|max:9999999999',
-                'ban'            => $this->rules->banUsers ? 'checkbox' : 'absent',
-                'delete'         => $this->rules->deleteUsers ? 'checkbox' : 'absent',
-                'change_group'   => $this->rules->changeGroup ? 'checkbox' : 'absent',
+                'ban'            => $this->c->userRules->banUsers ? 'checkbox' : 'absent',
+                'delete'         => $this->c->userRules->deleteUsers ? 'checkbox' : 'absent',
+                'change_group'   => $this->c->userRules->changeGroup ? 'checkbox' : 'absent',
             ])->addAliases([
                 'users'          => 'Select',
                 'users.*'        => 'Select',
@@ -75,11 +73,11 @@ class Result extends Users
             ]);
 
             if ($v->validation($_POST)) {
-                if (! empty($v->ban) && $this->rules->banUsers) {
+                if (! empty($v->ban) && $this->c->userRules->banUsers) {
                     $action = self::ACTION_BAN;
-                } elseif (! empty($v->delete) && $this->rules->deleteUsers) {
+                } elseif (! empty($v->delete) && $this->c->userRules->deleteUsers) {
                     $action = self::ACTION_DEL;
-                } elseif (! empty($v->change_group) && $this->rules->changeGroup) {
+                } elseif (! empty($v->change_group) && $this->c->userRules->changeGroup) {
                     $action = self::ACTION_CHG;
                 } else {
                     $this->fIswev = ['v', \ForkBB\__('Action not available')];
@@ -220,21 +218,21 @@ class Result extends Users
             'btns'   => [],
         ];
 
-        if ($this->rules->banUsers) {
+        if ($this->c->userRules->banUsers) {
             $form['btns']['ban'] = [
                 'type'      => 'submit',
                 'value'     => \ForkBB\__('Ban'),
                 'accesskey' => null,
             ];
         }
-        if ($this->rules->deleteUsers) {
+        if ($this->c->userRules->deleteUsers) {
             $form['btns']['delete'] = [
                 'type'      => 'submit',
                 'value'     => \ForkBB\__('Delete'),
                 'accesskey' => null,
             ];
         }
-        if ($this->rules->changeGroup) {
+        if ($this->c->userRules->changeGroup) {
             $form['btns']['change_group'] = [
                 'type'      => 'submit',
                 'value'     => \ForkBB\__('Change group'),

+ 1 - 1
app/Models/Pages/Admin/Users/View.php

@@ -119,7 +119,7 @@ class View extends Users
         $this->nameTpl    = 'admin/users';
         $this->formSearch = $this->form($data);
 
-        if ($this->c->UsersRules->init()->viewIP) {
+        if ($this->c->userRules->viewIP) {
             $this->formIP = $this->formIP($data);
         }
 

+ 2 - 2
app/Models/Validators/Username.php

@@ -12,11 +12,11 @@ class Username extends Validators
      * Проверяет имя пользователя
      *
      * @param Validator $v
-     * @param string $username
+     * @param null|string $username
      * @param string $z
      * @param mixed $originalUser
      *
-     * @return string
+     * @return null|string
      */
     public function username(Validator $v, $username, $z, $originalUser)
     {

+ 4 - 0
app/config/main.dist.php

@@ -100,6 +100,7 @@ return [
         'topics'     => \ForkBB\Models\Topic\Manager::class,
         'posts'      => \ForkBB\Models\Post\Manager::class,
         'user'       => '@users:current',
+        'userRules'  => '@UsersRules:init',
         'users'      => \ForkBB\Models\User\Manager::class,
         'groups'     => '@GroupManager:init',
         'categories' => '@CategoriesManager:init',
@@ -176,6 +177,7 @@ return [
         'AdminUsersAction' => \ForkBB\Models\Pages\Admin\Users\Action::class,
         'AdminUsersPromote' => \ForkBB\Models\Pages\Admin\Users\Promote::class,
         'AdminHost'       => \ForkBB\Models\Pages\Admin\Host::class,
+        'AdminBans'       => \ForkBB\Models\Pages\Admin\Bans::class,
 
         'ConfigModel'     => \ForkBB\Models\Config\Model::class,
         'ConfigModelLoad' => \ForkBB\Models\Config\Load::class,
@@ -188,6 +190,8 @@ return [
         'BanListModelCheck'    => \ForkBB\Models\BanList\Check::class,
         'BanListModelDelete'   => \ForkBB\Models\BanList\Delete::class,
         'BanListModelIsBanned' => \ForkBB\Models\BanList\IsBanned::class,
+        'BanListModelFilter'   => \ForkBB\Models\BanList\Filter::class,
+        'BanListModelGetList'  => \ForkBB\Models\BanList\GetList::class,
 
         'CensorshipModel'        => \ForkBB\Models\Censorship\Model::class,
         'CensorshipModelRefresh' => \ForkBB\Models\Censorship\Refresh::class,

+ 196 - 0
app/lang/en/admin_bans.po

@@ -0,0 +1,196 @@
+#
+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 "No user message"
+msgstr "No user by that username registered. If you want to add a ban not tied to a specific username just leave the username blank."
+
+msgid "No user ID message"
+msgstr "No user by that ID registered."
+
+msgid "User is admin message"
+msgstr "The user %s is an administrator and can't be banned. If you want to ban an administrator, you must first demote him/her."
+
+msgid "User is mod message"
+msgstr "The user %s is a moderator and can't be banned. If you want to ban a moderator, you must first demote him/her."
+
+msgid "Must enter message"
+msgstr "You must enter either a username, an IP address or an email address (at least)."
+
+msgid "Cannot ban guest message"
+msgstr "The guest user cannot be banned."
+
+msgid "Invalid IP message (%s)"
+msgstr "You entered an invalid IP/IP-range (%s)."
+
+msgid "Invalid e-mail message"
+msgstr "The email address (e.g. user@domain.com) or partial email address domain (e.g. domain.com or .com) you entered is invalid."
+
+msgid "Duplicate domain message"
+msgstr "The domain %s has already been banned."
+
+msgid "Duplicate e-mail message"
+msgstr "The email address %s has already been banned."
+
+msgid "Invalid date message"
+msgstr "You entered an invalid expire date."
+
+msgid "Invalid date reasons"
+msgstr "The format should be YYYY-MM-DD and the date must be at least one day in the future."
+
+msgid "Ban added redirect"
+msgstr "Ban added. Redirecting …"
+
+msgid "Ban edited redirect"
+msgstr "Ban edited. Redirecting …"
+
+msgid "Ban removed redirect"
+msgstr "Ban removed. Redirecting …"
+
+msgid "New ban head"
+msgstr "New ban"
+
+msgid "Add ban subhead"
+msgstr "Add ban"
+
+msgid "Username label"
+msgstr "Username"
+
+msgid "Username help"
+msgstr "Case-insensitive."
+
+msgid "Username advanced help"
+msgstr "The username to ban (case-insensitive). The next page will let you enter a custom IP and email. If you just want to ban a specific IP/IP-range or email just leave it blank."
+
+msgid "Ban search head"
+msgstr "Ban search"
+
+msgid "Ban search subhead"
+msgstr "Search criteria"
+
+msgid "Ban search info"
+msgstr "Search for bans in the database. You can enter one or more terms to search for. Wildcards in the form of asterisks (*) are accepted. To show all bans leave all fields empty."
+
+msgid "Date help"
+msgstr "(yyyy-mm-dd)"
+
+msgid "Message label"
+msgstr "Message"
+
+msgid "Expire after label"
+msgstr "Expire after"
+
+msgid "Expire before label"
+msgstr "Expire before"
+
+msgid "Order by label"
+msgstr "Order by"
+
+msgid "Order by id"
+msgstr "Record number"
+
+msgid "Order by username"
+msgstr "Username"
+
+msgid "Order by ip"
+msgstr "IP"
+
+msgid "Order by e-mail"
+msgstr "Email"
+
+msgid "Order by expire"
+msgstr "Expire date"
+
+msgid "Ascending"
+msgstr "Ascending"
+
+msgid "Descending"
+msgstr "Descending"
+
+msgid "Submit search"
+msgstr "Submit search"
+
+msgid "E-mail label"
+msgstr "Email"
+
+msgid "E-mail help"
+msgstr "The email or email domain you wish to ban (e.g. someone@somewhere.com, somewhere.com or .com)."
+
+msgid "IP label"
+msgstr "IP address/IP-ranges"
+
+msgid "IP help"
+msgstr "The IP address or IP-ranges you wish to ban (e.g. 150.11.110.1 or 150.11.110). Separate addresses with spaces. If an IP is entered already it is the last known IP of this user in the database."
+
+msgid "IP help link"
+msgstr "Click %s to see IP statistics for this user."
+
+msgid "Ban advanced head"
+msgstr "Ban advanced settings"
+
+msgid "Ban advanced subhead"
+msgstr "Supplement ban with IP and email"
+
+msgid "Ban message label"
+msgstr "Ban message"
+
+msgid "Ban message help"
+msgstr "A message that will be displayed to the banned user when he/she visits the board."
+
+msgid "Message expiry subhead"
+msgstr "Ban message and expiry"
+
+msgid "Ban IP range info"
+msgstr "You should be very careful when banning an IP-range because of the possibility of multiple users matching the same partial IP."
+
+msgid "Expire date label"
+msgstr "Expire date"
+
+msgid "Expire date help"
+msgstr "The date when this ban should be automatically removed (format: yyyy-mm-dd). Leave blank to remove manually."
+
+msgid "Results head"
+msgstr "Search Results"
+
+msgid "Results username head"
+msgstr "Username"
+
+msgid "Results e-mail head"
+msgstr "Email"
+
+msgid "Results IP address head"
+msgstr "IP/IP-ranges"
+
+msgid "Results expire head"
+msgstr "Expires"
+
+msgid "Results message head"
+msgstr "Message"
+
+msgid "Results banned by head"
+msgstr "Banned by"
+
+msgid "Results actions head"
+msgstr "Actions"
+
+msgid "No match"
+msgstr "No match"
+
+msgid "Unknown"
+msgstr "Unknown"
+
+msgid "Search results legend"
+msgstr "Select how to view search results"
+
+msgid "No bans found"
+msgstr "No bans were found matching your criteria."

+ 196 - 0
app/lang/ru/admin_bans.po

@@ -0,0 +1,196 @@
+#
+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 "No user message"
+msgstr "Нет пользователя с таким именем. Если хотите добавить бан без привязки к имени, просто оставьте имя пользователя пустым."
+
+msgid "No user ID message"
+msgstr "Нет зарегистрированного пользователя с таким ID."
+
+msgid "User is admin message"
+msgstr "Пользователь %s является администратором и не может быть забанен. Если хотите забанить администратора, вы должны понизить его в правах."
+
+msgid "User is mod message"
+msgstr "Пользователь %s является модератором и не может быть забанен. Если вы хотите забанить модератора, вы должны понизить его в правах."
+
+msgid "Must enter message"
+msgstr "Вы должны ввести имя пользователя, IP или email (хотя бы что-нибудь одно)."
+
+msgid "Cannot ban guest message"
+msgstr "Гостя нельзя забанить."
+
+msgid "Invalid IP message (%s)"
+msgstr "Вы ввели неверный IP или IP-диапазон (%s)."
+
+msgid "Invalid e-mail message"
+msgstr "Email (т.е. user@domain.com) или доменная часть (т.е. domain.com или .com) введен(а) неверно."
+
+msgid "Duplicate domain message"
+msgstr "Домен %s уже забанен."
+
+msgid "Duplicate e-mail message"
+msgstr "Email %s уже забанен."
+
+msgid "Invalid date message"
+msgstr "Вы ввели неправильную дату окончания."
+
+msgid "Invalid date reasons"
+msgstr "Дата должна быть в формате YYYY-MM-DD и должна быть не ранее, чем завтрашнее число."
+
+msgid "Ban added redirect"
+msgstr "Бан добавлен. Переадресация …"
+
+msgid "Ban edited redirect"
+msgstr "Бан изменен. Переадресация …"
+
+msgid "Ban removed redirect"
+msgstr "Бан удален. Переадресация …"
+
+msgid "New ban head"
+msgstr "Новый бан"
+
+msgid "Add ban subhead"
+msgstr "Добавление бана"
+
+msgid "Username label"
+msgstr "Имя пользователя"
+
+msgid "Username help"
+msgstr "Регистр не важен."
+
+msgid "Username advanced help"
+msgstr "Имя пользователя (регистр неважен). На следующей странице вы сможете ввести IP и email. Если хотите забанить IP/IP-диапазон или email просто оставьте поле пустым."
+
+msgid "Ban search head"
+msgstr "Поиск бана"
+
+msgid "Ban search subhead"
+msgstr "Критерии поиска"
+
+msgid "Ban search info"
+msgstr "Поиск бана по базе. Вы можете ввести одно или несколько условий. Возможны маски в виде звездочки (*). Оставьте все поля пустыми, чтобы увидеть все баны."
+
+msgid "Date help"
+msgstr "(yyyy-mm-dd)"
+
+msgid "Message label"
+msgstr "Сообщение"
+
+msgid "Expire after label"
+msgstr "Устаревает после"
+
+msgid "Expire before label"
+msgstr "Устаревает до"
+
+msgid "Order by label"
+msgstr "Сортировать по"
+
+msgid "Order by id"
+msgstr "Номеру записи"
+
+msgid "Order by username"
+msgstr "Имени пользователя"
+
+msgid "Order by ip"
+msgstr "IP"
+
+msgid "Order by e-mail"
+msgstr "Email"
+
+msgid "Order by expire"
+msgstr "Дате устаревания"
+
+msgid "Ascending"
+msgstr "По возрастанию"
+
+msgid "Descending"
+msgstr "По убыванию"
+
+msgid "Submit search"
+msgstr "Искать"
+
+msgid "E-mail label"
+msgstr "Email"
+
+msgid "E-mail help"
+msgstr "Email или домен, который вы хотите забанить (т.е. someone@somewhere.com, somewhere.com или .com)."
+
+msgid "IP label"
+msgstr "IP адрес/IP-подсеть"
+
+msgid "IP help"
+msgstr "IP или IP-подсеть, которую вы будете банить (т.е. 150.11.110.1 или 150.11.110). Можно перечислить несколько адресов через пробел. Если поле IP уже заполнено, то это последний из использованных IP данного пользователя."
+
+msgid "IP help link"
+msgstr "Кликните %s чтобы посмотреть статистику IP этого пользователя."
+
+msgid "Ban advanced head"
+msgstr "Расширенные настройки"
+
+msgid "Ban advanced subhead"
+msgstr "Дополнительно банить IP и email"
+
+msgid "Ban message label"
+msgstr "Сообщение забаненному"
+
+msgid "Ban message help"
+msgstr "Этот текст будет показан забаненному пользователю, когда он/она посетит форум."
+
+msgid "Message expiry subhead"
+msgstr "Сообщение и срок действия"
+
+msgid "Ban IP range info"
+msgstr "Будьте предельно осторожны с IP-диапазонами, потому что под это правило могут попасть несколько пользователей."
+
+msgid "Expire date label"
+msgstr "Дата окончания"
+
+msgid "Expire date help"
+msgstr "Дата, когда бан автоматически снимется (формат: yyyy-mm-dd). Оставьте пустым если бан можно снять только вручную."
+
+msgid "Results head"
+msgstr "Результаты поиска"
+
+msgid "Results username head"
+msgstr "Пользователь"
+
+msgid "Results e-mail head"
+msgstr "Email"
+
+msgid "Results IP address head"
+msgstr "IP/IP-диапазон"
+
+msgid "Results expire head"
+msgstr "Окончание"
+
+msgid "Results message head"
+msgstr "Сообщение"
+
+msgid "Results banned by head"
+msgstr "Кем забанен"
+
+msgid "Results actions head"
+msgstr "Действия"
+
+msgid "No match"
+msgstr "Не найдено"
+
+msgid "Unknown"
+msgstr "Неизвестно"
+
+msgid "Search results legend"
+msgstr "Настройка параметров вывода результатов"
+
+msgid "No bans found"
+msgstr "По вашему запросу ничего не найдено."

+ 17 - 0
app/templates/admin/bans.forkbb.php

@@ -0,0 +1,17 @@
+@extends ('layouts/admin')
+@if ($form = $p->formSearch)
+      <section class="f-admin f-search-bans">
+        <h2>{!! __('Ban search head') !!}</h2>
+        <div class="f-fdiv">
+    @include ('layouts/form')
+        </div>
+      </section>
+@endif
+@if ($form = $p->formBan)
+      <section class="f-admin f-bans">
+        <h2>{!! $p->formBanHead !!}</h2>
+        <div class="f-fdiv">
+    @include ('layouts/form')
+        </div>
+      </section>
+@endif

+ 44 - 0
app/templates/admin/bans_result.forkbb.php

@@ -0,0 +1,44 @@
+@section ('pagination')
+    @if ($p->pagination)
+        <nav class="f-pages">
+        @foreach ($p->pagination as $cur)
+            @if ($cur[2])
+          <a class="f-page active" href="{!! $cur[0] !!}">{{ $cur[1] }}</a>
+            @elseif ('info' === $cur[1])
+          <span class="f-pinfo">{!! $cur[0] !!}</span>
+            @elseif ('space' === $cur[1])
+          <span class="f-page f-pspacer">{!! __('Spacer') !!}</span>
+            @elseif ('prev' === $cur[1])
+          <a rel="prev" class="f-page f-pprev" href="{!! $cur[0] !!}">{!! __('Previous') !!}</a>
+            @elseif ('next' === $cur[1])
+          <a rel="next" class="f-page f-pnext" href="{!! $cur[0] !!}">{!! __('Next') !!}</a>
+            @else
+          <a class="f-page" href="{!! $cur[0] !!}">{{ $cur[1] }}</a>
+            @endif
+        @endforeach
+        </nav>
+    @endif
+@endsection
+@extends ('layouts/admin')
+@if ($p->pagination)
+      <div class="f-nav-links">
+        <div class="f-nlinks-b">
+    @yield ('pagination')
+        </div>
+      </div>
+@endif
+      <section class="f-admin f-search-result-form">
+        <h2>{!! __('Results head') !!}</h2>
+        <div class="f-fdiv">
+@if ($form = $p->formResult)
+    @include ('layouts/form')
+@endif
+        </div>
+      </section>
+@if ($p->pagination)
+      <div class="f-nav-links">
+        <div class="f-nlinks">
+    @yield ('pagination')
+        </div>
+      </div>
+@endif

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

@@ -1702,7 +1702,7 @@ body,
   display: inline-block;
 }
 
-#fork #id-message {
+#fork textarea#id-message {
   height: 11rem;
 }