Explorar el Código

* Admin -> Users

Visman hace 6 años
padre
commit
5e50f2f5e9

+ 2 - 4
app/Controllers/Routing.php

@@ -125,8 +125,7 @@ class Routing
             $r->add('GET', '/admin/statistics', 'AdminStatistics:statistics', 'AdminStatistics');
             $r->add('GET', '/admin/statistics', 'AdminStatistics:statistics', 'AdminStatistics');
 
 
             $r->add(['GET', 'POST'], '/admin/users', 'AdminUsers:view', 'AdminUsers');
             $r->add(['GET', 'POST'], '/admin/users', 'AdminUsers:view', 'AdminUsers');
-            $r->add('GET',           '/admin/users/filter/{filters}/{hash}[/{page:[1-9]\d*}]', 'AdminUsers:viewFilter', 'AdminUsersFilter');
-
+            $r->add(['GET', 'POST'], '/admin/users/result/{data}[/{page:[1-9]\d*}]', 'AdminUsersResult:view', 'AdminUsersResult');
         }
         }
         // только админ
         // только админ
         if ($user->isAdmin) {
         if ($user->isAdmin) {
@@ -150,8 +149,7 @@ class Routing
             $r->add('GET',           '/admin/maintenance/rebuild/{token}/{clear:[01]}/{limit:[1-9]\d*}/{start:[1-9]\d*}', 'AdminMaintenance:rebuild', 'AdminRebuildIndex' );
             $r->add('GET',           '/admin/maintenance/rebuild/{token}/{clear:[01]}/{limit:[1-9]\d*}/{start:[1-9]\d*}', 'AdminMaintenance:rebuild', 'AdminRebuildIndex' );
 
 
             $r->add('GET',           '/admin/get/host/{ip:[0-9a-fA-F:.]+}',      'AdminHost:view',           'AdminHost');
             $r->add('GET',           '/admin/get/host/{ip:[0-9a-fA-F:.]+}',      'AdminHost:view',           'AdminHost');
-            $r->add('GET',           '/admin/users/ip/{ip:[0-9a-fA-F:.]+}[/{page:[1-9]\d*}]',      'AdminUsers:viewIP',            'AdminUsersIP');
-            $r->add('GET',           '/admin/users/user/{id:[2-9]|[1-9]\d+}[/{page:[1-9]\d*}]',      'AdminUsers:viewUserStat',            'AdminUserStat');
+            $r->add('GET',           '/admin/users/user/{id:[2-9]|[1-9]\d+}[/{page:[1-9]\d*}]',      'AdminUsersStat:view',            'AdminUserStat');
         }
         }
 
 
         $uri = $_SERVER['REQUEST_URI'];
         $uri = $_SERVER['REQUEST_URI'];

+ 1 - 1
app/Models/Pages/Admin/Host.php

@@ -28,7 +28,7 @@ class Host extends Admin
 
 
         $this->nameTpl     = 'message';
         $this->nameTpl     = 'message';
         $this->titles      = \ForkBB\__('Info');
         $this->titles      = \ForkBB\__('Info');
-        $this->message     = \ForkBB\__('Host info', $ip, $host, $this->c->Router->link('AdminUsersIP', ['ip' => $ip]));
+        $this->message     = \ForkBB\__('Host info', $ip, $host, $this->c->Router->link('AdminUsersResult', ['data' => "ip:{$ip}"]));
         $this->back        = true;
         $this->back        = true;
 
 
         return $this;
         return $this;

+ 32 - 717
app/Models/Pages/Admin/Users.php

@@ -2,755 +2,70 @@
 
 
 namespace ForkBB\Models\Pages\Admin;
 namespace ForkBB\Models\Pages\Admin;
 
 
-use ForkBB\Core\Validator;
+use ForkBB\Core\Container;
 use ForkBB\Models\Pages\Admin;
 use ForkBB\Models\Pages\Admin;
