2018-01-19

This commit is contained in:
Visman 2018-01-19 00:12:50 +07:00
parent ccf1616454
commit e81cfb21d7
25 changed files with 2586 additions and 93 deletions

View file

@ -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') {

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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');

View file

@ -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);

View file

@ -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');

288
app/Models/Pages/Search.php Normal file
View file

@ -0,0 +1,288 @@
<?php
namespace ForkBB\Models\Pages;
use ForkBB\Models\Page;
use ForkBB\Core\Validator;
use InvalidArgumentException;
class Search extends Page
{
use CrumbTrait;
/**
* Поиск
*
* @param array $args
* @param string $method
*
* @return Page
*/
public function view(array $args, $method)
{
$this->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\__('<a href="%s">Simple search</a>', $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\__('<a href="%s">Advanced search</a>', $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;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace ForkBB\Models\Search;
use ForkBB\Models\Method;
use ForkBB\Models\Forum\Model as Forum;
use PDO;
use InvalidArgumentException;
use RuntimeException;
class ActionT extends Method
{
/**
* Действия с темами
*
* @param string $action
*
* @throws InvalidArgumentException
*
* @return false|array
*/
public function actionT($action)
{
$root = $this->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);
}
}

View file

@ -0,0 +1,222 @@
<?php
namespace ForkBB\Models\Search;
use ForkBB\Models\Method;
use ForkBB\Models\Forum\Model as Forum;
use ForkBB\Models\Post\Model as Post;
use PDO;
use InvalidArgumentException;
use RuntimeException;
class Execute extends Method
{
protected $selectForIndex;
protected $selectForPosts;
protected $sortType;
protected $words;
protected $stmt;
/**
* @param array $options
*
* @throws RuntimeException
*
* @return array
*/
public function execute(array $options)
{
if (! is_array($this->model->queryWords) || ! is_string($this->model->queryText)) {
throw new InvalidArgumentException('No query data');
}
echo '<pre>';
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 '</pre>';
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;
}
}

150
app/Models/Search/Index.php Normal file
View file

@ -0,0 +1,150 @@
<?php
namespace ForkBB\Models\Search;
use ForkBB\Models\Method;
use ForkBB\Models\Forum\Model as Forum;
use ForkBB\Models\Post\Model as Post;
use PDO;
use InvalidArgumentException;
use RuntimeException;
class Index extends Method
{
/**
* Индексация сообщения/темы
*
* @param Post $post
* @param string $mode
*/
public function index(Post $post, $mode = 'add')
{
//???? пост после валидации должен иметь дерево тегов
$mesWords = $this->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;
}
}

144
app/Models/Search/Model.php Normal file
View file

@ -0,0 +1,144 @@
<?php
namespace ForkBB\Models\Search;
use ForkBB\Models\Model as BaseModel;
use ForkBB\Models\Forum\Model as Forum;
use ForkBB\Models\Post\Model as Post;
use PDO;
use InvalidArgumentException;
use RuntimeException;
class Model extends BaseModel
{
const CJK_REGEX = '['.
'\x{1100}-\x{11FF}'. // Hangul Jamo
'\x{3130}-\x{318F}'. // Hangul Compatibility Jamo
'\x{AC00}-\x{D7AF}'. // Hangul Syllables
// Hiragana
'\x{3040}-\x{309F}'. // Hiragana
// Katakana
'\x{30A0}-\x{30FF}'. // Katakana
'\x{31F0}-\x{31FF}'. // Katakana Phonetic Extensions
// CJK Unified Ideographs (http://en.wikipedia.org/wiki/CJK_Unified_Ideographs)
'\x{2E80}-\x{2EFF}'. // CJK Radicals Supplement
'\x{2F00}-\x{2FDF}'. // Kangxi Radicals
'\x{2FF0}-\x{2FFF}'. // Ideographic Description Characters
'\x{3000}-\x{303F}'. // CJK Symbols and Punctuation
'\x{31C0}-\x{31EF}'. // CJK Strokes
'\x{3200}-\x{32FF}'. // Enclosed CJK Letters and Months
'\x{3400}-\x{4DBF}'. // CJK Unified Ideographs Extension A
'\x{4E00}-\x{9FFF}'. // CJK Unified Ideographs
'\x{20000}-\x{2A6DF}'. // CJK Unified Ideographs Extension B
']';
/**
* Ссылка на результат поиска
*
* @return string
*/
protected function getlink()
{
return $this->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('%((?<![\p{L}\p{N}])[\'\-]|[\'\-](?![\p{L}\p{N}]))%u', ' ', $text);
if (false !== strpos($text, '-')) {
// удаление слов c -либо|нибу[дт]ь|нить
$text = preg_replace('%\b[\p{L}\p{N}\-\']+\-(?:либо|нибу[дт]ь|нить)(?![\p{L}\p{N}\'\-])%u', '', $text);
// удаление из слов все хвосты с 1 или 2 русскими буквами или -таки|чуть
$text = preg_replace('%(?<=[\p{L}\p{N}])(\-(?:таки|чуть|[а-я]{1,2}))+(?![\p{L}\p{N}\'\-])%u', '', $text);
}
// удаление символов отличающихся от букв и цифр
$text = preg_replace('%(?![\'\-'.($indexing ? '' : '\?\*').'])[^\p{L}\p{N}]+%u', ' ', $text);
// сжатие пробелов
$text = preg_replace('% {2,}%', ' ', $text);
return trim($text);
}
/**
* Проверка слова на:
* стоп-слово
* слово из языков CJK
* длину
*
* @param string $word
* @param bool $indexing
*
* @return null|string
*/
public function word($word, $indexing = false)
{
if (isset($this->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; //?????
}
}

View file

@ -0,0 +1,210 @@
<?php
namespace ForkBB\Models\Search;
use ForkBB\Models\Method;
use ForkBB\Models\Forum\Model as Forum;
use ForkBB\Models\Post\Model as Post;
use PDO;
use InvalidArgumentException;
use RuntimeException;
class Prepare extends Method
{
/**
* Проверка и подготовка поискового запроса
*
* @param string $query
*
* @return array
*/
public function prepare($query)
{
if (substr_count($query, '"') % 2) {
$this->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|(?<![\p{L}\p{N}])\-|[()+|!])\s*|\s+%u',
$subQuery,
-1,
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
) as $cur
) {
$key = null;
switch ($cur) {
case 'AND':
case '+':
$key = 'AND';
case 'OR':
case '|':
$key = $key ?: 'OR';
case 'NOT':
case '-':
case '!':
$key = $key ?: 'NOT';
if (! $keyword) {
$keyword = true;
} elseif (empty($words)) {
$error = 'Syntactic word at the beginning of the search (sub)query: \'%s\'';
} else {
$error = 'Syntactic words follow one after another: \'%s\'';
}
$words[] = $key;
break;
case '(':
$stack[] = [$words, $keyword, $count];
$words = [];
$keyword = true;
$count = 0;
break;
case ')':
if (! $count) {
$error = 'Empty subquery: \'%s\'';
} elseif ($keyword) {
$error = 'Syntactic word at the end of the search subquery: \'%s\'';
}
if (empty($stack)) {
$error = 'The order of brackets is broken: \'%s\'';
} else {
$temp = $words;
list($words, $keyword, $count) = array_pop($stack);
if (! $keyword) {
$words[] = 'AND';
}
$words[] = $temp;
$keyword = false;
++$count;
}
break;
default:
$cur = mb_strtolower($cur, 'UTF-8');
$cur = $this->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;
}
}

View file

@ -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;

View file

@ -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);

190
app/lang/English/search.po Normal file
View file

@ -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 <mio.visman@yandex.ru>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en\n"
msgid "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 "<a href=\"%s\">Advanced search</a>"
msgstr "<a href=\"%s\">Advanced search</a>"
msgid "<a href=\"%s\">Simple search</a>"
msgstr "<a href=\"%s\">Simple search</a>"

View file

@ -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

190
app/lang/Russian/search.po Normal file
View file

@ -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 <mio.visman@yandex.ru>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ru\n"
msgid "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 "<a href=\"%s\">Advanced search</a>"
msgstr "<a href=\"%s\">Расширенный поиск</a>"
msgid "<a href=\"%s\">Simple search</a>"
msgstr "<a href=\"%s\">Простой поиск</a>"

View file

@ -0,0 +1,699 @@
ага
агаага
агага
айай
айайай
айяй
айяйяй
алсо
ахах
ахахах
без
блабла
блаблабла
блджад
блиин
бля
блять
более
буд
будем
будемто
будет
буду
будут
будуто
будь
будьбудь
был
была
былали
былато
былбы
были
былито
было
былоб
былобы
былото
былто
быть
бытьто
вай
вайвай
вам
вами
вамито
вамто
вас
васто
ваш
ваша
вашато
ваши
вашито
вашто
ваще
ващето
весь
весьто
вот
вота
вотвот
вотс
воттак
воттакой
вотто
вроде
вродето
все
всегда
всегдато
всего
всеголишь
всегонавсего
всегонавсегото
всегото
всем
всеми
всемито
всемто
всетаки
всето
всех
всехто
всю
всюду
всюдуто
всюто
вся
всякий
всякийто
всято
вышло
вышлото
где
гдеб
гдегде
гдеж
гделибо
гденибудь
гденить
гдета
гдето
гмгм
говорится
говоритсято
говориться
говоритьсято
гугу
гыгы
гыгыгы
гыы
гыыы
даже
далее
далеето
для
его
егото
ейто
ему
емуто
если
еслиб
еслибы
еслив
есть
естьли
еще
ещеб
ещелибо
ещето
зато
зачем
зачемлибо
зачемто
зачто
зашто
здесь
здесьто
здеся
ибо
изза
изпод
или
илито
имто
кажется
кажетсято
кажеться
кажетьсято
как
какая
какаялибо
какаянибудь
какаянибуть
какаянить
какаято
какбе
какбудто
какбы
какбэ
какда
какже
какие
какиелибо
какиенибудь
какиенибуть
какиенить
какието
каким
какими
какиминибудь
какиминибуть
какимито
какимлибо
какимнибудь
какимнибуть
какимто
каких
какихлибо
какихнибудь
какихнибуть
какихнить
какихто
каклибо
какнибудь
какнибуть
какнить
каков
какова
каково
каковы
какого
какоголибо
какогонибудь
какогонибуть
какогонить
какогото
какое
какоелибо
какоенибудь
какоенибуть
какоето
какой
какойлибо
какойнибудь
какойнибуть
какойнить
какойта
какойто
каком
какомнибудь
какомнибуть
какомнить
какомто
какому
какомулибо
какомунибудь
какомунибуть
какомуто
както
какунибудь
какунибуть
какую
какуюнибудь
какуюнибуть
какуюнить
какуюто
кгм
кем
кемлибо
кемнибудь
кемнибуть
кемто
когда
когдаж
когдаже
когдалибо
когданибудь
когданибуть
когдато
кого
коголибо
когонибудь
когонибуть
когото
коего
коекто
кои
коий
кой
който
комлибо
комнибудь
комнибуть
комто
кому
комукому
комулибо
комунибудь
комунибуть
комуто
которая
котораянибудь
котораянибуть
котораято
которого
которогонибудь
которогонибуть
которогото
которой
которойнибудь
которойнибуть
которойто
которомуто
который
которыйнибудь
которыйнибуть
которыйто
которым
которыми
которымито
которымнибудь
которымто
кроме
кто
ктобы
ктож
ктокто
ктолибо
ктонибудь
ктонибуть
ктонить
ктото
куда
кудакуда
кудалибо
куданибудь
кудато
куды
кудылибо
кудынибудь
кудыто
куку
кхемкхем
кхм
лалала
либо
лишь
лол
любой
любойто
ляд
ляда
лядато
лядом
лядомто
лядто
ляду
лядуто
ляды
лядыто
ляляля
меня
менято
миня
мне
мнеб
мнебы
мнето
мну
мое
моей
моейто
моему
моемуто
моето
может
можетто
мои
моим
моимто
моито
мой
мойто
моя
моято
наверное
наверноето
наверняка
навернякато
навсего
надо
надобы
надото
надоть
нам
намто
нанана
нафиг
нафига
нафигато
нафигто
нах
нахрен
нахрена
нахренато
нахренто
нахуй
нахуйто
нахуя
нахуято
наш
наша
наше
нашего
нашей
нашем
нашему
наши
нашим
нашими
него
негото
нее
незнаю
ней
нейто
некто
нему
немуто
непойми
непоймито
нет
нибудь
нигде
нигденигде
никак
никакто
никогда
никогданикогда
никогдато
никого
никогото
никому
никомуникому
никомуто
ним
нимто
нифига
нифигато
них
нихто
ничего
ничегото
ничегошеньки
ничему
ничемуто
нынает
нынаю
нынают
однако
одночасье
одночасьето
оказалось
оказалосьто
ололо
она
онато
они
онито
оно
оного
оногото
оному
ономуто
оното
онто
оный
оныйто
оным
оными
онымито
онымто
оных
оныхто
опять
опятьто
откуда
откудалибо
откуданибудь
откудато
отож
оттого
отчего
отчегото
очень
оченьто
пасиб
под
поделаеш
поделаешто
поделаешь
поделаешьто
подумаешь
подумаешьто
пожалуйста
поихнему
поихнемуто
пока
покалибо
покапока
покачто
помоему
понашему
понашемуто
попутно
попутното
порой
поройто
поскольку
посколькуто
постольку
постолькуто
потвоему
потому
потомуто
почему
почемулибо
почемуто
почти
почтито
почтичто
поэтому
поэтомуто
практически
практическито
при
простото
пыщ
пыщпыщ
савсем
савсемто
сам
самой
самойто
самом
самомто
самому
самомуто
самто
сейчас
сейчасто
сенкс
сенкью
сенькс
следовательно
снова
сновато
совершенно
совершенното
совсем
соответственно
спасиб
спасибо
так
такая
такаято
также
таки
такие
такието
таким
такими
такимито
такимто
такое
такоето
такой
такойто
тактак
такто
там
тамто
татата
тащем
тащемто
твайу
твайуто
тваю
тваюто
твоей
твоейто
твои
твоим
твоими
твоимито
твоимто
твоито
твой
твойто
твою
твоюто
твоя
твоято
тебе
тебето
тем
теми
темто
типа
типато
того
тогото
тожа
тоже
той
тойто
только
толькото
том
томто
тому
томуто
тот
тото
тотто
тош
тратата
тсс
тссс
тудато
туды
тудыто
тут
тутто
тьфу
тьфутьфу
тьфутьфутьфу
угу
угугу
угум
угумс
угуугу
уже
ужето
уйуй
уйуйуй
ура
ураа
урааа
ураааа
фактически
фактическито
фиг
фигли
фиглито
фуу
фууу
фууух
фуух
фух
хаха
хахаха
хех
хмхм
хоть
хотя
хотяб
хотябы
хренли
хренлито
хули
хулито
чего
чеголибо
чегочего
чей
чейчей
чем
чемлибо
чемто
чему
чемулибо
чемунибудь
чемуто
чемучему
чемчем
что
чтоб
чтобы
чтолибо
чтото
чточто
чуть
чутьто
чутьчто
чутьчуть
чье
чьем
чьелибо
чьето
чьечье
чьи
чьим
чьилибо
чьито
чья
чьялибо
чьято
чьячья
што
штолибо
штото
эге
эгеге
эгегей
эйэй
эйэйэй
эта
этато
эти
этим
этими
этимито
этимто
это
этого
этогото
этой
этойто
этом
этомто
этому
этомуто
этото
эту
этуто

View file

@ -12,15 +12,15 @@
</ul>
@endsection
@section ('linknewtopic')
@if ($p->forum->canCreateTopic)
@if ($p->model->canCreateTopic)
<div class="f-link-post">
<a class="f-btn" href="{!! $p->forum->linkCreateTopic !!}">{!! __('Post topic') !!}</a>
<a class="f-btn" href="{!! $p->model->linkCreateTopic !!}">{!! __('Post topic') !!}</a>
</div>
@endif
@endsection
@section ('pagination')
<nav class="f-pages">
@foreach ($p->forum->pagination as $cur)
@foreach ($p->model->pagination as $cur)
@if ($cur[2])
<span class="f-page active">{{ $cur[1] }}</span>
@elseif ($cur[1] === 'space')
@ -36,13 +36,13 @@
</nav>
@endsection
@extends ('layouts/main')
@if ($forums = $p->forums)
@if ($forums = $p->model->subforums)
<div class="f-nav-links">
@yield ('crumbs')
</div>
<section class="f-subforums">
<ol class="f-ftlist">
<li id="id-subforums{!! $p->forum->id !!}" class="f-category">
<li id="id-subforums{!! $p->model->id !!}" class="f-category">
<h2>{{ __('Sub forum', 2) }}</h2>
<ol class="f-table">
<li class="f-row f-thead" value="0">
@ -58,7 +58,7 @@
@endif
<div class="f-nav-links">
@yield ('crumbs')
@if ($p->forum->canCreateTopic || $p->forum->pagination)
@if ($p->model->canCreateTopic || $p->model->pagination)
<div class="f-links-b clearfix">
@yield ('pagination')
@yield ('linknewtopic')
@ -67,7 +67,7 @@
</div>
@if ($p->topics)
<section class="f-main f-forum">
<h2>{{ $p->forum->forum_name }}</h2>
<h2>{{ $p->model->forum_name or $p->model->name }}</h2>
<div class="f-ftlist">
<ol class="f-table">
<li class="f-row f-thead" value="0">
@ -104,8 +104,6 @@
<span class="f-polltxt">{!! __('Poll') !!}</span>
@endif
<a class="f-ftname" href="{!! $topic->link !!}">{{ cens($topic->subject) }}</a>
</h3>
<span class="f-cmposter">{!! __('by') !!} {{ $topic->poster }}</span>
@if ($topic->pagination)
<span class="f-tpages">
@foreach ($topic->pagination as $cur)
@ -119,6 +117,11 @@
@endif
@if ($topic->hasNew !== false)
<span class="f-newtxt"><a href="{!! $topic->linkNew !!}" title="{!! __('New posts info') !!}">{!! __('New posts') !!}</a></span>
@endif
</h3>
<p class="f-cmposter">{!! __('by') !!} {{ $topic->poster }}</p>
@if ($p->showForum)
<p class="f-cmforum"><a href="{!! $topic->parent->link !!}">{{ $topic->parent->forum_name }}</a></p>
@endif
</div>
</div>
@ -143,7 +146,7 @@
</div>
</section>
<div class="f-nav-links">
@if ($p->forum->canCreateTopic || $p->forum->pagination)
@if ($p->model->canCreateTopic || $p->model->pagination)
<div class="f-links-a clearfix">
@yield ('linknewtopic')
@yield ('pagination')

View file

@ -7,11 +7,7 @@
@foreach ($form['sets'] as $set)
@if (isset($set['info']))
@foreach ($set['info'] as $key => $cur)
@if (empty($cur['html']))
<p class="f-finfo">{{ $cur['value'] }}</p>
@else
<p class="f-finfo">{!! $cur['value'] !!}</p>
@endif
<p class="f-finfo"> @if (empty($cur['html'])){{ $cur['value'] }} @else{!! $cur['value'] !!} @endif</p>
@endforeach
@elseif (isset($set['fields']))
<fieldset @if (isset($set['id'])) id="{{ $set['id'] }}" @endif>
@ -19,48 +15,52 @@
<legend>{!! $set['legend'] !!}</legend>
@endif
@foreach ($set['fields'] as $key => $cur)
@if ('info' === $cur['type'])
<p class="f-child6"> @if (empty($cur['html'])){{ $cur['value'] }} @else{!! $cur['value'] !!} @endif</p>
@else
<dl @if (isset($cur['dl'])) class="f-field-{!! implode(' f-field-', (array) $cur['dl']) !!}" @endif>
<dt> @if (isset($cur['title']))<label class="f-child1 @if (! empty($cur['required'])) f-req @endif" @if (is_string($key)) for="id-{{ $key }}" @endif>{!! $cur['title'] !!}</label> @endif</dt>
<dd>
@if ('text' === $cur['type'])
@if ('text' === $cur['type'])
<input @if (! empty($cur['required'])) required @endif @if (! empty($cur['disabled'])) disabled @endif @if (isset($cur['autofocus'])) autofocus @endif class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}" type="text" @if (! empty($cur['maxlength'])) maxlength="{{ $cur['maxlength'] }}" @endif @if (isset($cur['pattern'])) pattern="{{ $cur['pattern'] }}" @endif @if (isset($cur['value'])) value="{{ $cur['value'] }}" @endif>
@elseif ('textarea' === $cur['type'])
@elseif ('textarea' === $cur['type'])
<textarea @if (! empty($cur['required'])) required @endif @if (! empty($cur['disabled'])) disabled @endif @if (isset($cur['autofocus'])) autofocus @endif class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}">{{ $cur['value'] or '' }}</textarea>
@if (isset($cur['bb']))
@if (isset($cur['bb']))
<ul class="f-child5">
@foreach ($cur['bb'] as $val)
@foreach ($cur['bb'] as $val)
<li><span><a href="{!! $val[0] !!}">{!! $val[1] !!}</a> {!! $val[2] !!}</span></li>
@endforeach
@endforeach
</ul>
@endif
@elseif ('select' === $cur['type'])
<select @if (! empty($cur['required'])) required @endif @if (! empty($cur['disabled'])) disabled @endif @if (isset($cur['autofocus'])) autofocus @endif class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}">
@foreach ($cur['options'] as $v => $option)
@if (is_array($option))
<option value="{{ $option[0] }}" @if ($option[0] == $cur['value']) selected @endif @if (isset($option[2])) disabled @endif>{{ $option[1] }}</option>
@else
<option value="{{ $v }}" @if ($v == $cur['value']) selected @endif>{{ $option }}</option>
@endif
@endforeach
@elseif ('select' === $cur['type'])
<select @if (! empty($cur['required'])) required @endif @if (! empty($cur['disabled'])) disabled @endif @if (isset($cur['autofocus'])) autofocus @endif class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}">
@foreach ($cur['options'] as $v => $option)
@if (is_array($option))
<option value="{{ $option[0] }}" @if ($option[0] == $cur['value']) selected @endif @if (isset($option[2])) disabled @endif>{{ $option[1] }}</option>
@else
<option value="{{ $v }}" @if ($v == $cur['value']) selected @endif>{{ $option }}</option>
@endif
@endforeach
</select>
@elseif ('number' === $cur['type'])
@elseif ('number' === $cur['type'])
<input @if (! empty($cur['required'])) required @endif @if (! empty($cur['disabled'])) disabled @endif @if (isset($cur['autofocus'])) autofocus @endif class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}" type="number" min="{{ $cur['min'] }}" max="{{ $cur['max'] }}" @if (isset($cur['value'])) value="{{ $cur['value'] }}" @endif>
@elseif ('checkbox' === $cur['type'])
@elseif ('checkbox' === $cur['type'])
<label class="f-child2"><input @if (isset($cur['autofocus'])) autofocus @endif @if (! empty($cur['disabled'])) disabled @endif type="checkbox" id="id-{{ $key }}" name="{{ $key }}" value="{{ $cur['value'] or '1' }}" @if (! empty($cur['checked'])) checked @endif>{!! $cur['label'] !!}</label>
@elseif ('radio' === $cur['type'])
@foreach ($cur['values'] as $v => $n)
@elseif ('radio' === $cur['type'])
@foreach ($cur['values'] as $v => $n)
<label class="f-label"><input @if (isset($cur['autofocus'])) autofocus @endif @if (! empty($cur['disabled'])) disabled @endif type="radio" id="id-{{ $key }}-{{ $v }}" name="{{ $key }}" value="{{ $v }}" @if ($v == $cur['value']) checked @endif>{{ $n }}</label>
@endforeach
@elseif ('password' === $cur['type'])
@endforeach
@elseif ('password' === $cur['type'])
<input @if (! empty($cur['required'])) required @endif @if (! empty($cur['disabled'])) disabled @endif @if (isset($cur['autofocus'])) autofocus @endif class="f-ctrl" id="id-{{ $key }}" name="{{ $key }}" type="password" @if (! empty($cur['maxlength'])) maxlength="{{ $cur['maxlength'] }}" @endif @if (isset($cur['pattern'])) pattern="{{ $cur['pattern'] }}" @endif @if (isset($cur['value'])) value="{{ $cur['value'] }}" @endif>
@elseif ('btn' === $cur['type'])
@elseif ('btn' === $cur['type'])
<a class="f-btn @if (! empty($cur['disabled'])) f-disabled @endif" href="{!! $cur['link'] !!}" @if (! empty($cur['disabled'])) tabindex="-1" @endif>{{ $cur['value'] }}</a>
@endif
@if (isset($cur['info']))
@endif
@if (isset($cur['info']))
<p class="f-child4">{!! $cur['info'] !!}</p>
@endif
@endif
</dd>
</dl>
@endif
@endforeach
</fieldset>
@endif

