diff --git a/app/Controllers/Routing.php b/app/Controllers/Routing.php index 1742c40a..a81e5bbc 100644 --- a/app/Controllers/Routing.php +++ b/app/Controllers/Routing.php @@ -66,7 +66,8 @@ class Routing } // поиск if ($user->g_search == '1') { - $r->add('GET', '/search', 'Search:view', 'Search'); + $r->add(['GET','POST'], '/search[/{advanced:advanced}]', 'Search:view', 'Search'); + $r->add('GET', '/search/{action:last|unanswered}[/{page:[1-9]\d*}]', 'Search:action', 'SearchAction'); } // юзеры if ($user->g_view_users == '1') { diff --git a/app/Models/Config/Load.php b/app/Models/Config/Load.php index 883bedeb..efa4ddbe 100644 --- a/app/Models/Config/Load.php +++ b/app/Models/Config/Load.php @@ -3,6 +3,7 @@ namespace ForkBB\Models\Config; use ForkBB\Models\Method; +use PDO; class Load extends Method { @@ -14,7 +15,7 @@ class Load extends Method */ public function load() { - $config = $this->c->DB->query('SELECT conf_name, conf_value FROM ::config')->fetchAll(\PDO::FETCH_KEY_PAIR); + $config = $this->c->DB->query('SELECT conf_name, conf_value FROM ::config')->fetchAll(PDO::FETCH_KEY_PAIR); $this->model->setAttrs($config); $this->c->Cache->set('config', $config); return $this->model; diff --git a/app/Models/Forum/Model.php b/app/Models/Forum/Model.php index 52b815ca..64355ab2 100644 --- a/app/Models/Forum/Model.php +++ b/app/Models/Forum/Model.php @@ -186,7 +186,7 @@ class Model extends DataModel throw new RuntimeException('The model does not have the required data'); } - return $this->num_topics === 0 ? 1 : (int) ceil($this->num_topics / $this->c->user->disp_topics); + return (int) ceil(($this->num_topics ?: 1) / $this->c->user->disp_topics); } /** @@ -249,8 +249,8 @@ class Model extends DataModel ORDER BY sticky DESC, {$sortBy}, id DESC LIMIT ?i:offset, ?i:rows"; - $ids = $this->c->DB->query($sql, $vars)->fetchAll(PDO::FETCH_COLUMN); + $this->idsList = $this->c->DB->query($sql, $vars)->fetchAll(PDO::FETCH_COLUMN); - return empty($ids) ? [] : $this->c->topics->view($ids); + return empty($this->idsList) ? [] : $this->c->topics->view($this); } } diff --git a/app/Models/Pages/CrumbTrait.php b/app/Models/Pages/CrumbTrait.php index 83188710..f9acdff6 100644 --- a/app/Models/Pages/CrumbTrait.php +++ b/app/Models/Pages/CrumbTrait.php @@ -3,6 +3,7 @@ namespace ForkBB\Models\Pages; use ForkBB\Models\Model; +use ForkBB\Models\Search\Model as Search; trait CrumbTrait { @@ -20,8 +21,18 @@ trait CrumbTrait $active = true; foreach ($args as $arg) { - // Раздел или топик - if ($arg instanceof Model) { + // поиск + if ($arg instanceof Search) { + if ($arg->page > 1) { + $this->titles = $arg->name . ' ' . \ForkBB\__('Page %s', $arg->page); + } else { + $this->titles = $arg->name; + } + $crumbs[] = [$arg->link, $arg->name, $active]; + $this->titles = \ForkBB\__('Search'); + $crumbs[] = [$this->c->Router->link('Search'), \ForkBB\__('Search'), null]; + // раздел или топик + } elseif ($arg instanceof Model) { while (null !== $arg->parent && $arg->link) { if (isset($arg->forum_name)) { $name = $arg->forum_name; @@ -37,13 +48,17 @@ trait CrumbTrait $this->titles = $name; } $crumbs[] = [$arg->link, $name, $active]; - $active = null; - $arg = $arg->parent; + $active = null; + $arg = $arg->parent; } - // Строка + // ссылка + } elseif (is_array($arg)) { + $this->titles = $arg[1]; + $crumbs[] = [$arg[0], $arg[1], $active]; + // строка } else { $this->titles = (string) $arg; - $crumbs[] = [null, (string) $arg, $active]; + $crumbs[] = [null, (string) $arg, $active]; } $active = null; } diff --git a/app/Models/Pages/Edit.php b/app/Models/Pages/Edit.php index 3732be3a..692041b3 100644 --- a/app/Models/Pages/Edit.php +++ b/app/Models/Pages/Edit.php @@ -14,10 +14,10 @@ class Edit extends Page /** * Редактирование сообщения - * + * * @param array $args * @param string $method - * + * * @return Page */ public function edit(array $args, $method) @@ -64,20 +64,22 @@ class Edit extends Page $this->formTitle = $editSubject ? \ForkBB\__('Edit topic') : \ForkBB\__('Edit post'); $this->crumbs = $this->crumbs($this->formTitle, $topic); $this->form = $this->messageForm($post, 'EditPost', $args, true, $editSubject); - + return $this; } /** * Сохранение сообщения - * + * * @param Post $post * @param Validator $v - * + * * @return Page */ protected function endEdit(Post $post, Validator $v) { + $this->c->DB->beginTransaction(); + $now = time(); $user = $this->c->user; $executive = $user->isAdmin || $user->isModerator($post); @@ -139,13 +141,17 @@ class Edit extends Page $topic->parent->calcStat(); } $this->c->forums->update($topic->parent); - - // антифлуд - if ($calcPost || $calcForum) { + + // антифлуд + if ($calcPost || $calcForum) { $user->last_post = $now; //???? $this->c->users->update($user); } - + + $this->c->search->index($post, 'edit'); + + $this->c->DB->commit(); + return $this->c->Redirect ->page('ViewPost', ['id' => $post->id]) ->message('Edit redirect'); diff --git a/app/Models/Pages/Forum.php b/app/Models/Pages/Forum.php index 43d1cc2e..5d2b7705 100644 --- a/app/Models/Pages/Forum.php +++ b/app/Models/Pages/Forum.php @@ -39,8 +39,7 @@ class Forum extends Page $this->nameTpl = 'forum'; $this->onlinePos = 'forum-' . $args['id']; $this->canonical = $this->c->Router->link('Forum', ['id' => $args['id'], 'name' => $forum->forum_name, 'page' => $forum->page]); - $this->forum = $forum; - $this->forums = $forum->subforums; + $this->model = $forum; $this->topics = $forum->pageData(); $this->crumbs = $this->crumbs($forum); diff --git a/app/Models/Pages/Post.php b/app/Models/Pages/Post.php index 893f64a6..d3aeb9ee 100644 --- a/app/Models/Pages/Post.php +++ b/app/Models/Pages/Post.php @@ -16,10 +16,10 @@ class Post extends Page /** * Создание новой темы - * + * * @param array $args * @param string $method - * + * * @return Page */ public function newTopic(array $args, $method) @@ -38,7 +38,7 @@ class Post extends Page if ($v->validation($_POST) && null === $v->preview) { return $this->endPost($forum, $v); } - + $this->fIswev = $v->getErrors(); $args['_vars'] = $v->getData(); //???? @@ -54,16 +54,16 @@ class Post extends Page $this->crumbs = $this->crumbs(\ForkBB\__('Post new topic'), $forum); $this->formTitle = \ForkBB\__('Post new topic'); $this->form = $this->messageForm($forum, 'NewTopic', $args, false, true); - + return $this; } /** * Подготовка данных для шаблона создания сообщения - * + * * @param array $args * @param string $method - * + * * @return Page */ public function newReply(array $args, $method) @@ -82,10 +82,10 @@ class Post extends Page if ($v->validation($_POST) && null === $v->preview) { return $this->endPost($topic, $v); } - + $this->fIswev = $v->getErrors(); $args['_vars'] = $v->getData(); //???? - + if (null !== $v->preview && ! $v->getErrors()) { $this->previewHtml = $this->c->Parser->parseMessage(null, (bool) $v->hide_smilies); } @@ -109,37 +109,39 @@ class Post extends Page $this->crumbs = $this->crumbs(\ForkBB\__('Post a reply'), $topic); $this->formTitle = \ForkBB\__('Post a reply'); $this->form = $this->messageForm($topic, 'NewReply', $args); - + return $this; } /** * Создание темы/сообщения - * + * * @param Model $model * @param Validator $v - * + * * @return Page */ protected function endPost(Model $model, Validator $v) { + $this->c->DB->beginTransaction(); + $now = time(); $user = $this->c->user; $username = $user->isGuest ? $v->username : $user->username; $merge = false; $executive = $user->isAdmin || $user->isModerator($model); - + // подготовка к объединению/сохранению сообщения if (null === $v->subject) { $createTopic = false; $forum = $model->parent; $topic = $model; - + if (! $user->isGuest && $topic->last_poster === $username) { if ($executive) { if ($v->merge_post) { $merge = true; - } + } } else { if ($this->c->config->o_merge_timeout > 0 // ???? стоит завязать на время редактирование сообщений? && $now - $topic->last_post < $this->c->config->o_merge_timeout @@ -165,7 +167,7 @@ class Post extends Page # $topic->poll_time = ; # $topic->poll_term = ; # $topic->poll_kol = ; - + $this->c->topics->insert($topic); } @@ -184,11 +186,11 @@ class Post extends Page $merge = false; } } - + // создание нового сообщения if (! $merge) { $post = $this->c->posts->create(); - + $post->poster = $username; $post->poster_id = $this->c->user->id; $post->poster_ip = $this->c->user->ip; @@ -224,7 +226,15 @@ class Post extends Page } $user->last_post = $now; $this->c->users->update($user); - + + if ($merge) { + $this->c->search->index($lastPost, 'merge'); + } else { + $this->c->search->index($post); + } + + $this->c->DB->commit(); + return $this->c->Redirect ->page('ViewPost', ['id' => $merge ? $lastPost->id : $post->id]) ->message('Post redirect'); diff --git a/app/Models/Pages/Search.php b/app/Models/Pages/Search.php new file mode 100644 index 00000000..39c00c9b --- /dev/null +++ b/app/Models/Pages/Search.php @@ -0,0 +1,288 @@ +c->Lang->load('search'); + + $v = null; + if ('POST' === $method) { + $v = $this->c->Validator->reset() + ->addValidators([ + 'check_query' => [$this, 'vCheckQuery'], + ])->addRules([ + 'token' => 'token:Search', + 'keywords' => 'required|string:trim|max:100|check_query', + 'author' => 'absent', + 'forums' => 'absent', + 'serch_in' => 'absent', + 'sort_by' => 'absent', + 'sort_dir' => 'absent', + 'show_as' => 'absent', + ])->addArguments([ + 'token' => $args, + ])->addAliases([ + 'keywords' => 'Keyword search', + 'author' => 'Author search', + 'forums' => 'Forum search', + 'serch_in' => 'Search in', + 'sort_by' => 'Sort by', + 'sort_dir' => 'Sort order', + 'show_as' => 'Show as', + ]); + + if (isset($args['advanced'])) { + $v->addRules([ + 'author' => 'string:trim|max:25', + 'forums' => 'integer', + 'serch_in' => 'required|integer|in:0,1,2', + 'sort_by' => 'required|integer|in:0,1,2,3', + 'sort_dir' => 'required|integer|in:0,1', + 'show_as' => 'required|integer|in:0,1', + ]); + } + + if ($v->validation($_POST)) { + $this->c->search->execute([ + 'keywords' => $v->keywords, + 'author' => (string) $v->author, + 'forums' => $v->forums, + 'serch_in' => ['all', 'posts', 'topics'][(int) $v->serch_in], + 'sort_by' => ['post', 'author', 'subject', 'forum'][(int) $v->sort_by], + 'sort_dir' => ['desc', 'asc'][(int) $v->sort_dir], + 'show_as' => ['posts', 'topics'][(int) $v->show_as], + ]); + } + + $this->fIswev = $v->getErrors(); + } + + $form = [ + 'action' => $this->c->Router->link('Search', $args), + 'hidden' => [ + 'token' => $this->c->Csrf->create('Search', $args), + ], + 'sets' => [], + 'btns' => [ + 'search' => [ + 'type' => 'submit', + 'value' => \ForkBB\__('Search btn'), + 'accesskey' => 's', + ], + ], + ]; + + if (isset($args['advanced'])) { + $form['sets'][] = [ + 'fields' => [ + [ + 'type' => 'info', + 'value' => \ForkBB\__('Simple search', $this->c->Router->link('Search')), + 'html' => true, + ], + 'keywords' => [ + 'dl' => 't2', + 'type' => 'text', + 'maxlength' => 100, + 'title' => \ForkBB\__('Keyword search'), + 'value' => $v ? $v->keywords : '', + 'required' => true, + 'autofocus' => true, + ], + 'author' => [ + 'dl' => 't1', + 'type' => 'text', + 'maxlength' => 25, + 'title' => \ForkBB\__('Author search'), + 'value' => $v ? $v->author : '', + ], + [ + 'type' => 'info', + 'value' => \ForkBB\__('Search info'), + ], + ], + ]; + $form['sets'][] = [ + 'legend' => \ForkBB\__('Search in legend'), + 'fields' => [ + 'forums' => [ + 'type' => 'multiselect', + 'options' => [], + 'value' => $v ? $v->forums : null, + 'title' => \ForkBB\__('Forum search'), + ], + 'serch_in' => [ + 'type' => 'select', + 'options' => [ + 0 => \ForkBB\__('Message and subject'), + 1 => \ForkBB\__('Message only'), + 2 => \ForkBB\__('Topic only'), + ], + 'value' => $v ? $v->serch_in : 0, + 'title' => \ForkBB\__('Search in'), + ], + [ + 'type' => 'info', + 'value' => \ForkBB\__('Search in info'), + ], + [ + 'type' => 'info', + 'value' => \ForkBB\__('Search multiple forums info'), + ], + + ], + ]; + $form['sets'][] = [ + 'legend' => \ForkBB\__('Search results legend'), + 'fields' => [ + 'sort_by' => [ + 'type' => 'select', + 'options' => [ + 0 => \ForkBB\__('Sort by post time'), + 1 => \ForkBB\__('Sort by author'), + 2 => \ForkBB\__('Sort by subject'), + 3 => \ForkBB\__('Sort by forum'), + ], + 'value' => $v ? $v->sort_by : 0, + 'title' => \ForkBB\__('Sort by'), + ], + 'sort_dir' => [ + 'type' => 'radio', + 'values' => [ + 0 => \ForkBB\__('Descending'), + 1 => \ForkBB\__('Ascending'), + ], + 'value' => $v ? $v->sort_dir : 0, + 'title' => \ForkBB\__('Sort order'), + ], + 'show_as' => [ + 'type' => 'radio', + 'values' => [ + 0 => \ForkBB\__('Show as posts'), + 1 => \ForkBB\__('Show as topics'), + ], + 'value' => $v ? $v->show_as : 0, + 'title' => \ForkBB\__('Show as'), + ], + [ + 'type' => 'info', + 'value' => \ForkBB\__('Search results info'), + ], + ], + + ]; + } else { + $form['sets'][] = [ + 'fields' => [ + [ + 'type' => 'info', + 'value' => \ForkBB\__('Advanced search', $this->c->Router->link('Search', ['advanced' => 'advanced'])), + 'html' => true, + ], + 'keywords' => [ + 'type' => 'text', + 'maxlength' => 100, + 'title' => \ForkBB\__('Keyword search'), + 'value' => $v ? $v->keywords : '', + 'required' => true, + 'autofocus' => true, + ], + ], + ]; + } + + $this->fIndex = 'search'; + $this->nameTpl = 'search'; + $this->onlinePos = 'search'; + $this->canonical = $this->c->Router->link('Search'); + $this->robots = 'noindex'; + $this->form = $form; + $this->crumbs = $this->crumbs([$this->c->Router->link('Search'), \ForkBB\__('Search')]); + + return $this; + } + + /** + * Дополнительная проверка строки запроса + * + * @param Validator $v + * @param string $query + * + * @return string + */ + public function vCheckQuery(Validator $v, $query) + { + $search = $this->c->search; + + if (! $search->prepare($query)) { + $v->addError(\ForkBB\__($search->queryError, $search->queryText)); + } + + return $query; + } + + /** + * Типовые действия + * + * @param array $args + * @param string $method + * + * @throws InvalidArgumentException + * + * @return Page + */ + public function action(array $args, $method) + { + $this->c->Lang->load('search'); + + $model = $this->c->search; + $model->page = isset($args['page']) ? (int) $args['page'] : 1; + $action = $args['action']; + switch ($action) { + case 'last': + case 'unanswered': + $list = $model->actionT($action); + $model->name = \ForkBB\__('Quick search show_' . $action); + $model->linkMarker = 'SearchAction'; + $model->linkArgs = ['action' => $action]; + break; + default: + throw new InvalidArgumentException('Unknown action: ' . $action); + } + + if (false === $list) { + return $this->c->Message->message('Bad request'); + } elseif (empty($list)) { + $this->a['fIswev']['i'][] = \ForkBB\__('No hits'); + return $this->view(['advanced' => 'advanced'], 'GET'); + } + + $this->fIndex = 'search'; + $this->nameTpl = 'forum'; + $this->onlinePos = 'search'; + $this->robots = 'noindex'; + $this->model = $model; + $this->topics = $list; + $this->crumbs = $this->crumbs($model); + $this->showForum = true; + + return $this; + } +} diff --git a/app/Models/Search/ActionT.php b/app/Models/Search/ActionT.php new file mode 100644 index 00000000..62309173 --- /dev/null +++ b/app/Models/Search/ActionT.php @@ -0,0 +1,64 @@ +c->forums->get(0); + if (! $root instanceof Forum || empty($root->descendants)) { + return []; //???? + } + + switch ($action) { + case 'last': + $sql = 'SELECT t.id + FROM ::topics AS t + WHERE t.forum_id IN (?ai:forums) AND t.moved_to IS NULL + ORDER BY t.last_post DESC'; + break; + case 'unanswered': + $sql = 'SELECT t.id + FROM ::topics AS t + WHERE t.forum_id IN (?ai:forums) AND t.moved_to IS NULL AND t.num_replies=0 + ORDER BY t.last_post DESC'; + break; + default: + throw new InvalidArgumentException('Unknown action: ' . $action); + } + $vars = [ + ':forums' => array_keys($root->descendants), + ]; + $list = $this->c->DB->query($sql, $vars)->fetchAll(PDO::FETCH_COLUMN); + + $this->model->numPages = (int) ceil((count($list) ?: 1) / $this->c->user->disp_topics); + + // нет такой страницы в результате поиска + if (! $this->model->hasPage()) { + return false; + // результат пуст + } elseif (empty($list)) { + return []; + } + + $this->model->idsList = array_slice($list, ($this->model->page - 1) * $this->c->user->disp_topics, $this->c->user->disp_topics); + + return $this->c->topics->view($this->model); + } +} diff --git a/app/Models/Search/Execute.php b/app/Models/Search/Execute.php new file mode 100644 index 00000000..84b74b97 --- /dev/null +++ b/app/Models/Search/Execute.php @@ -0,0 +1,222 @@ +model->queryWords) || ! is_string($this->model->queryText)) { + throw new InvalidArgumentException('No query data'); + } + +echo '
';
+var_dump($this->model->queryText);
+
+        $this->words = [];
+        $this->stmt  = null;
+        $vars        = $this->buildSelect($options);
+
+        $ids = $this->exec($this->model->queryWords, $vars);
+
+        if ('asc' === $options['sort_dir']) {
+            asort($ids, $this->sortType);
+        } else {
+            arsort($ids, $this->sortType);
+        }
+
+var_dump($ids);
+echo '
'; + exit(); + } + + /** + * Поиск по словам рекурсивного списка + * + * @param array $words + * @param array $vars + * @param array $ids + * + * @return array + */ + protected function exec(array $words, array $vars, array $ids = []) + { + $type = 'AND'; + $count = 0; + + foreach ($words as $word) { + +var_dump($word); + + // служебное слово + if ('AND' === $word || 'OR' === $word || 'NOT' === $word) { + $type = $word; + continue; + } + + // если до сих пор ни чего не найдено и тип операции не ИЛИ, то выполнять не надо + if ($count && empty($ids) && 'OR' !== $type) { + continue; + } + + if (is_array($word) && (! isset($word['type']) || 'CJK' !== $word['type'])) { + $ids = $this->exec($word, $vars, $ids); + } else { + $CJK = false; + if (isset($word['type']) && 'CJK' === $word['type']) { + $CJK = true; + $word = $word['word']; //???? добавить * + } + + $word = str_replace(['*', '?'], ['%', '_'], $word); + + if (isset($this->words[$word])) { + $list = $this->words[$word]; + } else { + $vars[':word'] = $word; + + if (null === $this->stmt) { + $this->stmt = $this->c->DB->prepare($this->selectForIndex, $vars); + $this->stmt->execute(); + } else { + $this->stmt->execute($vars); + } + $this->words[$word] = $list = $this->stmt->fetchAll(PDO::FETCH_KEY_PAIR); + } + +var_dump($list); + if (! $count) { + $ids = $list; + } elseif ('AND' === $type) { + $ids = array_intersect_key($ids, $list); + } elseif ('OR' === $type) { + $ids += $list; + } elseif ('NOT' === $type) { + $ids = array_diff_key($ids, $list); + } + } + + ++$count; + } + + return $ids; + } + + /** + * @param array $options + * + * @return array + */ + protected function buildSelect(array $options) + { + # ["keywords"]=> string(5) "fnghj" + # ["author"] => string(0) "" + # ["forums"] => NULL + # ["serch_in"]=> string(3) "all" + # ["sort_by"] => string(4) "post" + # ["sort_dir"]=> string(4) "desc" + # ["show_as"] => string(5) "posts" + $vars = []; + $where = []; + $joinT = false; + $joinP = false; + + switch ($options['serch_in']) { + case 'posts': + $where[] = 'm.subject_match=0'; + break; + case 'topics': + $where[] = 'm.subject_match=1'; + // при поиске в заголовках результат только в виде списка тем + $options['show_as'] = 'topics'; + break; + } + + if (! empty($options['forums'])) { + $joinT = true; + $where[] = 't.forum_id IN (?ai:forums)'; + $vars[':forums'] = (array) $options['forums']; + } + + if ('topics' === $options['show_as']) { + $showTopics = true; + $joinP = true; + $selectF = 'p.topic_id'; + } else { + $showTopics = false; + $selectF = 'm.post_id'; + } + + //???? нужен индекс по авторам сообщений + //???? что делать с подчеркиванием в именах? + if ('' != $options['author']) { + $joinP = true; + $vars[':author'] = str_replace(['*', '?'], ['%', '_'], $options['author']); + $where[] = 'p.poster LIKE ?s:author'; + } + + switch ($options['sort_by']) { + case 'author': + if ($showTopics) { + $sortBy = 't.poster'; + $joinT = true; + } else { + $sortBy = 'p.poster'; + $joinP = true; + } + $this->sortType = SORT_STRING; + break; + case 'subject': + $sortBy = 't.subject'; + $joinT = true; + $this->sortType = SORT_STRING; + break; + case 'forum': + $sortBy = 't.forum_id'; + $joinT = true; + $this->sortType = SORT_NUMERIC; + break; + default: + if ($showTopics) { + $sortBy = 't.last_post'; + $joinT = true; + } else { + $sortBy = 'm.post_id'; + } + $this->sortType = SORT_NUMERIC; + break; + } + + $joinP = $joinP || $joinT ? 'INNER JOIN ::posts AS p ON p.id=m.post_id ' : ''; + $joinT = $joinT ? 'INNER JOIN ::topics AS t ON t.id=p.topic_id ' : ''; + $where = empty($where) ? '' : ' AND ' . implode(' AND ', $where); + + $this->selectForIndex = "SELECT {$selectF}, {$sortBy} FROM ::search_words AS w " . + 'INNER JOIN ::search_matches AS m ON m.word_id=w.id ' . + $joinP . + $joinT . + 'WHERE w.word LIKE ?s:word' . $where; + + return $vars; + } +} diff --git a/app/Models/Search/Index.php b/app/Models/Search/Index.php new file mode 100644 index 00000000..0ec4ce0f --- /dev/null +++ b/app/Models/Search/Index.php @@ -0,0 +1,150 @@ +words(mb_strtolower($this->c->Parser->getText(), 'UTF-8')); + $subWords = $post->id === $post->parent->first_post_id + ? $this->words(mb_strtolower($post->parent->subject, 'UTF-8')) + : []; + + if ('add' !== $mode) { + $vars = [ + ':pid' => $post->id, + ]; + $sql = 'SELECT w.id, w.word, m.subject_match + FROM ::search_words AS w + INNER JOIN ::search_matches AS m ON w.id=m.word_id + WHERE m.post_id=?i:pid'; + $stmt = $this->c->DB->query($sql, $vars); + + $mesCurWords = []; + $subCurWords = []; + while ($row = $stmt->fetch()) { + if ($row['subject_match']) { + $subCurWords[$row['word']] = $row['id']; + } else { + $mesCurWords[$row['word']] = $row['id']; + } + } + } + + $words = []; + if ('edit' === $mode) { + $words['add']['p'] = array_diff($mesWords, array_keys($mesCurWords)); + $words['add']['s'] = array_diff($subWords, array_keys($subCurWords)); + $words['del']['p'] = array_diff_key($mesCurWords, array_flip($mesWords)); + $words['del']['s'] = array_diff_key($subCurWords, array_flip($subWords)); + } elseif ('merge' === $mode) { + $words['add']['p'] = array_diff($mesWords, array_keys($mesCurWords)); + $words['add']['s'] = array_diff($subWords, array_keys($subCurWords)); + $words['del']['p'] = []; + $words['del']['s'] = []; + } else { + $words['add']['p'] = $mesWords; + $words['add']['s'] = $subWords; + $words['del']['p'] = []; + $words['del']['s'] = []; + } + + if (empty($words['add']['s'])) { + $allWords = $words['add']['p']; + } else { + $allWords = array_unique(array_merge($words['add']['p'], $words['add']['s'])); + } + if (! empty($allWords)) { + $vars = [ + ':words' => $allWords, + ]; + $sql = 'SELECT word + FROM ::search_words + WHERE word IN(?as:words)'; + $oldWords = $this->c->DB->query($sql, $vars)->fetchAll(PDO::FETCH_COLUMN); + $newWords = array_diff($allWords, $oldWords); + + if (! empty($newWords)) { + $sql = 'INSERT INTO ::search_words (word) VALUES(?s:word)'; + $stmt = null; + foreach ($newWords as $word) { + if (null === $stmt) { + $stmt = $this->c->DB->prepare($sql, [':word' => $word]); + $stmt->execute(); + } else { + $stmt->execute([':word' => $word]); + } + } + } + } + + foreach ($words['del'] as $key => $list) { + if (empty($list)) { + continue; + } + + $vars = [ + ':pid' => $post->id, + ':subj' => 's' === $key ? 1 : 0, + ':ids' => $list, + ]; + $sql = 'DELETE FROM ::search_matches + WHERE word_id IN(?ai:ids) AND post_id=?i:pid AND subject_match=?i:subj'; + $this->c->DB->exec($sql, $vars); + } + + foreach ($words['add'] as $key => $list) + { + if (empty($list)) { + continue; + } + + $vars = [ + ':pid' => $post->id, + ':subj' => 's' === $key ? 1 : 0, + ':words' => $list, + ]; + $sql = 'INSERT INTO ::search_matches (post_id, word_id, subject_match) + SELECT ?i:pid, id, ?i:subj + FROM ::search_words + WHERE word IN(?as:words)'; + $this->c->DB->exec($sql, $vars); + } + } + + /** + * Получение слов из текста для построения поискового индекса + * + * @param string $text + * + * @return array + */ + protected function words($text) + { + $text = $this->model->cleanText($text, true); + + $words = []; + foreach (array_unique(explode(' ', $text)) as $word) { + $word = $this->model->word($word, true); + if (null !== $word) { + $words[] = $word; + } + } + return $words; + } +} diff --git a/app/Models/Search/Model.php b/app/Models/Search/Model.php new file mode 100644 index 00000000..e3edf5ae --- /dev/null +++ b/app/Models/Search/Model.php @@ -0,0 +1,144 @@ +c->Router->link($this->linkMarker, $this->linkArgs); + } + + /** + * Массив страниц результата поиска + * + * @return array + */ + protected function getpagination() + { + return $this->c->Func->paginate($this->numPages, $this->page, $this->linkMarker, $this->linkArgs); + } + + /** + * Статус наличия установленной страницы в результате поиска + * + * @return bool + */ + public function hasPage() + { + return $this->page > 0 && $this->page <= $this->numPages; + } + + /** + * Очистка текста для дальнейшего разделения на слова + * + * @param string $text + * @param bool $indexing + * + * @return string + */ + public function cleanText($text, $indexing = false) + { + $text = str_replace(['`', '’', 'ё'], ['\'', '\'', 'е'], $text); + // четыре одинаковых буквы в одну + $text = preg_replace('%(\p{L})\1{3,}%u', '\1', $text); + // удаление ' и - вне слов + $text = preg_replace('%((?c->stopwords->list[$word])) { + return null; + } + + if ($this->isCJKWord($word)) { + return $indexing ? null : $word; + } + + $len = mb_strlen(trim($word, '?*'), 'UTF-8'); + + if ($len < 3) { + return null; + } + + if ($len > 20) { + $word = mb_substr($word, 0, 20, 'UTF-8'); + } + + return $word; + } + + /** + * Проверка слова на язык CJK + * + * @param string $word + * + * @return bool + */ + public function isCJKWord($word) + { + return preg_match('%' . self::CJK_REGEX . '%u', $word) ? true : false; //????? + } +} diff --git a/app/Models/Search/Prepare.php b/app/Models/Search/Prepare.php new file mode 100644 index 00000000..bc301fce --- /dev/null +++ b/app/Models/Search/Prepare.php @@ -0,0 +1,210 @@ +model->queryError = 'Odd number of quotes: \'%s\''; + $this->model->queryWords = []; + $this->model->queryText = $query; + return false; + } + + $error = null; + $this->model->queryWords = null; + $this->model->queryText = null; + + $stack = []; + $quotes = false; + $words = []; + $keyword = true; + $count = 0; + + foreach (preg_split('%"%', $query) as $subQuery) { + // подстрока внутри кавычек + if ($quotes) { + $subQuery = mb_strtolower(trim($subQuery), 'UTF-8'); + // не стоп-слово и минимальная длина удовлетворяет условию + if (null !== $this->model->word($subQuery)) { + // подстрока является словом и нет символов CJK языков + if (false === strpos($subQuery, ' ') + && ! $this->model->isCJKWord($subQuery) + && $this->model->cleanText($subQuery) === $subQuery + ) { + $words[] = $subQuery; + // это не слово или есть символы CJK языков + // искать придется через LIKE по тексту сообщений + } else { + $words[] = ['type' => 'CJK', 'word' => $subQuery]; + } + $keyword = false; + ++$count; + } + $quotes = false; + continue; + } + + // действуют управляющие слова + foreach ( + preg_split( + '%\s*(\b(?:AND|OR|NOT)\b|(?model->cleanText($cur); //???? + $temp = []; + $countT = 0; + foreach (explode(' ', $cur) as $word) { + $word = $this->model->word($word); + if (null === $word) { + continue; + } + if (! empty($temp)) { + $temp[] = 'AND'; + } + if ($this->model->isCJKWord($word)) { + $temp[] = ['type' => 'CJK', 'word' => $word]; + } elseif (rtrim($word, '?*') === $word) { + $temp[] = $word . '*'; //???? + } else { + $temp[] = $word; + } + ++$countT; + } + if ($countT) { + if (! $keyword) { + $words[] = 'AND'; + } + if (1 === $countT || 'AND' === end($words)) { + $words = array_merge($words, $temp); + $count += $countT; + } else { + $words[] = $temp; + ++$count; + } + $keyword = false; + } + break; + } + } + $quotes = true; + } + + if (! $count) { + $error = 'There is no word for search: "%s"'; + } else if ($keyword) { + $error = 'Syntactic word at the end of the search query: "%s"'; + } elseif (! empty($stack)) { + $error = 'The order of brackets is broken: \'%s\''; + } + + $this->model->queryError = $error; + $this->model->queryWords = $words; + $this->model->queryText = $this->queryText($words); + + return null === $error; + } + + /** + * Замена в строке по массиву шаблонов + * + * @param string $str + * @param array $repl + * + * @return string + */ + protected function repl($str, array $repl) + { + return preg_replace(array_keys($repl), array_values($repl), $str); + } + + /** + * Восстановление текста запроса по массиву слов + * + * @param array $words + * + * @return string + */ + protected function queryText(array $words) + { + $space = ''; + $result = ''; + foreach ($words as $word) { + if (isset($word['type']) && 'CJK' === $word['type']) { + $word = '"' . $word['word'] . '"'; + } elseif (is_array($word)) { + $word = '(' . $this->queryText($word) . ')'; + } + $result .= $space . $word; + $space = ' '; + } + return $result; + } +} diff --git a/app/Models/Stopwords.php b/app/Models/Stopwords.php index 69248f1f..510e35d5 100644 --- a/app/Models/Stopwords.php +++ b/app/Models/Stopwords.php @@ -74,6 +74,7 @@ class Stopwords extends Model // Tidy up and filter the stopwords $stopwords = array_map('trim', $stopwords); $stopwords = array_filter($stopwords); + $stopwords = array_flip($stopwords); $this->c->Cache->set('stopwords', ['id' => $id, 'stopwords' => $stopwords]); $this->list = $stopwords; diff --git a/app/Models/Topic/View.php b/app/Models/Topic/View.php index ae8ebafd..1ce07d21 100644 --- a/app/Models/Topic/View.php +++ b/app/Models/Topic/View.php @@ -3,28 +3,41 @@ namespace ForkBB\Models\Topic; use ForkBB\Models\Action; +use ForkBB\Models\Forum\Model as Forum; +use ForkBB\Models\Search\Model as Search; use ForkBB\Models\Topic\Model as Topic; use PDO; +use InvalidArgumentException; +use RuntimeException; class View extends Action { /** * Возвращает список тем * - * @param array $list - * @param bool $expanded + * @param mixed $arg * + * @throws InvalidArgumentException + * * @return array */ - public function view(array $list, $expanded = false) + public function view($arg) { - if (empty($list)) { - return []; + if ($arg instanceof Forum) { + $expanded = false; + } elseif ($arg instanceof Search) { + $expanded = true; + } else { + throw new InvalidArgumentException('Expected Forum or Search'); + } + + if (empty($arg->idsList) || ! is_array($arg->idsList)) { + throw new RuntimeException('Model does not contain a list of topics to display'); } $vars = [ ':uid' => $this->c->user->id, - ':ids' => $list, + ':ids' => $arg->idsList, ]; if (! $this->c->user->isGuest && '1' == $this->c->config->o_show_dot) { @@ -56,7 +69,7 @@ class View extends Action } $stmt = $this->c->DB->query($sql, $vars); - $result = array_flip($list); + $result = array_flip($arg->idsList); while ($row = $stmt->fetch()) { $row['dot'] = isset($dots[$row['id']]); $result[$row['id']] = $this->manager->create($row); diff --git a/app/lang/English/search.po b/app/lang/English/search.po new file mode 100644 index 00000000..611c5915 --- /dev/null +++ b/app/lang/English/search.po @@ -0,0 +1,190 @@ +# +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 \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" + +msgid "User search" +msgstr "User search" + +msgid "No search permission" +msgstr "You do not have permission to use the search feature." + +msgid "Search flood" +msgstr "At least %s seconds have to pass between searches. Please wait %s seconds and try searching again." + +msgid "Search" +msgstr "Search" + +msgid "Search criteria legend" +msgstr "Enter your search criteria" + +msgid "Search info" +msgstr "To search by keyword, enter a term or terms to search for. Separate terms with spaces. Use AND, OR and NOT to refine your search. To search by author enter the username of the author whose posts you wish to search for. Use wildcard character * for partial matches." + +msgid "Keyword search" +msgstr "Keyword search" + +msgid "Author search" +msgstr "Author search" + +msgid "Search in legend" +msgstr "Select where to search" + +msgid "Search in info" +msgstr "Choose in which forum you would like to search and if you want to search in topic subjects, message text or both." + +msgid "Search multiple forums info" +msgstr "If no forums are selected, all forums will be searched." + +msgid "Forum search" +msgstr "Forum" + +msgid "Search in" +msgstr "Search in" + +msgid "Message and subject" +msgstr "Message text and topic subject" + +msgid "Message only" +msgstr "Message text only" + +msgid "Topic only" +msgstr "Topic subject only" + +msgid "Sort by" +msgstr "Sort by" + +msgid "Sort order" +msgstr "Sort order" + +msgid "Search results legend" +msgstr "Select how to view search results" + +msgid "Search results info" +msgstr "You can choose how you wish to sort and show your results." + +msgid "Sort by post time" +msgstr "Post time" + +msgid "Sort by author" +msgstr "Author" + +msgid "Sort by subject" +msgstr "Subject" + +msgid "Sort by forum" +msgstr "Forum" + +msgid "Ascending" +msgstr "Ascending" + +msgid "Descending" +msgstr "Descending" + +msgid "Show as" +msgstr "Show results as" + +msgid "Show as topics" +msgstr "Topics" + +msgid "Show as posts" +msgstr "Posts" + +msgid "Search" +msgstr "Search" + +msgid "Search results" +msgstr "Search results" + +msgid "Quick search show_new" +msgstr "New" + +msgid "Quick search show_recent" +msgstr "Active" + +msgid "Quick search show_unanswered" +msgstr "Unanswered" + +msgid "Quick search show_replies" +msgstr "Posted" + +msgid "Quick search show_user_topics" +msgstr "Topics by %s" + +msgid "Quick search show_user_posts" +msgstr "Posts by %s" + +msgid "Quick search show_subscriptions" +msgstr "Subscribed by %s" + +msgid "Quick search show_user_warn" +msgstr "Warnings for %s" + +msgid "Quick search show_last" +msgstr "Last posts" + +msgid "By keywords show as topics" +msgstr "Topics with posts containing \'%s\'" + +msgid "By keywords show as posts" +msgstr "Posts containing \'%s\'" + +msgid "By user show as topics" +msgstr "Topics with posts by %s" + +msgid "By user show as posts" +msgstr "Posts by %s" + +msgid "By both show as topics" +msgstr "Topics with posts containing \'%s\', by %s" + +msgid "By both show as posts" +msgstr "Posts containing \'%s\', by %s" + +msgid "No terms" +msgstr "You have to enter at least one keyword and/or an author to search for." + +msgid "No hits" +msgstr "Your search returned no hits." + +msgid "No user posts" +msgstr "There are no posts by this user in this forum." + +msgid "No user topics" +msgstr "There are no topics by this user in this forum." + +msgid "No subscriptions" +msgstr "This user is currently not subscribed to any topics." + +msgid "No new posts" +msgstr "There are no topics with new posts since your last visit." + +msgid "No recent posts" +msgstr "No new posts have been made within the last 24 hours." + +msgid "No unanswered" +msgstr "There are no unanswered posts in this forum." + +msgid "Go to post" +msgstr "Go to post" + +msgid "Go to topic" +msgstr "Go to topic" + +msgid "Search btn" +msgstr "Serach" + +msgid "Advanced search" +msgstr "Advanced search" + +msgid "Simple search" +msgstr "Simple search" diff --git a/app/lang/English/stopwords.txt b/app/lang/English/stopwords.txt new file mode 100644 index 00000000..907a2600 --- /dev/null +++ b/app/lang/English/stopwords.txt @@ -0,0 +1,175 @@ +about +after +ago +all +almost +along +also +any +anybody +anywhere +are +arent +aren't +around +ask +been +before +being +between +but +came +can +cant +can't +come +could +couldnt +couldn't +did +didnt +didn't +does +doesnt +doesn't +dont +don't +each +either +else +even +every +everybody +everyone +find +for +from +get +going +gone +got +had +has +have +havent +haven't +having +her +here +hers +him +his +how +ill +i'll +i'm +into +isnt +isn't +itll +it'll +its +it's +ive +i've +just +know +less +like +make +many +may +more +most +much +must +near +never +none +nothing +now +off +often +once +one +only +other +our +ours +our's +out +over +please +rather +really +said +see +she +should +small +some +something +sometime +somewhere +take +than +thank +thanks +that +thats +that's +the +their +theirs +them +then +there +these +they +thing +think +this +those +though +through +thus +too +true +two +under +until +upon +use +very +want +was +way +well +were +what +when +where +which +who +whom +whose +why +will +with +within +without +would +yes +yet +you +your +youre +you're +yours +http +https +ftp +www +com +net +org diff --git a/app/lang/Russian/search.po b/app/lang/Russian/search.po new file mode 100644 index 00000000..52fbde4b --- /dev/null +++ b/app/lang/Russian/search.po @@ -0,0 +1,190 @@ +# +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 \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" + +msgid "User search" +msgstr "Поиск пользователей" + +msgid "No search permission" +msgstr "У вас нет прав для использования поиска." + +msgid "Search flood" +msgstr "Должно пройти как минимум %s секунд между повторными попытками поиска информации. Пожалуйста, подождите %s секунд, а затем повторите попытку." + +msgid "Search" +msgstr "Поиск" + +msgid "Search criteria legend" +msgstr "Условия поиска" + +msgid "Search info" +msgstr "Введите одно или несколько слов для поиска. Отделяйте слова пробелами. Используйте условия AND, OR и NOT. Вы можете дополнить условие, указав автора сообщений. Используйте символ звездочки (*) для поиска по части слова." + +msgid "Keyword search" +msgstr "Ключевое слово или фраза" + +msgid "Author search" +msgstr "Автор" + +msgid "Search in legend" +msgstr "Где искать" + +msgid "Search in info" +msgstr "Выберите в каком разделе искать и где: в заголовках, сообщениях или там и там." + +msgid "Search multiple forums info" +msgstr "Если ни один раздел не выбран, то поиск ведется по всем разделам." + +msgid "Forum search" +msgstr "Раздел(ы)" + +msgid "Search in" +msgstr "Искать в" + +msgid "Message and subject" +msgstr "тексте сообщений и заголовках тем" + +msgid "Message only" +msgstr "только в текстах сообщений" + +msgid "Topic only" +msgstr "только в заголовках тем" + +msgid "Sort by" +msgstr "Сортировать по" + +msgid "Sort order" +msgstr "Порядок сортировки" + +msgid "Search results legend" +msgstr "Настройка параметров вывода результатов" + +msgid "Search results info" +msgstr "Вы можете выбрать, как будут сортироваться, и выводиться результаты поиска." + +msgid "Sort by post time" +msgstr "дате сообщения" + +msgid "Sort by author" +msgstr "автору" + +msgid "Sort by subject" +msgstr "названию темы" + +msgid "Sort by forum" +msgstr "разделу" + +msgid "Ascending" +msgstr "по возрастанию" + +msgid "Descending" +msgstr "по убыванию" + +msgid "Show as" +msgstr "Показать как" + +msgid "Show as topics" +msgstr "темы" + +msgid "Show as posts" +msgstr "сообщения" + +msgid "Search" +msgstr "Поиск" + +msgid "Search results" +msgstr "Результаты поиска" + +msgid "Quick search show_new" +msgstr "Темы с новыми сообщениями" + +msgid "Quick search show_recent" +msgstr "Активные темы" + +msgid "Quick search show_unanswered" +msgstr "Темы без ответа" + +msgid "Quick search show_replies" +msgstr "Темы с вашим участием" + +msgid "Quick search show_user_topics" +msgstr "Темы от %s" + +msgid "Quick search show_user_posts" +msgstr "Сообщения от %s" + +msgid "Quick search show_subscriptions" +msgstr "Подписки пользователя %s" + +msgid "Quick search show_user_warn" +msgstr "Предупреждения для %s" + +msgid "Quick search show_last" +msgstr "Последние сообщения" + +msgid "By keywords show as topics" +msgstr "Ключевые слова "%s" (темы)" + +msgid "By keywords show as posts" +msgstr "Ключевые слова "%s" (сообщения)" + +msgid "By user show as topics" +msgstr "Автор %s (темы)" + +msgid "By user show as posts" +msgstr "Автор %s (сообщения)" + +msgid "By both show as topics" +msgstr "Ключевые слова "%s" и автор %s (темы)" + +msgid "By both show as posts" +msgstr "Ключевые слова "%s" и автор %s (сообщения)" + +msgid "No terms" +msgstr "Необходимо ввести хотя бы одно ключевое слово или автора для проведения поиска." + +msgid "No hits" +msgstr "Ничего не найдено." + +msgid "No user posts" +msgstr "На форуме нет сообщений указанного пользователя." + +msgid "No user topics" +msgstr "На форуме нет тем указанного пользователя." + +msgid "No subscriptions" +msgstr "Данный пользователь не подписан ни на одну тему." + +msgid "No new posts" +msgstr "С вашего последнего визита новых сообщений нет." + +msgid "No recent posts" +msgstr "Нет сообщений за последние сутки." + +msgid "No unanswered" +msgstr "Темы без ответа отсутствуют." + +msgid "Go to post" +msgstr "Перейти к сообщению" + +msgid "Go to topic" +msgstr "Перейти к теме" + +msgid "Search btn" +msgstr "Найти" + +msgid "Advanced search" +msgstr "Расширенный поиск" + +msgid "Simple search" +msgstr "Простой поиск" diff --git a/app/lang/Russian/stopwords.txt b/app/lang/Russian/stopwords.txt new file mode 100644 index 00000000..2671c1dc --- /dev/null +++ b/app/lang/Russian/stopwords.txt @@ -0,0 +1,699 @@ +ага +агаага +агага +айай +айайай +айяй +айяйяй +алсо +ахах +ахахах +без +блабла +блаблабла +блджад +блиин +бля +блять +более +буд +будем +будемто +будет +буду +будут +будуто +будь +будьбудь +был +была +былали +былато +былбы +были +былито +было +былоб +былобы +былото +былто +быть +бытьто +вай +вайвай +вам +вами +вамито +вамто +вас +васто +ваш +ваша +вашато +ваши +вашито +вашто +ваще +ващето +весь +весьто +вот +вота +вотвот +вотс +воттак +воттакой +вотто +вроде +вродето +все +всегда +всегдато +всего +всеголишь +всегонавсего +всегонавсегото +всегото +всем +всеми +всемито +всемто +всетаки +всето +всех +всехто +всю +всюду +всюдуто +всюто +вся +всякий +всякийто +всято +вышло +вышлото +где +гдеб +гдегде +гдеж +гделибо +гденибудь +гденить +гдета +гдето +гмгм +говорится +говоритсято +говориться +говоритьсято +гугу +гыгы +гыгыгы +гыы +гыыы +даже +далее +далеето +для +его +егото +ейто +ему +емуто +если +еслиб +еслибы +еслив +есть +естьли +еще +ещеб +ещелибо +ещето +зато +зачем +зачемлибо +зачемто +зачто +зашто +здесь +здесьто +здеся +ибо +изза +изпод +или +илито +имто +кажется +кажетсято +кажеться +кажетьсято +как +какая +какаялибо +какаянибудь +какаянибуть +какаянить +какаято +какбе +какбудто +какбы +какбэ +какда +какже +какие +какиелибо +какиенибудь +какиенибуть +какиенить +какието +каким +какими +какиминибудь +какиминибуть +какимито +какимлибо +какимнибудь +какимнибуть +какимто +каких +какихлибо +какихнибудь +какихнибуть +какихнить +какихто +каклибо +какнибудь +какнибуть +какнить +каков +какова +каково +каковы +какого +какоголибо +какогонибудь +какогонибуть +какогонить +какогото +какое +какоелибо +какоенибудь +какоенибуть +какоето +какой +какойлибо +какойнибудь +какойнибуть +какойнить +какойта +какойто +каком +какомнибудь +какомнибуть +какомнить +какомто +какому +какомулибо +какомунибудь +какомунибуть +какомуто +както +какунибудь +какунибуть +какую +какуюнибудь +какуюнибуть +какуюнить +какуюто +кгм +кем +кемлибо +кемнибудь +кемнибуть +кемто +когда +когдаж +когдаже +когдалибо +когданибудь +когданибуть +когдато +кого +коголибо +когонибудь +когонибуть +когото +коего +коекто +кои +коий +кой +който +комлибо +комнибудь +комнибуть +комто +кому +комукому +комулибо +комунибудь +комунибуть +комуто +которая +котораянибудь +котораянибуть +котораято +которого +которогонибудь +которогонибуть +которогото +которой +которойнибудь +которойнибуть +которойто +которомуто +который +которыйнибудь +которыйнибуть +которыйто +которым +которыми +которымито +которымнибудь +которымто +кроме +кто +ктобы +ктож +ктокто +ктолибо +ктонибудь +ктонибуть +ктонить +ктото +куда +кудакуда +кудалибо +куданибудь +кудато +куды +кудылибо +кудынибудь +кудыто +куку +кхемкхем +кхм +лалала +либо +лишь +лол +любой +любойто +ляд +ляда +лядато +лядом +лядомто +лядто +ляду +лядуто +ляды +лядыто +ляляля +меня +менято +миня +мне +мнеб +мнебы +мнето +мну +мое +моей +моейто +моему +моемуто +моето +может +можетто +мои +моим +моимто +моито +мой +мойто +моя +моято +наверное +наверноето +наверняка +навернякато +навсего +надо +надобы +надото +надоть +нам +намто +нанана +нафиг +нафига +нафигато +нафигто +нах +нахрен +нахрена +нахренато +нахренто +нахуй +нахуйто +нахуя +нахуято +наш +наша +наше +нашего +нашей +нашем +нашему +наши +нашим +нашими +него +негото +нее +незнаю +ней +нейто +некто +нему +немуто +непойми +непоймито +нет +нибудь +нигде +нигденигде +никак +никакто +никогда +никогданикогда +никогдато +никого +никогото +никому +никомуникому +никомуто +ним +нимто +нифига +нифигато +них +нихто +ничего +ничегото +ничегошеньки +ничему +ничемуто +нынает +нынаю +нынают +однако +одночасье +одночасьето +оказалось +оказалосьто +ололо +она +онато +они +онито +оно +оного +оногото +оному +ономуто +оното +онто +оный +оныйто +оным +оными +онымито +онымто +оных +оныхто +опять +опятьто +откуда +откудалибо +откуданибудь +откудато +отож +оттого +отчего +отчегото +очень +оченьто +пасиб +под +поделаеш +поделаешто +поделаешь +поделаешьто +подумаешь +подумаешьто +пожалуйста +поихнему +поихнемуто +пока +покалибо +покапока +покачто +помоему +понашему +понашемуто +попутно +попутното +порой +поройто +поскольку +посколькуто +постольку +постолькуто +потвоему +потому +потомуто +почему +почемулибо +почемуто +почти +почтито +почтичто +поэтому +поэтомуто +практически +практическито +при +простото +пыщ +пыщпыщ +савсем +савсемто +сам +самой +самойто +самом +самомто +самому +самомуто +самто +сейчас +сейчасто +сенкс +сенкью +сенькс +следовательно +снова +сновато +совершенно +совершенното +совсем +соответственно +спасиб +спасибо +так +такая +такаято +также +таки +такие +такието +таким +такими +такимито +такимто +такое +такоето +такой +такойто +тактак +такто +там +тамто +татата +тащем +тащемто +твайу +твайуто +тваю +тваюто +твоей +твоейто +твои +твоим +твоими +твоимито +твоимто +твоито +твой +твойто +твою +твоюто +твоя +твоято +тебе +тебето +тем +теми +темто +типа +типато +того +тогото +тожа +тоже +той +тойто +только +толькото +том +томто +тому +томуто +тот +тото +тотто +тош +тратата +тсс +тссс +тудато +туды +тудыто +тут +тутто +тьфу +тьфутьфу +тьфутьфутьфу +угу +угугу +угум +угумс +угуугу +уже +ужето +уйуй +уйуйуй +ура +ураа +урааа +ураааа +фактически +фактическито +фиг +фигли +фиглито +фуу +фууу +фууух +фуух +фух +хаха +хахаха +хех +хмхм +хоть +хотя +хотяб +хотябы +хренли +хренлито +хули +хулито +чего +чеголибо +чегочего +чей +чейчей +чем +чемлибо +чемто +чему +чемулибо +чемунибудь +чемуто +чемучему +чемчем +что +чтоб +чтобы +чтолибо +чтото +чточто +чуть +чутьто +чутьчто +чутьчуть +чье +чьем +чьелибо +чьето +чьечье +чьи +чьим +чьилибо +чьито +чья +чьялибо +чьято +чьячья +што +штолибо +штото +эге +эгеге +эгегей +эйэй +эйэйэй +эта +этато +эти +этим +этими +этимито +этимто +это +этого +этогото +этой +этойто +этом +этомто +этому +этомуто +этото +эту +этуто diff --git a/app/templates/forum.tpl b/app/templates/forum.tpl index 546c00ff..1c81c5a7 100644 --- a/app/templates/forum.tpl +++ b/app/templates/forum.tpl @@ -12,15 +12,15 @@ @endsection @section ('linknewtopic') - @if ($p->forum->canCreateTopic) + @if ($p->model->canCreateTopic)
- {!! __('Post topic') !!} + {!! __('Post topic') !!}
@endif @endsection @section ('pagination') @endsection @extends ('layouts/main') -@if ($forums = $p->forums) +@if ($forums = $p->model->subforums)
    -
  1. +
  2. {{ __('Sub forum', 2) }}

    1. @@ -58,7 +58,7 @@ @endif @@ -143,7 +146,7 @@