-use ForkBB\Models\Config\Model as Config;
 
 
-class Users extends Admin
+abstract class Users extends Admin
 {
 {
     /**
     /**
-     * Генерирует список доступных групп пользователей
+     * Конструктор
      *
      *
-     * @param bool $onlyKeys
-     *
-     * @return array
+     * @param Container $container
      */
      */
-    protected function groups($onlyKeys = false)
+    public function __construct(Container $container)
     {
     {
-        $groups = [
-            -1 => \ForkBB\__('All groups'),
-            0  => \ForkBB\__('Unverified users'),
-        ];
-
-        foreach ($this->c->groups->getList() as $group) {
-            if (! $group->groupGuest) {
-                $groups[$group->g_id] = $group->g_title;
-            }
-        }
-
-        return $onlyKeys ? \array_keys($groups) : $groups;
-    }
-
-    /**
-     * Подготавливает данные для шаблона ip статистики для пользователя
-     *
-     * @param array $args
-     * @param string $method
-     *
-     * @return Page
-     */
-    public function viewUserStat(array $args, $method)
-    {
-        $this->c->Lang->load('admin_users');
-
-        $stat   = $this->c->posts->userStat($args['id']);
-        $number = \count($stat);
-
-        $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;
-        $stat     = \array_slice($stat, $startNum, $this->c->config->o_disp_users);
-
-        $user = $this->c->users->load((int) $args['id']);
-
-        if (0 == $number) {
-            $this->fIswev = ['i', \ForkBB\__('Results no posts found')];
-        }
+        parent::__construct($container);
 
 
-        $this->nameTpl    = 'admin/users_result';
         $this->aIndex     = 'users';
         $this->aIndex     = 'users';
-        $this->mainSuffix = '-one-column';
-        $this->aCrumbs[]  = [$this->c->Router->link('AdminUserStat', ['id' => $args['id']]), $user->username];
-        $this->formResult = $this->formStat($stat, $startNum);
-        $this->pagination = $this->c->Func->paginate($pages, $page, 'AdminUserStat', ['id' => $args['id']]);
-
-        return $this;
-    }
-
-    /**
-     * Подготавливает данные для шаблона найденных по ip пользователей
-     *
-     * @param array $args
-     * @param string $method
-     *
-     * @return Page
-     */
-    public function viewIP(array $args, $method)
-    {
-        $ip = \filter_var($args['ip'], \FILTER_VALIDATE_IP);
-        if (false === $ip) {
-            return $this->c->Message->message('Bad request');
-        }
 
 
         $this->c->Lang->load('admin_users');
         $this->c->Lang->load('admin_users');
-
-        $fromPosts = $this->c->posts->userInfoFromIP($ip);
-        $ids       = $this->c->users->filter([
-            'registration_ip' => ['=', $ip],
-        ]);
-        $ids       = \array_flip($ids);
-
-        foreach ($fromPosts as $val) {
-            if (isset($ids[$val])) {
-                unset($ids[$val]);
-            }
-        }
-        $ids    = \array_merge($fromPosts, $ids);
-        $number = \count($ids);
-
-        if (0 == $number) {
-            $this->fIswev = ['i', \ForkBB\__('No users found')];
-
-            return $this->view([], 'GET', ['ip' => $ip]);
-        }
-
-        $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;
-        $ids      = \array_slice($ids, $startNum, $this->c->config->o_disp_users);
-        $userList = $this->c->users->load($ids);
-
-        $this->nameTpl    = 'admin/users_result';
-        $this->aIndex     = 'users';
-        $this->mainSuffix = '-one-column';
-        $this->aCrumbs[]  = [$this->c->Router->link('AdminUsersIP', ['ip' => $ip]), $ip];
-        $this->formResult = $this->formUsers($userList, $startNum);
-        $this->pagination = $this->c->Func->paginate($pages, $page, 'AdminUsersIP', ['ip' => $ip]);
-
-        return $this;
     }
     }
 
 
     /**
     /**
-     * Подготавливает данные для шаблона найденных по фильтру пользователей
+     * Кодирует данные фильтра для url
      *
      *
-     * @param array $args
-     * @param string $method
+     * @param string|array $data
      *
      *
-     * @return Page
+     * @return string
      */
      */
-    public function viewFilter(array $args, $method)
+    protected function encodeData($data)
     {
     {
-        if (! \hash_equals($args['hash'], $this->c->Secury->hash($args['filters']))
-            || ! \is_array($data = \json_decode(\base64_decode($args['filters'], true), true))
-        ) {
-            return $this->c->Message->message('Bad request');
-        }
-
-        $this->c->Lang->load('admin_users');
-
-        $order = [
-            $data['order_by'] => $data['direction'],
-        ];
-        $filters = [];
-
-        if ($data['user_group'] > -1) {
-            $filters['group_id'] = ['=', $data['user_group']];
-        }
-
-        foreach ($data as $field => $value) {
-            if ('order_by' === $field || 'direction' === $field || 'user_group' === $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;
-        }
-
-        $ids    = $this->c->users->filter($filters, $order);
-        $number = \count($ids);
-
-        if (0 == $number) {
-            $this->fIswev = ['i', \ForkBB\__('No users 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');
+        if (\is_array($data)) {
+            unset($data['token']);
+            $data = \base64_encode(\json_encode($data));
+            $hash = $this->c->Secury->hash($data);
+            return "{$data}:{$hash}";
+        } else {
+            return "ip:{$data}";
         }
         }
-
-        $startNum = ($page - 1) * $this->c->config->o_disp_users;
-        $ids      = \array_slice($ids, $startNum, $this->c->config->o_disp_users);
-        $userList = $this->c->users->load($ids);
-
-        $this->nameTpl    = 'admin/users_result';
-        $this->aIndex     = 'users';
-        $this->mainSuffix = '-one-column';
-        $this->aCrumbs[]  = [$this->c->Router->link('AdminUsersFilter', ['filters' => $args['filters'], 'hash' => $args['hash']]), \ForkBB\__('Results head')];
-        $this->formResult = $this->formUsers($userList, $startNum);
-        $this->pagination = $this->c->Func->paginate($pages, $page, 'AdminUsersFilter', ['filters' => $args['filters'], 'hash' => $args['hash']]);
-
-        return $this;
     }
     }
 
 
     /**
     /**
-     * Подготавливает данные для шаблона поиска пользователей
+     * Декодирует данные фильтра из url
      *
      *
-     * @param array $args
-     * @param string $method
-     * @param array $data
+     * @param string $data
      *
      *
-     * @return Page
+     * @return mixed
      */
      */
-    public function view(array $args, $method, array $data = [])
+    protected function decodeData($data)
     {
     {
-        $this->c->Lang->load('admin_users');
-
-        if ('POST' === $method) {
-            $v = $this->c->Validator->reset()
-                ->addRules([
-                    'token' => 'token:AdminUsers',
-                    'ip'    => 'required',
-                ]);
+        $data = \explode(':', $data, 2);
 
 
-            if ($v->validation($_POST)) {
-                $ip = \filter_var($v->ip, \FILTER_VALIDATE_IP);
-
-                if (false === $ip) {
-                    $this->fIswev = ['v', \ForkBB\__('Bad IP message')];
-                    $data         = $v->getData();
-                } else {
-                    return $this->c->Redirect->page('AdminUsersIP', ['ip' => $ip]);
-                }
-            } else {
-                $v = $this->c->Validator->reset()
-                    ->addValidators([
-                    ])->addRules([
-                        'token'           => 'token:AdminUsers',
-                        'username'        => 'string|max:25',
-                        'email'           => 'string|max:80',
-                        'title'           => 'string|max:50',
-                        'realname'        => 'string|max:40',
-                        'gender'          => 'integer|in:0,1,2',
-                        'url'             => 'string|max:100',
-                        'location'        => 'string|max:30',
-                        'signature'       => 'string|max:512',
-                        'admin_note'      => 'string|max:30',
-                        'num_posts_1'     => 'integer|min:0|max:9999999999',
-                        'num_posts_2'     => 'integer|min:0|max:9999999999',
-                        'last_post_1'     => 'date',
-                        'last_post_2'     => 'date',
-                        'last_visit_1'    => 'date',
-                        'last_visit_2'    => 'date',
-                        'registered_1'    => 'date',
-                        'registered_2'    => 'date',
-                        'order_by'        => 'required|string|in:username,email,num_posts,last_post,last_visit,registered',
-                        'direction'       => 'required|string|in:ASC,DESC',
-                        'user_group'      => 'required|integer|in:' . \implode(',', $this->groups(true)),
-                    ])->addAliases([
-                        'username'        => 'Username label',
-                        'email'           => 'E-mail address label',
-                        'title'           => 'Title label',
-                        'realname'        => 'Real name label',
-                        'gender'          => 'Gender label',
-                        'url'             => 'Website label',
-                        'location'        => 'Location label',
-                        'signature'       => 'Signature label',
-                        'admin_note'      => 'Admin note label',
-                        'num_posts_1'     => 'Posts label',
-                        'num_posts_2'     => 'Posts label',
-                        'last_post_1'     => 'Last post label',
-                        'last_post_2'     => 'Last post label',
-                        'last_visit_1'    => 'Last visit label',
-                        'last_visit_2'    => 'Last visit label',
-                        'registered_1'    => 'Registered label',
-                        'registered_2'    => 'Registered label',
-                        'order_by'        => 'Order by label',
-#                        'direction'       => ,
-                        'user_group'      => 'User group label',
-                    ])->addArguments([
-                    ])->addMessages([
-                    ]);
-
-                if ($v->validation($_POST)) {
-                    $filters = $v->getData();
-                    unset($filters['token']);
-                    $filters = \base64_encode(\json_encode($filters));
-                    $hash    = $this->c->Secury->hash($filters);
-                    return $this->c->Redirect->page('AdminUsersFilter', ['filters' => $filters, 'hash' => $hash]);
-                }
-
-                $this->fIswev = $v->getErrors();
-                $data         = $v->getData();
-            }
+        if (2 !== \count($data)) {
+            return false;
         }
         }
 
 
-        $this->nameTpl    = 'admin/users';
-        $this->aIndex     = 'users';
-        $this->formSearch = $this->formSearch($data);
-
-        if ($this->user->isAdmin) {
-            $this->formIP = $this->formIP($data);
+        if ('ip' === $data[0]) {
+            $ip = \filter_var($data[1], \FILTER_VALIDATE_IP);
+            return false === $ip ? false : ['ip' => $ip];
         }
         }
 
 
-        return $this;
-    }
-
-    /**
-     * Создает массив данных для формы поиска
-     *
-     * @param array $data
-     *
-     * @return array
-     */
-    protected function formSearch(array $data)
-    {
-        $form = [
-            'action' => $this->c->Router->link('AdminUsers'),
-            'hidden' => [
-                'token' => $this->c->Csrf->create('AdminUsers'),
-            ],
-            'sets'   => [],
-            'btns'   => [
-                'search' => [
-                    'type'      => 'submit',
-                    'value'     => \ForkBB\__('Submit search'),
-                    'accesskey' => 's',
-                ],
-            ],
-        ];
-        $form['sets']['search-info'] = [
-            'info' => [
-                'info1' => [
-                    'type'  => '', //????
-                    'value' => \ForkBB\__('User search info'),
-                ],
-            ],
-        ];
-        $fields = [];
-        $fields['username'] = [
-            'type'      => 'text',
-            'maxlength' => 25,
-            'caption'   => \ForkBB\__('Username label'),
-            'value'     => isset($data['username']) ? $data['username'] : null,
-        ];
-        $fields['email'] = [
-            'type'      => 'text',
-            'maxlength' => 80,
-            'caption'   => \ForkBB\__('E-mail address label'),
-            'value'     => isset($data['email']) ? $data['email'] : null,
-        ];
-        $fields['title'] = [
-            'type'      => 'text',
-            'maxlength' => 50,
-            'caption'   => \ForkBB\__('Title label'),
-            'value'     => isset($data['title']) ? $data['title'] : null,
-        ];
-        $fields['realname'] = [
-            'type'      => 'text',
-            'maxlength' => 40,
-            'caption'   => \ForkBB\__('Real name label'),
-            'value'     => isset($data['realname']) ? $data['realname'] : null,
-        ];
-        $genders = [
-            0 => \ForkBB\__('Do not display'),
-            1 => \ForkBB\__('Male'),
-            2 => \ForkBB\__('Female'),
-        ];
-        $fields['gender'] = [
-#            'class'   => 'block',
-            'type'    => 'radio',
-            'value'   => isset($data['gender']) ? $data['gender'] : -1,
-            'values'  => $genders,
-            'caption' => \ForkBB\__('Gender label'),
-        ];
-        $fields['url'] = [
-            'id'        => 'website',
-            'type'      => 'text',
-            'maxlength' => 100,
-            'caption'   => \ForkBB\__('Website label'),
-            'value'     => isset($data['url']) ? $data['url'] : null,
-        ];
-        $fields['location'] = [
-            'type'      => 'text',
-            'maxlength' => 30,
-            'caption'   => \ForkBB\__('Location label'),
-            'value'     => isset($data['location']) ? $data['location'] : null,
-        ];
-        $fields['signature'] = [
-            'type'      => 'text',
-            'maxlength' => 512,
-            'caption'   => \ForkBB\__('Signature label'),
-            'value'     => isset($data['signature']) ? $data['signature'] : null,
-        ];
-        $fields['admin_note'] = [
-            'type'      => 'text',
-            'maxlength' => 30,
-            'caption'   => \ForkBB\__('Admin note label'),
-            'value'     => isset($data['admin_note']) ? $data['admin_note'] : null,
-        ];
-        $fields['between1'] = [
-            'class' => 'between',
-            'type'  => 'wrap',
-        ];
-        $fields['num_posts_1'] = [
-            'type'    => 'number',
-            'class'   => 'bstart',
-            'min'     => 0,
-            'max'     => 9999999999,
-            'value'   => isset($data['num_posts_1']) ? $data['num_posts_1'] : null,
-            'caption' => \ForkBB\__('Posts label'),
-        ];
-        $fields['num_posts_2'] = [
-            'type'    => 'number',
-            'class'   => 'bend',
-            'min'     => 0,
-            'max'     => 9999999999,
-            'value'   => isset($data['num_posts_2']) ? $data['num_posts_2'] : null,
-        ];
-        $fields[] = [
-            'type' => 'endwrap',
-        ];
-        $fields['between2'] = [
-            'class' => 'between',
-            'type'  => 'wrap',
-        ];
-        $fields['last_post_1'] = [
-            'class'     => 'bstart',
-            'type'      => 'text',
-            'maxlength' => 100,
-            'value'     => isset($data['last_post_1']) ? $data['last_post_1'] : null,
-            'caption'   => \ForkBB\__('Last post label'),
-        ];
-        $fields['last_post_2'] = [
-            'class'     => 'bend',
-            'type'      => 'text',
-            'maxlength' => 100,
-            'value'     => isset($data['last_post_2']) ? $data['last_post_2'] : null,
-        ];
-        $fields[] = [
-            'type' => 'endwrap',
-        ];
-        $fields['between3'] = [
-            'class' => 'between',
-            'type'  => 'wrap',
-        ];
-        $fields['last_visit_1'] = [
-            'class'     => 'bstart',
-            'type'      => 'text',
-            'maxlength' => 100,
-            'value'     => isset($data['last_visit_1']) ? $data['last_visit_1'] : null,
-            'caption'   => \ForkBB\__('Last visit label'),
-        ];
-        $fields['last_visit_2'] = [
-            'class'     => 'bend',
-            'type'      => 'text',
-            'maxlength' => 100,
-            'value'     => isset($data['last_visit_2']) ? $data['last_visit_2'] : null,
-        ];
-        $fields[] = [
-            'type' => 'endwrap',
-        ];
-        $fields['between4'] = [
-            'class' => 'between',
-            'type'  => 'wrap',
-        ];
-        $fields['registered_1'] = [
-            'class'     => 'bstart',
-            'type'      => 'text',
-            'maxlength' => 100,
-            'value'     => isset($data['registered_1']) ? $data['registered_1'] : null,
-            'caption'   => \ForkBB\__('Registered label'),
-        ];
-        $fields['registered_2'] = [
-            'class'     => 'bend',
-            'type'      => 'text',
-            'maxlength' => 100,
-            'value'     => isset($data['registered_2']) ? $data['registered_2'] : null,
-        ];
-        $fields[] = [
-            'type' => 'endwrap',
-        ];
-        $form['sets']['filters'] = [
-            'legend' => \ForkBB\__('User search subhead'),
-            'fields' => $fields,
-        ];
-
-        $fields = [];
-        $fields['between5'] = [
-            'class' => 'between',
-            'type'  => 'wrap',
-        ];
-        $fields['order_by'] = [
-            'class'   => 'bstart',
-            'type'    => 'select',
-            'options' => [
-                'username'   => \ForkBB\__('Order by username'),
-                'email'      => \ForkBB\__('Order by e-mail'),
-                'num_posts'  => \ForkBB\__('Order by posts'),
-                'last_post'  => \ForkBB\__('Order by last post'),
-                'last_visit' => \ForkBB\__('Order by last visit'),
-                'registered' => \ForkBB\__('Order by registered'),
-            ],
-            'value'   => isset($data['order_by']) ? $data['order_by'] : 'registered',
-            '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',
-        ];
-        $fields['user_group'] = [
-            'type'    => 'select',
-            'options' => $this->groups(),
-            'value'   => isset($data['user_group']) ? $data['user_group'] : -1,
-            'caption' => \ForkBB\__('User group label'),
-        ];
-
-        $form['sets']['sorting'] = [
-            'legend' => \ForkBB\__('Search results legend'),
-            'fields' => $fields,
-        ];
-
-        return $form;
-    }
-
-    /**
-     * Создает массив данных для формы поиска по IP
-     *
-     * @param array $data
-     *
-     * @return array
-     */
-    protected function formIP(array $data)
-    {
-        $form = [
-            'action' => $this->c->Router->link('AdminUsers'),
-            'hidden' => [
-                'token' => $this->c->Csrf->create('AdminUsers'),
-            ],
-            'sets'   => [],
-            'btns'   => [
-                'find' => [
-                    'type'      => 'submit',
-                    'value'     => \ForkBB\__('Find IP address'),
-                    'accesskey' => 'f',
-                ],
-            ],
-        ];
-        $fields = [];
-        $fields['ip'] = [
-            'type'      => 'text',
-            'maxlength' => 49,
-            'caption'   => \ForkBB\__('IP address label'),
-            'value'     => isset($data['ip']) ? $data['ip'] : null,
-            'required'  => true,
-        ];
-        $form['sets']['ip'] = [
-            'legend' => \ForkBB\__('IP search subhead'),
-            'fields' => $fields,
-        ];
-
-        return $form;
-    }
-
-    /**
-     * Создает массив данных для формы найденных по фильтру пользователей
-     *
-     * @param array $users
-     * @param int $number
-     *
-     * @return array
-     */
-    protected function formUsers(array $users, $number)
-    {
-        $form = [
-            'action' => $this->c->Router->link(''),
-            'hidden' => [
-                'token' => $this->c->Csrf->create(''),
-            ],
-            'sets'   => [],
-            'btns'   => [
-                'find1' => [
-                    'type'      => 'submit',
-                    'value'     => \ForkBB\__('???'),
-                    'accesskey' => 's',
-                ],
-            ],
-        ];
-
-        \array_unshift($users, $this->c->users->create(['id' => -1]));
-
-        foreach ($users as $user) {
-            if (\is_string($user)) {
-                $user = $this->c->users->create(['id' => 1, 'username' => $user]);
-            }
-
-            $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'    => $user->isGuest ? 'str' : 'link',
-                'caption' => \ForkBB\__('Results username head'),
-                'value'   => $user->username,
-                'href'    => $user->link,
-            ];
-            $fields["l{$number}-email"] = [
-                'class'   => $user->isGuest ? ['result', 'email', 'no-data'] : ['result', 'email'],
-                'type'    => $user->isGuest ? 'str' : 'link',
-                'caption' => \ForkBB\__('Results e-mail head'),
-                'value'   => $user->isGuest ? '' : $user->email,
-                'href'    => $user->isGuest ? '' : 'mailto:' . $user->email,
-            ];
-            $fields[] = [
-                'type' => 'endwrap',
-            ];
-            $fields["l{$number}-title"] = [
-                'class'   => ['result', 'title'],
-                'type'    => 'str',
-                'caption' => \ForkBB\__('Results title head'),
-                'value'   => -1 === $user->id ? null : $user->title(),
-            ];
-            $fields["l{$number}-posts"] = [
-                'class'   => $user->isGuest ? ['result', 'posts', 'no-data'] : ['result', 'posts'],
-                'type'    => $user->num_posts ? 'link' : 'str',
-                'caption' => \ForkBB\__('Results posts head'),
-                'value'   => $user->num_posts ? \ForkBB\num($user->num_posts) : null,
-                'href'    => $this->c->Router->link('SearchAction', ['action' => 'posts', 'uid' => $user->id]),
-                'title'   => \ForkBB\__('Results show posts link'),
-            ];
-            $fields["l{$number}-note"] = [
-                'class'   => '' === \trim($user->admin_note) ? ['result', 'note', 'no-data'] : ['result', 'note'],
-                'type'    => 'str',
-                'caption' => \ForkBB\__('Примечание админа'),
-                'value'   => $user->admin_note,
-            ];
-
-            if ($this->user->isAdmin) {
-                $fields["l{$number}-view-ip"] = [
-                    'class'   => $user->isGuest ? ['result', 'view-ip', 'no-data'] : ['result', 'view-ip'],
-                    'type'    => $user->isGuest || ! $user->num_posts ? 'str' : 'link',
-                    'caption' => \ForkBB\__('Results action head'),
-                    'value'   => $user->isGuest ? null : \ForkBB\__('Results view IP link'),
-                    'href'    => $this->c->Router->link('AdminUserStat', ['id' => $user->id]),
-                ];
-            }
-
-            $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;
-    }
-
-    /**
-     * Создает массив данных для формы статистики пользователя по ip
-     *
-     * @param array $stat
-     * @param int $number
-     *
-     * @return array
-     */
-    protected function formStat(array $stat, $number)
-    {
-        $form = [
-            'action' => null,
-            'hidden' => null,
-            'sets'   => [],
-            'btns'   => null,
-        ];
-
-        \array_unshift($stat, ['last_used' => null, 'used_times' => null]);
-        $flag = false;
-
-        foreach ($stat as $ip => $data) {
-            $fields = [];
-
-            $fields["l{$number}-ip"] = [
-                'class'   => ['result', 'ip'],
-                'type'    => $flag ? 'link' : 'str',
-                'caption' => \ForkBB\__('Results IP address head'),
-                'value'   => $flag ? $ip : null,
-                'href'    => $flag ? $this->c->Router->link('AdminHost', ['ip' => $ip]) : null,
-            ];
-            $fields["l{$number}-last-used"] = [
-                'class'   => ['result', 'last-used'],
-                'type'    => 'str',
-                'caption' => \ForkBB\__('Results last used head'),
-                'value'   => $flag ? \ForkBB\dt($data['last_used']) : null,
-            ];
-            $fields["l{$number}-used-times"] = [
-                'class'   => ['result', 'used-times'],
-                'type'    => 'str',
-                'caption' => \ForkBB\__('Results times found head'),
-                'value'   => $flag ? \ForkBB\num($data['used_times']) : null,
-            ];
-            $fields["l{$number}-action"] = [
-                'class'   => ['result', 'action'],
-                'type'    => $flag ? 'link' : 'str',
-                'caption' => \ForkBB\__('Results action head'),
-                'value'   => $flag ? \ForkBB\__('Results find more link') : null,
-                'href'    => $flag ? $this->c->Router->link('AdminUsersIP', ['ip' => $ip]) : null,
-            ];
-
-            $form['sets']["l{$number}"] = [
-                'class'  => ['result', 'stat'],
-                'legend' => $flag ? $number : null,
-                'fields' => $fields,
-            ];
-
-            ++$number;
-            $flag = true;
+        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 $form;
+        return $data;
     }
     }
 }
 }

+ 257 - 0
app/Models/Pages/Admin/Users/Result.php

@@ -0,0 +1,257 @@
+<?php
+
+namespace ForkBB\Models\Pages\Admin\Users;
+
+use ForkBB\Core\Validator;
+use ForkBB\Models\Pages\Admin\Users;
+
+class Result extends Users
+{
+    /**
+     * Подготавливает данные для шаблона найденных пользователей
+     *
+     * @param array $args
+     * @param string $method
+     *
+     * @return Page
+     */
+    public function view(array $args, $method)
+    {
+        $data = $this->decodeData($args['data']);
+        if (false === $data) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        if (isset($data['ip'])) {
+            if (! $this->user->isAdmin) {
+                return $this->c->Message->message('Bad request');
+            }
+
+            $ids = $this->forIP($data['ip']);
+            $crName = $data['ip'];
+        } else {
+            $ids = $this->forFilter($data);
+            $crName = \ForkBB\__('Results head');
+        }
+
+        $number = \count($ids);
+        if (0 == $number) {
+            $view = $this->c->AdminUsers;
+            $view->fIswev = ['i', \ForkBB\__('No users found')];
+
+            return $view->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;
+        $ids      = \array_slice($ids, $startNum, $this->c->config->o_disp_users);
+        $userList = $this->c->users->load($ids);
+
+        $this->nameTpl    = 'admin/users_result';
+        $this->aIndex     = 'users';
+        $this->mainSuffix = '-one-column';
+        $this->aCrumbs[]  = [$this->c->Router->link('AdminUsersResult', ['data' => $args['data']]), $crName];
+        $this->formResult = $this->form($userList, $startNum);
+        $this->pagination = $this->c->Func->paginate($pages, $page, 'AdminUsersResult', ['data' => $args['data']]);
+
+        return $this;
+    }
+
+    /**
+     * Возвращает список id пользователей по ip
+     *
+     * @param string $ip
+     *
+     * @return array
+     */
+    protected function forIP($ip)
+    {
+        $fromPosts = $this->c->posts->userInfoFromIP($ip);
+        $ids       = $this->c->users->filter([
+            'registration_ip' => ['=', $ip],
+        ]);
+        $ids       = \array_flip($ids);
+
+        foreach ($fromPosts as $val) {
+            if (isset($ids[$val])) {
+                unset($ids[$val]);
+            }
+        }
+
+        $ids = \array_flip($ids);
+        return \array_merge($fromPosts, $ids);
+    }
+
+    /**
+     * Возвращает список id пользователей по фильтру
+     *
+     * @param array $data
+     *
+     * @return array
+     */
+    protected function forFilter(array $data)
+    {
+        $order = [
+            $data['order_by'] => $data['direction'],
+        ];
+        $filters = [];
+
+        if ($data['user_group'] > -1) {
+            $filters['group_id'] = ['=', $data['user_group']];
+        }
+
+        foreach ($data as $field => $value) {
+            if ('order_by' === $field || 'direction' === $field || 'user_group' === $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->users->filter($filters, $order);
+    }
+
+    /**
+     * Создает массив данных для формы найденных по фильтру пользователей
+     *
+     * @param array $users
+     * @param int $number
+     *
+     * @return array
+     */
+    protected function form(array $users, $number)
+    {
+        $form = [
+            'action' => $this->c->Router->link(''),
+            'hidden' => [
+                'token' => $this->c->Csrf->create(''),
+            ],
+            'sets'   => [],
+            'btns'   => [
+                'ban' => [
+                    'type'      => 'submit',
+                    'value'     => \ForkBB\__('Ban'),
+                    'accesskey' => null,
+                ],
+                'delete' => [
+                    'type'      => 'submit',
+                    'value'     => \ForkBB\__('Delete'),
+                    'accesskey' => null,
+                ],
+                'change_group' => [
+                    'type'      => 'submit',
+                    'value'     => \ForkBB\__('Change group'),
+                    'accesskey' => null,
+                ],
+            ],
+        ];
+
+        \array_unshift($users, $this->c->users->create(['id' => -1]));
+
+        foreach ($users as $user) {
+            if (\is_string($user)) {
+                $user = $this->c->users->create(['id' => 1, 'username' => $user]);
+            }
+
+            $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'    => $user->isGuest ? 'str' : 'link',
+                'caption' => \ForkBB\__('Results username head'),
+                'value'   => $user->username,
+                'href'    => $user->link,
+            ];
+            $fields["l{$number}-email"] = [
+                'class'   => $user->isGuest ? ['result', 'email', 'no-data'] : ['result', 'email'],
+                'type'    => $user->isGuest ? 'str' : 'link',
+                'caption' => \ForkBB\__('Results e-mail head'),
+                'value'   => $user->isGuest ? '' : $user->email,
+                'href'    => $user->isGuest ? '' : 'mailto:' . $user->email,
+            ];
+            $fields[] = [
+                'type' => 'endwrap',
+            ];
+            $fields["l{$number}-title"] = [
+                'class'   => ['result', 'title'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results title head'),
+                'value'   => -1 === $user->id ? null : $user->title(),
+            ];
+            $fields["l{$number}-posts"] = [
+                'class'   => $user->isGuest ? ['result', 'posts', 'no-data'] : ['result', 'posts'],
+                'type'    => $user->num_posts ? 'link' : 'str',
+                'caption' => \ForkBB\__('Results posts head'),
+                'value'   => $user->num_posts ? \ForkBB\num($user->num_posts) : null,
+                'href'    => $this->c->Router->link('SearchAction', ['action' => 'posts', 'uid' => $user->id]),
+                'title'   => \ForkBB\__('Results show posts link'),
+            ];
+            $fields["l{$number}-note"] = [
+                'class'   => '' === \trim($user->admin_note) ? ['result', 'note', 'no-data'] : ['result', 'note'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Примечание админа'),
+                'value'   => $user->admin_note,
+            ];
+
+            if ($this->user->isAdmin) {
+                $fields["l{$number}-view-ip"] = [
+                    'class'   => $user->isGuest ? ['result', 'view-ip', 'no-data'] : ['result', 'view-ip'],
+                    'type'    => $user->isGuest || ! $user->num_posts ? 'str' : 'link',
+                    'caption' => \ForkBB\__('Results action head'),
+                    'value'   => $user->isGuest ? null : \ForkBB\__('Results view IP link'),
+                    'href'    => $this->c->Router->link('AdminUserStat', ['id' => $user->id]),
+                ];
+            }
+
+            $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;
+    }
+}

+ 111 - 0
app/Models/Pages/Admin/Users/Stat.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace ForkBB\Models\Pages\Admin\Users;
+
+use ForkBB\Core\Validator;
+use ForkBB\Models\Pages\Admin\Users;
+
+class Stat extends Users
+{
+    /**
+     * Подготавливает данные для шаблона ip статистики для пользователя
+     *
+     * @param array $args
+     * @param string $method
+     *
+     * @return Page
+     */
+    public function view(array $args, $method)
+    {
+        $stat   = $this->c->posts->userStat($args['id']);
+        $number = \count($stat);
+
+        $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;
+        $stat     = \array_slice($stat, $startNum, $this->c->config->o_disp_users);
+
+        $user = $this->c->users->load((int) $args['id']);
+
+        if (0 == $number) {
+            $this->fIswev = ['i', \ForkBB\__('Results no posts found')];
+        }
+
+        $this->nameTpl    = 'admin/users_result';
+        $this->aIndex     = 'users';
+        $this->mainSuffix = '-one-column';
+        $this->aCrumbs[]  = [$this->c->Router->link('AdminUserStat', ['id' => $args['id']]), $user->username];
+        $this->formResult = $this->form($stat, $startNum);
+        $this->pagination = $this->c->Func->paginate($pages, $page, 'AdminUserStat', ['id' => $args['id']]);
+
+        return $this;
+    }
+
+    /**
+     * Создает массив данных для формы статистики пользователя по ip
+     *
+     * @param array $stat
+     * @param int $number
+     *
+     * @return array
+     */
+    protected function form(array $stat, $number)
+    {
+        $form = [
+            'action' => null,
+            'hidden' => null,
+            'sets'   => [],
+            'btns'   => null,
+        ];
+
+        \array_unshift($stat, ['last_used' => null, 'used_times' => null]);
+        $flag = false;
+
+        foreach ($stat as $ip => $data) {
+            $fields = [];
+
+            $fields["l{$number}-ip"] = [
+                'class'   => ['result', 'ip'],
+                'type'    => $flag ? 'link' : 'str',
+                'caption' => \ForkBB\__('Results IP address head'),
+                'value'   => $flag ? $ip : null,
+                'href'    => $flag ? $this->c->Router->link('AdminHost', ['ip' => $ip]) : null,
+            ];
+            $fields["l{$number}-last-used"] = [
+                'class'   => ['result', 'last-used'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results last used head'),
+                'value'   => $flag ? \ForkBB\dt($data['last_used']) : null,
+            ];
+            $fields["l{$number}-used-times"] = [
+                'class'   => ['result', 'used-times'],
+                'type'    => 'str',
+                'caption' => \ForkBB\__('Results times found head'),
+                'value'   => $flag ? \ForkBB\num($data['used_times']) : null,
+            ];
+            $fields["l{$number}-action"] = [
+                'class'   => ['result', 'action'],
+                'type'    => $flag ? 'link' : 'str',
+                'caption' => \ForkBB\__('Results action head'),
+                'value'   => $flag ? \ForkBB\__('Results find more link') : null,
+                'href'    => $flag ? $this->c->Router->link('AdminUsersResult', ['data' => $this->encodeData($ip)]) : null,
+            ];
+
+            $form['sets']["l{$number}"] = [
+                'class'  => ['result', 'stat'],
+                'legend' => $flag ? $number : null,
+                'fields' => $fields,
+            ];
+
+            ++$number;
+            $flag = true;
+        }
+
+        return $form;
+    }
+}

+ 394 - 0
app/Models/Pages/Admin/Users/View.php

@@ -0,0 +1,394 @@
+<?php
+
+namespace ForkBB\Models\Pages\Admin\Users;
+
+use ForkBB\Core\Validator;
+use ForkBB\Models\Pages\Admin\Users;
+
+class View extends Users
+{
+    /**
+     * Генерирует список доступных групп пользователей
+     *
+     * @param bool $onlyKeys
+     *
+     * @return array
+     */
+    protected function groups($onlyKeys = false)
+    {
+        $groups = [
+            -1 => \ForkBB\__('All groups'),
+            0  => \ForkBB\__('Unverified users'),
+        ];
+
+        foreach ($this->c->groups->getList() as $group) {
+            if (! $group->groupGuest) {
+                $groups[$group->g_id] = $group->g_title;
+            }
+        }
+
+        return $onlyKeys ? \array_keys($groups) : $groups;
+    }
+
+    /**
+     * Подготавливает данные для шаблона поиска пользователей
+     *
+     * @param array $args
+     * @param string $method
+     * @param array $data
+     *
+     * @return Page
+     */
+    public function view(array $args, $method, array $data = [])
+    {
+        if ('POST' === $method) {
+            $v = $this->c->Validator->reset()
+                ->addRules([
+                    'token' => 'token:AdminUsers',
+                    'ip'    => 'required',
+                ]);
+
+            if ($v->validation($_POST)) {
+                $ip = \filter_var($v->ip, \FILTER_VALIDATE_IP);
+
+                if (false === $ip) {
+                    $this->fIswev = ['v', \ForkBB\__('Bad IP message')];
+                    $data         = $v->getData();
+                } else {
+                    return $this->c->Redirect->page('AdminUsersResult', ['data' => $this->encodeData($ip)]);
+                }
+            } else {
+                $v = $this->c->Validator->reset()
+                    ->addValidators([
+                    ])->addRules([
+                        'token'           => 'token:AdminUsers',
+                        'username'        => 'string|max:25',
+                        'email'           => 'string|max:80',
+                        'title'           => 'string|max:50',
+                        'realname'        => 'string|max:40',
+                        'gender'          => 'integer|in:0,1,2',
+                        'url'             => 'string|max:100',
+                        'location'        => 'string|max:30',
+                        'signature'       => 'string|max:512',
+                        'admin_note'      => 'string|max:30',
+                        'num_posts_1'     => 'integer|min:0|max:9999999999',
+                        'num_posts_2'     => 'integer|min:0|max:9999999999',
+                        'last_post_1'     => 'date',
+                        'last_post_2'     => 'date',
+                        'last_visit_1'    => 'date',
+                        'last_visit_2'    => 'date',
+                        'registered_1'    => 'date',
+                        'registered_2'    => 'date',
+                        'order_by'        => 'required|string|in:username,email,num_posts,last_post,last_visit,registered',
+                        'direction'       => 'required|string|in:ASC,DESC',
+                        'user_group'      => 'required|integer|in:' . \implode(',', $this->groups(true)),
+                    ])->addAliases([
+                        'username'        => 'Username label',
+                        'email'           => 'E-mail address label',
+                        'title'           => 'Title label',
+                        'realname'        => 'Real name label',
+                        'gender'          => 'Gender label',
+                        'url'             => 'Website label',
+                        'location'        => 'Location label',
+                        'signature'       => 'Signature label',
+                        'admin_note'      => 'Admin note label',
+                        'num_posts_1'     => 'Posts label',
+                        'num_posts_2'     => 'Posts label',
+                        'last_post_1'     => 'Last post label',
+                        'last_post_2'     => 'Last post label',
+                        'last_visit_1'    => 'Last visit label',
+                        'last_visit_2'    => 'Last visit label',
+                        'registered_1'    => 'Registered label',
+                        'registered_2'    => 'Registered label',
+                        'order_by'        => 'Order by label',
+#                        'direction'       => ,
+                        'user_group'      => 'User group label',
+                    ])->addArguments([
+                    ])->addMessages([
+                    ]);
+
+                if ($v->validation($_POST)) {
+                    return $this->c->Redirect->page('AdminUsersResult', ['data' => $this->encodeData($v->getData())]);
+                }
+
+                $this->fIswev = $v->getErrors();
+                $data         = $v->getData();
+            }
+        }
+
+        $this->nameTpl    = 'admin/users';
+        $this->aIndex     = 'users';
+        $this->formSearch = $this->form($data);
+
+        if ($this->user->isAdmin) {
+            $this->formIP = $this->formIP($data);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Создает массив данных для формы поиска
+     *
+     * @param array $data
+     *
+     * @return array
+     */
+    protected function form(array $data)
+    {
+        $form = [
+            'action' => $this->c->Router->link('AdminUsers'),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('AdminUsers'),
+            ],
+            'sets'   => [],
+            'btns'   => [
+                'search' => [
+                    'type'      => 'submit',
+                    'value'     => \ForkBB\__('Submit search'),
+                    'accesskey' => 's',
+                ],
+            ],
+        ];
+        $form['sets']['search-info'] = [
+            'info' => [
+                'info1' => [
+                    'type'  => '', //????
+                    'value' => \ForkBB\__('User search info'),
+                ],
+            ],
+        ];
+        $fields = [];
+        $fields['username'] = [
+            'type'      => 'text',
+            'maxlength' => 25,
+            'caption'   => \ForkBB\__('Username label'),
+            'value'     => isset($data['username']) ? $data['username'] : null,
+        ];
+        $fields['email'] = [
+            'type'      => 'text',
+            'maxlength' => 80,
+            'caption'   => \ForkBB\__('E-mail address label'),
+            'value'     => isset($data['email']) ? $data['email'] : null,
+        ];
+        $fields['title'] = [
+            'type'      => 'text',
+            'maxlength' => 50,
+            'caption'   => \ForkBB\__('Title label'),
+            'value'     => isset($data['title']) ? $data['title'] : null,
+        ];
+        $fields['realname'] = [
+            'type'      => 'text',
+            'maxlength' => 40,
+            'caption'   => \ForkBB\__('Real name label'),
+            'value'     => isset($data['realname']) ? $data['realname'] : null,
+        ];
+        $genders = [
+            0 => \ForkBB\__('Do not display'),
+            1 => \ForkBB\__('Male'),
+            2 => \ForkBB\__('Female'),
+        ];
+        $fields['gender'] = [
+#            'class'   => 'block',
+            'type'    => 'radio',
+            'value'   => isset($data['gender']) ? $data['gender'] : -1,
+            'values'  => $genders,
+            'caption' => \ForkBB\__('Gender label'),
+        ];
+        $fields['url'] = [
+            'id'        => 'website',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'caption'   => \ForkBB\__('Website label'),
+            'value'     => isset($data['url']) ? $data['url'] : null,
+        ];
+        $fields['location'] = [
+            'type'      => 'text',
+            'maxlength' => 30,
+            'caption'   => \ForkBB\__('Location label'),
+            'value'     => isset($data['location']) ? $data['location'] : null,
+        ];
+        $fields['signature'] = [
+            'type'      => 'text',
+            'maxlength' => 512,
+            'caption'   => \ForkBB\__('Signature label'),
+            'value'     => isset($data['signature']) ? $data['signature'] : null,
+        ];
+        $fields['admin_note'] = [
+            'type'      => 'text',
+            'maxlength' => 30,
+            'caption'   => \ForkBB\__('Admin note label'),
+            'value'     => isset($data['admin_note']) ? $data['admin_note'] : null,
+        ];
+        $fields['between1'] = [
+            'class' => 'between',
+            'type'  => 'wrap',
+        ];
+        $fields['num_posts_1'] = [
+            'type'    => 'number',
+            'class'   => 'bstart',
+            'min'     => 0,
+            'max'     => 9999999999,
+            'value'   => isset($data['num_posts_1']) ? $data['num_posts_1'] : null,
+            'caption' => \ForkBB\__('Posts label'),
+        ];
+        $fields['num_posts_2'] = [
+            'type'    => 'number',
+            'class'   => 'bend',
+            'min'     => 0,
+            'max'     => 9999999999,
+            'value'   => isset($data['num_posts_2']) ? $data['num_posts_2'] : null,
+        ];
+        $fields[] = [
+            'type' => 'endwrap',
+        ];
+        $fields['between2'] = [
+            'class' => 'between',
+            'type'  => 'wrap',
+        ];
+        $fields['last_post_1'] = [
+            'class'     => 'bstart',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['last_post_1']) ? $data['last_post_1'] : null,
+            'caption'   => \ForkBB\__('Last post label'),
+        ];
+        $fields['last_post_2'] = [
+            'class'     => 'bend',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['last_post_2']) ? $data['last_post_2'] : null,
+        ];
+        $fields[] = [
+            'type' => 'endwrap',
+        ];
+        $fields['between3'] = [
+            'class' => 'between',
+            'type'  => 'wrap',
+        ];
+        $fields['last_visit_1'] = [
+            'class'     => 'bstart',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['last_visit_1']) ? $data['last_visit_1'] : null,
+            'caption'   => \ForkBB\__('Last visit label'),
+        ];
+        $fields['last_visit_2'] = [
+            'class'     => 'bend',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['last_visit_2']) ? $data['last_visit_2'] : null,
+        ];
+        $fields[] = [
+            'type' => 'endwrap',
+        ];
+        $fields['between4'] = [
+            'class' => 'between',
+            'type'  => 'wrap',
+        ];
+        $fields['registered_1'] = [
+            'class'     => 'bstart',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['registered_1']) ? $data['registered_1'] : null,
+            'caption'   => \ForkBB\__('Registered label'),
+        ];
+        $fields['registered_2'] = [
+            'class'     => 'bend',
+            'type'      => 'text',
+            'maxlength' => 100,
+            'value'     => isset($data['registered_2']) ? $data['registered_2'] : null,
+        ];
+        $fields[] = [
+            'type' => 'endwrap',
+        ];
+        $form['sets']['filters'] = [
+            'legend' => \ForkBB\__('User search subhead'),
+            'fields' => $fields,
+        ];
+
+        $fields = [];
+        $fields['between5'] = [
+            'class' => 'between',
+            'type'  => 'wrap',
+        ];
+        $fields['order_by'] = [
+            'class'   => 'bstart',
+            'type'    => 'select',
+            'options' => [
+                'username'   => \ForkBB\__('Order by username'),
+                'email'      => \ForkBB\__('Order by e-mail'),
+                'num_posts'  => \ForkBB\__('Order by posts'),
+                'last_post'  => \ForkBB\__('Order by last post'),
+                'last_visit' => \ForkBB\__('Order by last visit'),
+                'registered' => \ForkBB\__('Order by registered'),
+            ],
+            'value'   => isset($data['order_by']) ? $data['order_by'] : 'registered',
+            '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',
+        ];
+        $fields['user_group'] = [
+            'type'    => 'select',
+            'options' => $this->groups(),
+            'value'   => isset($data['user_group']) ? $data['user_group'] : -1,
+            'caption' => \ForkBB\__('User group label'),
+        ];
+
+        $form['sets']['sorting'] = [
+            'legend' => \ForkBB\__('Search results legend'),
+            'fields' => $fields,
+        ];
+
+        return $form;
+    }
+
+    /**
+     * Создает массив данных для формы поиска по IP
+     *
+     * @param array $data
+     *
+     * @return array
+     */
+    protected function formIP(array $data)
+    {
+        $form = [
+            'action' => $this->c->Router->link('AdminUsers'),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('AdminUsers'),
+            ],
+            'sets'   => [],
+            'btns'   => [
+                'find' => [
+                    'type'      => 'submit',
+                    'value'     => \ForkBB\__('Find IP address'),
+                    'accesskey' => 'f',
+                ],
+            ],
+        ];
+        $fields = [];
+        $fields['ip'] = [
+            'type'      => 'text',
+            'maxlength' => 49,
+            'caption'   => \ForkBB\__('IP address label'),
+            'value'     => isset($data['ip']) ? $data['ip'] : null,
+            'required'  => true,
+        ];
+        $form['sets']['ip'] = [
+            'legend' => \ForkBB\__('IP search subhead'),
+            'fields' => $fields,
+        ];
+
+        return $form;
+    }
+}

+ 1 - 1
app/Models/Pages/Userlist.php

@@ -85,7 +85,7 @@ class Userlist extends Page
         $ids    = $this->c->users->filter($filters, $order);
         $ids    = $this->c->users->filter($filters, $order);
         $number = \count($ids);
         $number = \count($ids);
         $page   = isset($args['page']) ? (int) $args['page'] : 1;
         $page   = isset($args['page']) ? (int) $args['page'] : 1;
-        $pages  = $number ? (int) \ceil($number / $this->c->config->o_disp_users) : 1;
+        $pages  = (int) \ceil(($number ?: 1) / $this->c->config->o_disp_users);
 
 
         if ($page > $pages) {
         if ($page > $pages) {
             return $this->c->Message->message('Bad request');
             return $this->c->Message->message('Bad request');

+ 3 - 1
app/config/main.dist.php

@@ -166,7 +166,9 @@ return [
         'AdminGroups'     => \ForkBB\Models\Pages\Admin\Groups::class,
         'AdminGroups'     => \ForkBB\Models\Pages\Admin\Groups::class,
         'AdminCensoring'  => \ForkBB\Models\Pages\Admin\Censoring::class,
         'AdminCensoring'  => \ForkBB\Models\Pages\Admin\Censoring::class,
         'AdminMaintenance' => \ForkBB\Models\Pages\Admin\Maintenance::class,
         'AdminMaintenance' => \ForkBB\Models\Pages\Admin\Maintenance::class,
-        'AdminUsers'      => \ForkBB\Models\Pages\Admin\Users::class,
+        'AdminUsers'      => \ForkBB\Models\Pages\Admin\Users\View::class,
+        'AdminUsersResult' => \ForkBB\Models\Pages\Admin\Users\Result::class,
+        'AdminUsersStat'  => \ForkBB\Models\Pages\Admin\Users\Stat::class,
         'AdminHost'       => \ForkBB\Models\Pages\Admin\Host::class,
         'AdminHost'       => \ForkBB\Models\Pages\Admin\Host::class,
 
 
         'ConfigModel'     => \ForkBB\Models\Config\Model::class,
         'ConfigModel'     => \ForkBB\Models\Config\Model::class,

+ 4 - 4
app/templates/admin/users.forkbb.php

@@ -1,17 +1,17 @@
 @extends ('layouts/admin')
 @extends ('layouts/admin')
+@if ($form = $p->formSearch)
       <section class="f-admin f-search-user-form">
       <section class="f-admin f-search-user-form">
         <h2>{!! __('User search head') !!}</h2>
         <h2>{!! __('User search head') !!}</h2>
         <div class="f-fdiv">
         <div class="f-fdiv">
-@if ($form = $p->formSearch)
   @include ('layouts/form')
   @include ('layouts/form')
-@endif
         </div>
         </div>
       </section>
       </section>
+@endif
+@if ($form = $p->formIP)
       <section class="f-admin f-search-ip-form">
       <section class="f-admin f-search-ip-form">
         <h2>{!! __('IP search head') !!}</h2>
         <h2>{!! __('IP search head') !!}</h2>
         <div class="f-fdiv">
         <div class="f-fdiv">
-@if ($form = $p->formIP)
   @include ('layouts/form')
   @include ('layouts/form')
-@endif
         </div>
         </div>
       </section>
       </section>
+@endif

+ 1 - 1
app/templates/admin/users_result.forkbb.php

@@ -27,7 +27,7 @@
         </div>
         </div>
       </div>
       </div>
 @endif
 @endif
-      <section class="f-admin f-search-user-form">
+      <section class="f-admin f-search-result-form">
         <h2>{!! __('Results head') !!}</h2>
         <h2>{!! __('Results head') !!}</h2>
         <div class="f-fdiv">
         <div class="f-fdiv">
 @if ($form = $p->formResult)
 @if ($form = $p->formResult)

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

@@ -446,7 +446,12 @@ select {
 }
 }
 
 
 .f-fdiv .f-btns {
 .f-fdiv .f-btns {
-  padding: 0.625rem 0.3125rem;
+  padding: 0.3125rem;
+}
+
+.f-fdiv .f-btns .f-btn {
+  margin: 0.3125rem 0;
+  min-width: 7rem;
 }
 }
 
 
 .f-fdiv legend {
 .f-fdiv legend {
@@ -2285,6 +2290,15 @@ select {
 /************************/
 /************************/
 /* Админка/Пользователи */
 /* Админка/Пользователи */
 /************************/
 /************************/
+.f-search-result-form .f-btns {
+  text-align: right;
+}
+
+.f-search-result-form .f-btns .f-btn {
+  display: inline;
+  width: auto;
+}
+
 .f-fs-result {
 .f-fs-result {
   display: flex;
   display: flex;
   align-items: stretch;
   align-items: stretch;