Search.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. <?php
  2. /**
  3. * This file is part of the ForkBB <https://github.com/forkbb>.
  4. *
  5. * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
  6. * @license The MIT License (MIT)
  7. */
  8. declare(strict_types=1);
  9. namespace ForkBB\Models\Pages;
  10. use ForkBB\Core\Validator;
  11. use ForkBB\Models\Page;
  12. use ForkBB\Models\Forum\Forum;
  13. use ForkBB\Models\User\User;
  14. use InvalidArgumentException;
  15. use function \ForkBB\__;
  16. class Search extends Page
  17. {
  18. /**
  19. * Составление списка категорий/разделов для выбора
  20. */
  21. protected function calcList(): void
  22. {
  23. $cid = null;
  24. $options = [];
  25. $idxs = [];
  26. $root = $this->c->forums->get(0);
  27. if ($root instanceof Forum) {
  28. foreach ($this->c->forums->depthList($root, -1) as $f) {
  29. if ($cid !== $f->cat_id) {
  30. $cid = $f->cat_id;
  31. $options[] = [__('Category prefix') . $f->cat_name];
  32. }
  33. $indent = \str_repeat(__('Forum indent'), $f->depth);
  34. if ($f->redirect_url) {
  35. $options[] = [$f->id, $indent . __('Forum prefix') . $f->forum_name, true];
  36. } else {
  37. $options[] = [$f->id, $indent . __('Forum prefix') . $f->forum_name];
  38. $idxs[] = $f->id;
  39. }
  40. }
  41. }
  42. $this->listOfIndexes = $idxs;
  43. $this->listForOptions = $options;
  44. }
  45. /**
  46. * Расширенный поиск
  47. */
  48. public function viewAdvanced(array $args, string $method): Page
  49. {
  50. return $this->view($args, $method, true);
  51. }
  52. /**
  53. * Поиск
  54. */
  55. public function view(array $args, string $method, bool $advanced = false): Page
  56. {
  57. $this->c->Lang->load('validator');
  58. $this->c->Lang->load('search');
  59. $this->calcList();
  60. $marker = $advanced ? 'SearchAdvanced' : 'Search';
  61. $v = null;
  62. if (
  63. 'POST' === $method
  64. || isset($args['keywords'])
  65. ) {
  66. $v = $this->c->Validator->reset()
  67. ->addValidators([
  68. 'check_query' => [$this, 'vCheckQuery'],
  69. 'check_forums' => [$this, 'vCheckForums'],
  70. 'check_author' => [$this, 'vCheckAuthor'],
  71. ])->addRules([
  72. 'author' => 'absent:*',
  73. 'forums' => 'absent:*',
  74. 'serch_in' => 'absent:0|integer',
  75. 'sort_by' => 'absent:0|integer',
  76. 'sort_dir' => 'absent:0|integer',
  77. 'show_as' => 'absent:0|integer',
  78. ])->addArguments([
  79. // 'token' => $args,
  80. ])->addAliases([
  81. 'keywords' => 'Keyword search',
  82. 'author' => 'Author search',
  83. 'forums' => 'Forum search',
  84. 'serch_in' => 'Search in',
  85. 'sort_by' => 'Sort by',
  86. 'sort_dir' => 'Sort order',
  87. 'show_as' => 'Show as',
  88. ]);
  89. if ($advanced) {
  90. $v->addRules([
  91. 'author' => 'required|string:trim|max:25|check_author',
  92. 'forums' => 'check_forums',
  93. 'serch_in' => 'required|integer|in:0,1,2',
  94. 'sort_by' => 'required|integer|in:0,1,2,3',
  95. 'sort_dir' => 'required|integer|in:0,1',
  96. 'show_as' => 'required|integer|in:0,1',
  97. ]);
  98. }
  99. if ('POST' === $method) {
  100. $v->addRules([
  101. 'token' => 'token:' . $marker,
  102. ]);
  103. }
  104. $v->addRules([
  105. 'keywords' => 'required|string:trim|max:100|check_query:' . $method,
  106. ]);
  107. if (
  108. 'POST' === $method
  109. && $v->validation($_POST)
  110. ) {
  111. return $this->c->Redirect->page($marker, $v->getData());
  112. } elseif (
  113. 'GET' === $method
  114. && $v->validation($args)
  115. ) {
  116. return $this->action(\array_merge($args, $v->getData(), ['action' => 'search']), $method, $advanced);
  117. }
  118. $this->fIswev = $v->getErrors();
  119. }
  120. if (! $this->c->config->insensitive()) {
  121. $this->fIswev = ['i', 'The search may be case sensitive'];
  122. }
  123. $this->fIndex = 'search';
  124. $this->nameTpl = 'search';
  125. $this->onlinePos = 'search';
  126. $this->onlineDetail = null;
  127. $this->canonical = $this->c->Router->link('Search');
  128. $this->robots = 'noindex';
  129. $this->form = $advanced ? $this->formSearchAdvanced($v) : $this->formSearch($v);
  130. $this->crumbs = $this->crumbs();
  131. return $this;
  132. }
  133. /**
  134. * Подготавливает массив данных для формы
  135. */
  136. protected function formSearch(Validator $v = null): array
  137. {
  138. return [
  139. 'action' => $this->c->Router->link('Search'),
  140. 'hidden' => [
  141. 'token' => $this->c->Csrf->create('Search'),
  142. ],
  143. 'sets' => [
  144. 'what' => [
  145. 'fields' => [
  146. [
  147. 'type' => 'info',
  148. 'value' => __(['<a href="%s">Advanced search</a>', $this->c->Router->link('SearchAdvanced')]),
  149. 'html' => true,
  150. ],
  151. 'keywords' => [
  152. 'class' => ['w0'],
  153. 'type' => 'text',
  154. 'maxlength' => '100',
  155. 'caption' => 'Keyword search',
  156. 'value' => $v->keywords ?? '',
  157. 'required' => true,
  158. 'autofocus' => true,
  159. ],
  160. ],
  161. ],
  162. ],
  163. 'btns' => [
  164. 'search' => [
  165. 'type' => 'submit',
  166. 'value' => __('Search btn'),
  167. ],
  168. ],
  169. ];
  170. }
  171. /**
  172. * Подготавливает массив данных для формы
  173. */
  174. protected function formSearchAdvanced(Validator $v = null): array
  175. {
  176. return [
  177. 'action' => $this->c->Router->link('SearchAdvanced'),
  178. 'hidden' => [
  179. 'token' => $this->c->Csrf->create('SearchAdvanced'),
  180. ],
  181. 'sets' => [
  182. 'what' => [
  183. 'fields' => [
  184. [
  185. 'type' => 'info',
  186. 'value' => __(['<a href="%s">Simple search</a>', $this->c->Router->link('Search')]),
  187. 'html' => true,
  188. ],
  189. 'keywords' => [
  190. 'class' => ['w2'],
  191. 'type' => 'text',
  192. 'maxlength' => '100',
  193. 'caption' => 'Keyword search',
  194. 'value' => $v->keywords ?? '',
  195. 'required' => true,
  196. 'autofocus' => true,
  197. ],
  198. 'author' => [
  199. 'class' => ['w1'],
  200. 'type' => 'text',
  201. 'maxlength' => '25',
  202. 'caption' => 'Author search',
  203. 'value' => $v->author ?? '*',
  204. 'required' => true,
  205. ],
  206. [
  207. 'type' => 'info',
  208. 'value' => __('Search info'),
  209. ],
  210. ],
  211. ],
  212. 'where' => [
  213. 'legend' => 'Search in legend',
  214. 'fields' => [
  215. 'forums' => [
  216. 'class' => ['w3'],
  217. 'type' => 'multiselect',
  218. 'options' => $this->listForOptions,
  219. 'value' => isset($v->forums) ? \explode('.', $v->forums) : null,
  220. 'caption' => 'Forum search',
  221. 'size' => \min(\count($this->listForOptions), 10),
  222. ],
  223. 'serch_in' => [
  224. 'class' => ['w3'],
  225. 'type' => 'select',
  226. 'options' => [
  227. 0 => __('Message and subject'),
  228. 1 => __('Message only'),
  229. 2 => __('Topic only'),
  230. ],
  231. 'value' => $v->serch_in ?? 0,
  232. 'caption' => 'Search in',
  233. ],
  234. [
  235. 'type' => 'info',
  236. 'value' => __('Search in info'),
  237. ],
  238. [
  239. 'type' => 'info',
  240. 'value' => __('Search multiple forums info'),
  241. ],
  242. ],
  243. ],
  244. 'how' => [
  245. 'legend' => 'Search results legend',
  246. 'fields' => [
  247. 'sort_by' => [
  248. 'class' => ['w4'],
  249. 'type' => 'select',
  250. 'options' => [
  251. 0 => __('Sort by post time'),
  252. 1 => __('Sort by author'),
  253. 2 => __('Sort by subject'),
  254. 3 => __('Sort by forum'),
  255. ],
  256. 'value' => $v->sort_by ?? 0,
  257. 'caption' => 'Sort by',
  258. ],
  259. 'sort_dir' => [
  260. 'class' => ['w4'],
  261. 'type' => 'radio',
  262. 'values' => [
  263. 0 => __('Descending'),
  264. 1 => __('Ascending'),
  265. ],
  266. 'value' => $v->sort_dir ?? 0,
  267. 'caption' => 'Sort order',
  268. ],
  269. 'show_as' => [
  270. 'class' => ['w4'],
  271. 'type' => 'radio',
  272. 'values' => [
  273. 0 => __('Show as posts'),
  274. 1 => __('Show as topics'),
  275. ],
  276. 'value' => $v->show_as ?? 0,
  277. 'caption' => 'Show as',
  278. ],
  279. [
  280. 'type' => 'info',
  281. 'value' => __('Search results info'),
  282. ],
  283. ],
  284. ],
  285. ],
  286. 'btns' => [
  287. 'search' => [
  288. 'type' => 'submit',
  289. 'value' => __('Search btn'),
  290. ],
  291. ],
  292. ];
  293. }
  294. /**
  295. * Дополнительная проверка строки запроса
  296. */
  297. public function vCheckQuery(Validator $v, string $query, string $method): string
  298. {
  299. if (empty($v->getErrors())) {
  300. $flood = $this->user->last_search && \time() - $this->user->last_search < $this->user->g_search_flood;
  301. if (
  302. 'POST' !== $method
  303. || ! $flood
  304. ) {
  305. $search = $this->c->search;
  306. if (! $search->prepare($query)) {
  307. $v->addError([$search->queryError, $search->queryText]);
  308. } else {
  309. if ($this->c->search->execute($v, $this->listOfIndexes, $flood)) {
  310. $flood = false;
  311. if (empty($search->queryIds)) {
  312. $v->addError('No hits', 'i');
  313. }
  314. if (
  315. $search->queryNoCache
  316. && $this->user->g_search_flood
  317. ) {
  318. $this->user->last_search = \time();
  319. $this->c->users->update($this->user); //?????
  320. }
  321. }
  322. }
  323. }
  324. if ($flood) {
  325. $v->addError(['Flood message', $this->user->g_search_flood - \time() + $this->user->last_search]);
  326. }
  327. }
  328. return $query;
  329. }
  330. /**
  331. * Дополнительная проверка разделов
  332. */
  333. public function vCheckForums(Validator $v, /* mixed */ $forums) /* : mixed */
  334. {
  335. if ('*' !== $forums) {
  336. if (
  337. \is_string($forums)
  338. && \preg_match('%^\d+(?:\.\d+)*$%D', $forums)
  339. ) {
  340. $forums = \explode('.', $forums);
  341. } elseif (null === $forums) {
  342. $forums = '*';
  343. } elseif (! \is_array($forums)) {
  344. $v->addError('The :alias contains an invalid value');
  345. $forums = '*';
  346. }
  347. }
  348. if ('*' !== $forums) {
  349. if (! empty(\array_diff($forums, $this->listOfIndexes))) {
  350. $v->addError('The :alias contains an invalid value');
  351. }
  352. \sort($forums, SORT_NUMERIC);
  353. $forums = \implode('.', $forums);
  354. }
  355. return $forums;
  356. }
  357. /**
  358. * Дополнительная проверка автора
  359. */
  360. public function vCheckAuthor(Validator $v, string $name): string
  361. {
  362. $name = \preg_replace('%\*+%', '*', $name);
  363. if (
  364. '*' !== $name
  365. && ! \preg_match('%[\p{L}\p{N}]%', $name)
  366. ) {
  367. $v->addError('The :alias is not valid format');
  368. }
  369. return $name;
  370. }
  371. /**
  372. * Типовые действия
  373. */
  374. public function action(array $args, string $method, bool $advanced = false): Page
  375. {
  376. $this->c->Lang->load('search');
  377. $forum = $args['forum'] ?? 0;
  378. $forum = $this->c->forums->get($forum);
  379. if (! $forum instanceof Forum) {
  380. return $this->c->Message->message('Bad request');
  381. }
  382. $model = $this->c->search;
  383. $model->page = $args['page'] ?? 1;
  384. $action = $args['action'];
  385. $asTopicsList = true;
  386. $list = false;
  387. $uid = $args['uid'] ?? null;
  388. $subIndex = [
  389. 'topics_with_your_posts' => 'with-your-posts',
  390. 'latest_active_topics' => 'latest',
  391. 'unanswered_topics' => 'unanswered',
  392. 'new' => 'new',
  393. ];
  394. switch ($action) {
  395. case 'search':
  396. if (1 === $model->showAs) {
  397. $list = $model->actionT($action, $forum);
  398. } else {
  399. $list = $model->actionP($action, $forum);
  400. $asTopicsList = false;
  401. }
  402. if ('*' === $args['author']) {
  403. $model->name = ['Search query: %s', $args['keywords']];
  404. } else {
  405. $model->name = ['Search query: %1$s and Author: %2$s', $args['keywords'], $args['author']];
  406. }
  407. $model->linkMarker = $advanced ? 'SearchAdvanced' : 'Search';
  408. $model->linkArgs = $args;
  409. break;
  410. case 'new':
  411. case 'topics_with_your_posts':
  412. if ($this->user->isGuest) {
  413. break;
  414. }
  415. case 'latest_active_topics':
  416. case 'unanswered_topics':
  417. if (isset($uid)) {
  418. break;
  419. }
  420. $uid = $this->user->id;
  421. $list = $model->actionT($action, $forum, $uid);
  422. $model->name = __('Quick search ' . $action);
  423. $model->linkMarker = 'SearchAction';
  424. if ($forum->id) {
  425. $model->linkArgs = ['action' => $action, 'forum' => $forum->id];
  426. } else {
  427. $model->linkArgs = ['action' => $action];
  428. }
  429. $this->fSubIndex = $subIndex[$action];
  430. break;
  431. case 'posts':
  432. $asTopicsList = false;
  433. case 'topics':
  434. case 'topics_subscriptions':
  435. case 'forums_subscriptions':
  436. if (! isset($uid)) {
  437. break;
  438. }
  439. $user = $this->c->users->load($uid);
  440. if (
  441. ! $user instanceof User
  442. || $user->isGuest
  443. ) {
  444. break;
  445. }
  446. if ('forums_subscriptions' == $action) {
  447. $list = $model->actionF($action, $forum, $user->id);
  448. } elseif ($asTopicsList) {
  449. $list = $model->actionT($action, $forum, $user->id);
  450. } else {
  451. $list = $model->actionP($action, $forum, $user->id);
  452. }
  453. $model->name = ['Quick search user ' . $action, $user->username];
  454. $model->linkMarker = 'SearchAction';
  455. if ($forum->id) {
  456. $model->linkArgs = ['action' => $action, 'uid' => $user->id, 'forum' => $forum->id];
  457. } else {
  458. $model->linkArgs = ['action' => $action, 'uid' => $user->id];
  459. }
  460. break;
  461. # default:
  462. # throw new InvalidArgumentException('Unknown action: ' . $action);
  463. }
  464. if (false === $list) {
  465. return $this->c->Message->message('Bad request');
  466. } elseif (empty($list)) {
  467. $this->fIswev = ['i', 'No hits'];
  468. return $this->view([], 'GET', true);
  469. }
  470. if ($asTopicsList) {
  471. $this->c->Lang->load('forum');
  472. $this->nameTpl = 'forum';
  473. if ('forums_subscriptions' == $action) {
  474. $this->c->Lang->load('subforums');
  475. $model->subforums = $list;
  476. } else {
  477. $this->topics = $list;
  478. }
  479. } else {
  480. $this->c->Lang->load('topic');
  481. $this->nameTpl = 'topic_in_search';
  482. $this->posts = $list;
  483. }
  484. $this->fIndex = self::FI_SRCH;
  485. $this->onlinePos = 'search';
  486. $this->robots = 'noindex';
  487. $this->model = $model;
  488. $this->crumbs = $this->crumbs($model);
  489. $this->searchMode = true;
  490. return $this;
  491. }
  492. /**
  493. * Возвращает массив хлебных крошек
  494. * Заполняет массив титула страницы
  495. */
  496. protected function crumbs(/* mixed */ ...$crumbs): array
  497. {
  498. $crumbs[] = [$this->c->Router->link('Search'), 'Search'];
  499. return parent::crumbs(...$crumbs);
  500. }
  501. }