Moderate.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  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\Container;
  11. use ForkBB\Core\Validator;
  12. use ForkBB\Models\Page;
  13. use ForkBB\Models\Forum\Forum;
  14. use ForkBB\Models\Topic\Topic;
  15. use ForkBB\Models\Post\Post;
  16. use function \ForkBB\__;
  17. use function \ForkBB\dt;
  18. class Moderate extends Page
  19. {
  20. const INFORUM = 1; // действие для форума
  21. const INTOPIC = 2; // действие для темы
  22. const TOTOPIC = 4; // список постов сменить на тему
  23. const IFTOTPC = 8; // список постов сменить на тему, если в нем только первый пост темы
  24. /**
  25. * Список действий
  26. * @var array
  27. */
  28. protected $actions = [
  29. 'open' => self::INFORUM + self::INTOPIC + self::TOTOPIC,
  30. 'close' => self::INFORUM + self::INTOPIC + self::TOTOPIC,
  31. 'delete' => self::INFORUM + self::INTOPIC + self::IFTOTPC,
  32. 'move' => self::INFORUM + self::INTOPIC + self::IFTOTPC,
  33. 'merge' => self::INFORUM,
  34. 'cancel' => self::INFORUM + self::INTOPIC + self::TOTOPIC + self::IFTOTPC,
  35. 'unstick' => self::INTOPIC + self::TOTOPIC,
  36. 'stick' => self::INTOPIC + self::TOTOPIC,
  37. 'split' => self::INTOPIC,
  38. ];
  39. public function __construct(Container $container)
  40. {
  41. parent::__construct($container);
  42. $this->nameTpl = 'moderate';
  43. $this->onlinePos = 'moderate';
  44. $this->robots = 'noindex, nofollow';
  45. $this->hhsLevel = 'secure';
  46. $container->Lang->load('validator');
  47. $container->Lang->load('misc');
  48. }
  49. /**
  50. * Составление списка категорий/разделов для выбора
  51. */
  52. protected function calcList(int $curForum, bool $noUseCurForum = true): void
  53. {
  54. $cid = null;
  55. $options = [];
  56. $idxs = [];
  57. $root = $this->c->forums->get(0);
  58. if ($root instanceof Forum) {
  59. foreach ($this->c->forums->depthList($root, -1) as $f) {
  60. if ($cid !== $f->cat_id) {
  61. $cid = $f->cat_id;
  62. $options[] = [__('Category prefix') . $f->cat_name];
  63. }
  64. $indent = \str_repeat(__('Forum indent'), $f->depth);
  65. if (
  66. $f->redirect_url
  67. || (
  68. $noUseCurForum
  69. && $f->id === $curForum
  70. )
  71. ) {
  72. $options[] = [$f->id, $indent . __('Forum prefix') . $f->forum_name, true];
  73. } else {
  74. $options[] = [$f->id, $indent . __('Forum prefix') . $f->forum_name];
  75. $idxs[] = $f->id;
  76. }
  77. }
  78. }
  79. $this->listOfIndexes = $idxs;
  80. $this->listForOptions = $options;
  81. }
  82. /**
  83. * Определяет действие
  84. */
  85. public function vActionProcess(Validator $v, ?string $action): ?string
  86. {
  87. if (empty($v->getErrors())) {
  88. $type = $v->topic ? self::INTOPIC : self::INFORUM;
  89. $sum = 0;
  90. foreach ($this->actions as $key => $val) {
  91. if (isset($v->{$key})) {
  92. $action = $key;
  93. ++$sum;
  94. }
  95. }
  96. // нажата не одна кнопка или недоступная кнопка
  97. if (
  98. 1 !== $sum
  99. || ! ($type & $this->actions[$action])
  100. ) {
  101. $v->addError('Action not available');
  102. // не выбрано ни одного сообщения для действий прямо этого требующих
  103. } elseif (
  104. $v->topic
  105. && 1 === \count($v->ids)
  106. && ! ((self::TOTOPIC + self::IFTOTPC) & $this->actions[$action])
  107. ) {
  108. $v->addError('No object selected');
  109. }
  110. // объединение тем
  111. if (
  112. 'merge' === $action
  113. && \count($v->ids) < 2
  114. ) {
  115. $v->addError('Not enough topics selected');
  116. // перенос тем или разделение постов
  117. } elseif (
  118. 'move' === $action
  119. || 'split' === $action
  120. ) {
  121. $this->calcList($v->forum, 'move' === $action);
  122. if (empty($this->listOfIndexes)) {
  123. $v->addError('Nowhere to move');
  124. } elseif (
  125. 1 === $v->confirm
  126. && ! \in_array($v->destination, $this->listOfIndexes, true)
  127. ) {
  128. $v->addError('Invalid destination');
  129. } elseif (
  130. 'split' === $action
  131. && 1 === $v->confirm
  132. && '' == $v->subject
  133. ) {
  134. $v->addError('No subject');
  135. }
  136. }
  137. }
  138. return $action;
  139. }
  140. /**
  141. * Обрабатывает модерирование разделов
  142. */
  143. public function action(array $args): Page
  144. {
  145. $v = $this->c->Validator->reset()
  146. ->addValidators([
  147. 'action_process' => [$this, 'vActionProcess'],
  148. ])->addRules([
  149. 'token' => 'token:Moderate',
  150. 'step' => 'required|integer|min:1',
  151. 'forum' => 'required|integer|min:1|max:9999999999',
  152. 'topic' => 'integer|min:1|max:9999999999',
  153. 'page' => 'integer|min:1',
  154. 'ids' => 'required|array',
  155. 'ids.*' => 'required|integer|min:1|max:9999999999',
  156. 'confirm' => 'integer',
  157. 'redirect' => 'integer',
  158. 'subject' => 'string:trim,spaces|min:1|max:70',
  159. 'destination' => 'integer',
  160. 'open' => 'string',
  161. 'close' => 'string',
  162. 'delete' => 'string',
  163. 'move' => 'string',
  164. 'merge' => 'string',
  165. 'cancel' => 'string',
  166. 'unstick' => 'string',
  167. 'stick' => 'string',
  168. 'split' => 'string',
  169. 'action' => 'action_process',
  170. ])->addAliases([
  171. ])->addArguments([
  172. ])->addMessages([
  173. 'ids' => 'No object selected',
  174. ]);
  175. if (! $v->validation($_POST)) {
  176. $message = $this->c->Message;
  177. $message->fIswev = $v->getErrors();
  178. return $message->message('');
  179. }
  180. $this->curForum = $this->c->forums->loadTree($v->forum);
  181. if (! $this->curForum instanceof Forum) {
  182. return $this->c->Message->message('Bad request');
  183. } elseif (
  184. ! $this->user->isAdmin
  185. && ! $this->user->isModerator($this->curForum)
  186. ) {
  187. return $this->c->Message->message('No permission', true, 403);
  188. }
  189. $page = $v->page ?? 1;
  190. if ($v->topic) {
  191. $this->curTopic = $this->c->topics->load($v->topic);
  192. if (
  193. ! $this->curTopic instanceof Topic
  194. || $this->curTopic->parent !== $this->curForum
  195. ) {
  196. return $this->c->Message->message('Bad request');
  197. }
  198. $objects = null;
  199. $curType = $this->actions[$v->action];
  200. $ids = $v->ids;
  201. $firstId = $this->curTopic->first_post_id;
  202. if (self::TOTOPIC & $curType) {
  203. $objects = [$this->curTopic];
  204. } elseif (self::IFTOTPC & $curType) {
  205. if (
  206. 1 === \count($ids)
  207. && \reset($ids) === $firstId
  208. ) {
  209. $objects = [$this->curTopic];
  210. }
  211. }
  212. if (null === $objects) {
  213. $objects = $this->c->posts->loadByIds(\array_diff($ids, [$firstId]), false);
  214. foreach ($objects as $post) {
  215. if (
  216. ! $post instanceof Post
  217. || $post->parent !== $this->curTopic
  218. ) {
  219. return $this->c->Message->message('Bad request');
  220. }
  221. }
  222. $this->processAsPosts = true;
  223. }
  224. $this->backLink = $this->c->Router->link(
  225. 'Topic',
  226. [
  227. 'id' => $this->curTopic->id,
  228. 'name' => $this->curTopic->name,
  229. 'page' => $page,
  230. ]
  231. );
  232. } else {
  233. $objects = $this->c->topics->loadByIds($v->ids, false);
  234. foreach ($objects as $topic) {
  235. if (
  236. ! $topic instanceof Topic
  237. || $topic->parent !== $this->curForum
  238. ) {
  239. return $this->c->Message->message('Bad request');
  240. }
  241. }
  242. $this->backLink = $this->c->Router->link(
  243. 'Forum',
  244. [
  245. 'id' => $this->curForum->id,
  246. 'name' => $this->curForum->forum_name,
  247. 'page' => $page,
  248. ]
  249. );
  250. }
  251. $this->numObj = \count($objects);
  252. return $this->{'action' . \ucfirst($v->action)}($objects, $v);
  253. }
  254. protected function actionCancel(array $objects, Validator $v): Page
  255. {
  256. return $this->c->Redirect->url($this->backLink)->message('No confirm redirect');
  257. }
  258. protected function actionOpen(array $topics, Validator $v): Page
  259. {
  260. switch ($v->step) {
  261. case 1:
  262. $this->formTitle = ['Open topic title', $this->numObj];
  263. $this->buttonValue = ['Open topic btn', $this->numObj];
  264. $this->crumbs = $this->crumbs(
  265. [null, $this->formTitle],
  266. 'Moderate',
  267. $v->topic ? $this->curTopic : $this->curForum
  268. );
  269. $this->form = $this->formConfirm($topics, $v);
  270. return $this;
  271. case 2:
  272. if (1 === $v->confirm) {
  273. $this->c->topics->access(true, ...$topics);
  274. return $this->c->Redirect->url($this->backLink)->message(['Open topic redirect', $this->numObj]);
  275. } else {
  276. return $this->actionCancel($topics, $v);
  277. }
  278. default:
  279. return $this->c->Message->message('Bad request');
  280. }
  281. }
  282. protected function actionClose(array $topics, Validator $v): Page
  283. {
  284. switch ($v->step) {
  285. case 1:
  286. $this->formTitle = ['Close topic title', $this->numObj];
  287. $this->buttonValue = ['Close topic btn', $this->numObj];
  288. $this->crumbs = $this->crumbs(
  289. [null, $this->formTitle],
  290. 'Moderate',
  291. $v->topic ? $this->curTopic : $this->curForum
  292. );
  293. $this->form = $this->formConfirm($topics, $v);
  294. return $this;
  295. case 2:
  296. if (1 === $v->confirm) {
  297. $this->c->topics->access(false, ...$topics);
  298. return $this->c->Redirect->url($this->backLink)->message(['Close topic redirect', $this->numObj]);
  299. } else {
  300. return $this->actionCancel($topics, $v);
  301. }
  302. default:
  303. return $this->c->Message->message('Bad request');
  304. }
  305. }
  306. protected function actionDelete(array $objects, Validator $v): Page
  307. {
  308. if (! $this->user->isAdmin) { //???? разобраться с правами на удаление
  309. foreach ($objects as $object) {
  310. if (
  311. (
  312. $object instanceof Topic
  313. && isset($this->c->admins->list[$object->poster_id])
  314. )
  315. || (
  316. $object instanceof Post
  317. && ! $object->canDelete
  318. )
  319. ) {
  320. return $this->c->Message->message('No permission', true, 403); //???? причина
  321. }
  322. }
  323. }
  324. switch ($v->step) {
  325. case 1:
  326. $this->formTitle = [
  327. true === $this->processAsPosts
  328. ? 'Delete post title'
  329. : 'Delete topic title',
  330. $this->numObj,
  331. ];
  332. $this->buttonValue = [
  333. true === $this->processAsPosts
  334. ? 'Delete post btn'
  335. : 'Delete topic btn',
  336. $this->numObj,
  337. ];
  338. $this->crumbs = $this->crumbs(
  339. [null, $this->formTitle],
  340. 'Moderate',
  341. $v->topic ? $this->curTopic : $this->curForum
  342. );
  343. $this->form = $this->formConfirm($objects, $v);
  344. return $this;
  345. case 2:
  346. if (1 === $v->confirm) {
  347. if (true === $this->processAsPosts) {
  348. $this->c->posts->delete(...$objects);
  349. $message = 'Delete post redirect';
  350. } else {
  351. $this->c->topics->delete(...$objects);
  352. $message = 'Delete topic redirect';
  353. }
  354. return $this->c->Redirect->url($this->curForum->link)->message([$message, $this->numObj]);
  355. } else {
  356. return $this->actionCancel($objects, $v);
  357. }
  358. default:
  359. return $this->c->Message->message('Bad request');
  360. }
  361. }
  362. protected function actionMove(array $topics, Validator $v): Page
  363. {
  364. switch ($v->step) {
  365. case 1:
  366. $this->formTitle = ['Move topic title', $this->numObj];
  367. $this->buttonValue = ['Move topic btn', $this->numObj];
  368. $this->crumbs = $this->crumbs(
  369. [null, $this->formTitle],
  370. 'Moderate',
  371. $v->topic ? $this->curTopic : $this->curForum
  372. );
  373. $this->chkRedirect = true;
  374. $this->form = $this->formConfirm($topics, $v);
  375. return $this;
  376. case 2:
  377. if (1 === $v->confirm) {
  378. $forum = $this->c->forums->get($v->destination);
  379. $this->c->topics->move(1 === $v->redirect, $forum, ...$topics);
  380. return $this->c->Redirect->url($this->curForum->link)->message(['Move topic redirect', $this->numObj]);
  381. } else {
  382. return $this->actionCancel($topics, $v);
  383. }
  384. default:
  385. return $this->c->Message->message('Bad request');
  386. }
  387. }
  388. protected function actionMerge(array $topics, Validator $v): Page
  389. {
  390. foreach ($topics as $topic) {
  391. if ($topic->moved_to) {
  392. return $this->c->Message->message('Topic links cannot be merged');
  393. }
  394. if (
  395. ! $this->firstTopic instanceof Topic
  396. || $topic->first_post_id < $this->firstTopic->first_post_id
  397. ) {
  398. $this->firstTopic = $topic;
  399. }
  400. }
  401. foreach ($topics as $topic) {
  402. if (
  403. $this->firstTopic !== $topic
  404. && $topic->poll_type > 0
  405. ) {
  406. return $this->c->Message->message('Poll cannot be attached');
  407. }
  408. }
  409. switch ($v->step) {
  410. case 1:
  411. $this->formTitle = 'Merge topics title';
  412. $this->buttonValue = 'Merge btn';
  413. $this->crumbs = $this->crumbs($this->formTitle, 'Moderate', $this->curForum);
  414. $this->chkRedirect = true;
  415. $this->form = $this->formConfirm($topics, $v);
  416. return $this;
  417. case 2:
  418. if (1 === $v->confirm) {
  419. $this->c->topics->merge(1 === $v->redirect, ...$topics);
  420. return $this->c->Redirect->url($this->curForum->link)->message('Merge topics redirect');
  421. } else {
  422. return $this->actionCancel($topics, $v);
  423. }
  424. default:
  425. return $this->c->Message->message('Bad request');
  426. }
  427. }
  428. protected function actionUnstick(array $topics, Validator $v): Page
  429. {
  430. switch ($v->step) {
  431. case 1:
  432. $this->formTitle = ['Unstick topic title', $this->numObj];
  433. $this->buttonValue = ['Unstick btn', $this->numObj];
  434. $this->crumbs = $this->crumbs(
  435. [null, $this->formTitle],
  436. 'Moderate',
  437. $v->topic ? $this->curTopic : $this->curForum
  438. );
  439. $this->form = $this->formConfirm($topics, $v);
  440. return $this;
  441. case 2:
  442. if (1 === $v->confirm) {
  443. foreach ($topics as $topic) {
  444. $topic->sticky = 0;
  445. $this->c->topics->update($topic);
  446. }
  447. return $this->c->Redirect->url($this->backLink)->message(['Unstick topic redirect', $this->numObj]);
  448. } else {
  449. return $this->actionCancel($topics, $v);
  450. }
  451. default:
  452. return $this->c->Message->message('Bad request');
  453. }
  454. }
  455. protected function actionStick(array $topics, Validator $v): Page
  456. {
  457. switch ($v->step) {
  458. case 1:
  459. $this->formTitle = ['Stick topic title', $this->numObj];
  460. $this->buttonValue = ['Stick btn', $this->numObj];
  461. $this->crumbs = $this->crumbs(
  462. [null, $this->formTitle],
  463. 'Moderate',
  464. $v->topic ? $this->curTopic : $this->curForum
  465. );
  466. $this->form = $this->formConfirm($topics, $v);
  467. return $this;
  468. case 2:
  469. if (1 === $v->confirm) {
  470. foreach ($topics as $topic) {
  471. $topic->sticky = 1;
  472. $this->c->topics->update($topic);
  473. }
  474. return $this->c->Redirect->url($this->backLink)->message(['Stick topic redirect', $this->numObj]);
  475. } else {
  476. return $this->actionCancel($topics, $v);
  477. }
  478. default:
  479. return $this->c->Message->message('Bad request');
  480. }
  481. }
  482. protected function actionSplit(array $posts, Validator $v): Page
  483. {
  484. switch ($v->step) {
  485. case 1:
  486. $this->formTitle = 'Split posts title';
  487. $this->buttonValue = 'Split btn';
  488. $this->needSubject = true;
  489. $this->crumbs = $this->crumbs($this->formTitle, 'Moderate', $this->curTopic);
  490. $this->form = $this->formConfirm($posts, $v);
  491. return $this;
  492. case 2:
  493. if (1 === $v->confirm) {
  494. $newTopic = $this->c->topics->create();
  495. $newTopic->subject = $v->subject;
  496. $newTopic->forum_id = $v->forum;
  497. $this->c->topics->insert($newTopic);
  498. $this->c->posts->move(false, $newTopic, ...$posts);
  499. return $this->c->Redirect->url($this->curForum->link)->message('Split posts redirect');
  500. } else {
  501. return $this->actionCancel($posts, $v);
  502. }
  503. default:
  504. return $this->c->Message->message('Bad request');
  505. }
  506. }
  507. /**
  508. * Подготавливает массив данных для формы подтверждения
  509. */
  510. protected function formConfirm(array $objects, Validator $v): array
  511. {
  512. $form = [
  513. 'action' => $this->c->Router->link('Moderate'),
  514. 'hidden' => [
  515. 'token' => $this->c->Csrf->create('Moderate'),
  516. 'step' => $v->step + 1,
  517. 'forum' => $v->forum,
  518. 'ids' => $v->ids,
  519. ],
  520. 'sets' => [],
  521. 'btns' => [],
  522. ];
  523. $autofocus = true;
  524. if ($v->topic) {
  525. $form['hidden']['topic'] = $v->topic;
  526. }
  527. $headers = [];
  528. foreach ($objects as $object) {
  529. if ($object instanceof Topic) {
  530. $headers[] = __(['Topic «%s»', $object->name]);
  531. } else {
  532. $headers[] = __(['Post «%1$s by %2$s»', dt($object->posted), $object->poster]);
  533. }
  534. }
  535. $form['sets']['info'] = [
  536. 'info' => [
  537. [
  538. 'value' => \implode('<br>', $headers),
  539. 'html' => true,
  540. ],
  541. ],
  542. ];
  543. if ($this->firstTopic instanceof Topic) {
  544. $form['sets']['info']['info'][] = [
  545. 'value' => __(['All posts will be posted in the «%s» topic', $this->firstTopic->name]),
  546. 'html' => true,
  547. ];
  548. }
  549. $fields = [];
  550. if ($this->needSubject) {
  551. $fields['subject'] = [
  552. 'type' => 'text',
  553. 'maxlength' => '70',
  554. 'caption' => 'New subject',
  555. 'required' => true,
  556. 'value' => '' == $v->subject ? $this->curTopic->subject : $v->subject,
  557. 'autofocus' => $autofocus,
  558. ];
  559. $autofocus = null;
  560. }
  561. if ($this->listForOptions) {
  562. $fields['destination'] = [
  563. 'type' => 'select',
  564. 'options' => $this->listForOptions,
  565. 'value' => null,
  566. 'caption' => 'Move to',
  567. 'autofocus' => $autofocus,
  568. ];
  569. $autofocus = null;
  570. }
  571. if (true === $this->chkRedirect) {
  572. $fields['redirect'] = [
  573. 'type' => 'checkbox',
  574. 'label' => 'Leave redirect',
  575. 'checked' => true,
  576. ];
  577. }
  578. $fields['confirm'] = [
  579. 'type' => 'checkbox',
  580. 'label' => 'Confirm action',
  581. 'checked' => false,
  582. ];
  583. $form['sets']['moderate']['fields'] = $fields;
  584. $form['btns'][$v->action] = [
  585. 'type' => 'submit',
  586. 'value' => __($this->buttonValue),
  587. ];
  588. $form['btns']['cancel'] = [
  589. 'type' => 'submit',
  590. 'value' => __('Cancel'),
  591. ];
  592. return $form;
  593. }
  594. }