View file

@ -17,7 +17,9 @@
<header class="f-header">
<div class="f-title">
<h1><a href="{!! $p->fRootLink !!}">{{ $p->fTitle }}</a></h1>
@if ($p->fDescription)
<p class="f-description">{!! $p->fDescription !!}</p>
@endif
</div>
@if ($p->fNavigation)
<nav class="main-nav f-menu">

25
app/templates/search.tpl Normal file
View file

@ -0,0 +1,25 @@
@section ('crumbs')
<ul class="f-crumbs">
@foreach ($p->crumbs as $cur)
<li class="f-crumb"><!-- inline -->
@if ($cur[0])
<a href="{!! $cur[0] !!}" @if ($cur[2]) class="active" @endif>{{ $cur[1] }}</a>
@else
<span @if ($cur[2]) class="active" @endif>{{ $cur[1] }}</span>
@endif
</li><!-- endinline -->
@endforeach
</ul>
@endsection
@extends ('layouts/main')
<div class="f-nav-links">
@yield ('crumbs')
</div>
@if ($form = $p->form)
<section class="f-search-form f-main">
<h2>{!! __('Search') !!}</h2>
<div class="f-fdiv">
@include ('layouts/form')
</div>
</section>
@endif

View file

@ -475,7 +475,8 @@ select {
.f-fdiv .f-child2,
.f-fdiv .f-child3,
.f-fdiv .f-child4,
.f-fdiv .f-child5 {
.f-fdiv .f-child5,
.f-fdiv .f-child6 {
display: block;
width: 100%;
}
@ -508,7 +509,8 @@ select {
font-size: 0.875rem;
}
.f-fdiv .f-child4 {
.f-fdiv .f-child4,
.f-fdiv .f-child6 {
font-size: 0.8125rem;
margin-top: 0.3125rem;
text-align: justify;
@ -540,7 +542,7 @@ select {
.f-fdiv .f-finfo + fieldset {
/* padding-top: 0; */
}
.f-ctrl {
border: 0.0625rem solid #AA7939;
}
@ -622,6 +624,7 @@ select {
width: 100%;
border: 0.0625rem solid #AA7939;
border-collapse: collapse;
word-break: break-all;
}
.f-debug thead {
@ -1222,12 +1225,18 @@ select {
font-weight: normal;
font-size: 1rem;
position: relative;
display: inline-block;
display: block;
word-break: break-all;
}
.f-cmposter {
.f-cmposter,
.f-cmforum {
font-size: 0.875rem;
float: left;
}
.f-cmforum:before {
content: ", ";
}
.f-tdot {
@ -1249,6 +1258,18 @@ select {
padding: 0;
}
.f-tpages .f-page,
.f-cmforum > a {
opacity: 0.4;
}
.f-tpages .f-page:focus,
.f-tpages .f-page:hover,
.f-cmforum > a:focus,
.f-cmforum > a:hover {
opacity: 1;
}
/*****************/
/* Страница темы */
/*****************/
@ -1556,16 +1577,19 @@ li + li .f-btn {
.f-fdiv .f-field-t1 {
width: 30%;
float: left;
padding-right: 0.3125rem;
padding-top: 0.625rem;
}
.f-fdiv .f-field-t2 {
width: 70%;
float: left;
padding-left: 0.3125rem;
padding-top: 0.625rem;
}
.f-field-t1 + .f-field-t2,
.f-field-t2 + .f-field-t1 {
padding-left: 0.625rem;
}
}
/*********************/
@ -1752,3 +1776,25 @@ li + li .f-btn {
.f-install > h2{
padding: 0 0.625rem;
}
/*********/
/* Поиск */
/*********/
.f-search-form > h2 {
display: none;
}
.f-search-form .f-child6:first-child {
text-align: right;
}
.f-search-form .f-child6:last-child {
padding-bottom: 0.625rem;
}
@media screen and (min-width: 50rem) {
.f-search-form .f-fdiv .f-field-t1,
.f-search-form .f-fdiv .f-field-t2 {
padding-top: 0;
}
}

View file

@ -205,6 +205,7 @@ class Parserus
}
$res['handler'] = isset($bb['handler']) ? $bb['handler'] : null;
$res['text handler'] = isset($bb['text handler']) ? $bb['text handler'] : null;
$required = [];
$attrs = [];
@ -1067,6 +1068,44 @@ class Parserus
return '[' . $tag . $def . $other . ']' . (isset($this->bbcodes[$tag]['single']) ? '' : $body . '[/' . $tag .']');
}
/**
* Метод возвращает текст без bb-кодов построенный на основании дерева тегов
*
* @param int $id Указатель на текущий тег
*
* @return string
*/
public function getText($id = 0)
{
if (isset($this->data[$id]['tag'])) {
$body = '';
foreach ($this->data[$id]['children'] as $cid) {
$body .= $this->getText($cid);
}
$bb = $this->bbcodes[$this->data[$id]['tag']];
if (null === $bb['text handler']) {
return $body;
}
$attrs = [];
foreach ($this->data[$id]['attrs'] as $key => $val) {
if (isset($bb['attrs'][$key])) {
$attrs[$key] = $val;
}
}
return $bb['text handler']($body, $attrs, $this);
}
$pid = $this->data[$id]['parent'];
$bb = $this->bbcodes[$this->data[$pid]['tag']];
return isset($bb['tags only']) ? '' : $this->data[$id]['text'];
}
/**
* Метод ищет в текстовых узлах ссылки и создает на их месте узлы с bb-кодами url
* Для уменьшения нагрузки использовать при сохранении, а не при выводе