Moderate.php 22 KB

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