. * * @copyright (c) Visman * @license The MIT License (MIT) */ declare(strict_types=1); namespace ForkBB\Models\Pages; use ForkBB\Core\Container; use ForkBB\Core\Validator; use ForkBB\Models\Page; use ForkBB\Models\Forum\Forum; use ForkBB\Models\Topic\Topic; use ForkBB\Models\Post\Post; use function \ForkBB\__; use function \ForkBB\dt; class Moderate extends Page { const INFORUM = 1; // действие для форума const INTOPIC = 2; // действие для темы const TOTOPIC = 4; // список постов сменить на тему const IFTOTPC = 8; // список постов сменить на тему, если в нем только первый пост темы /** * Список действий * @var array */ protected $actions = [ 'open' => self::INFORUM + self::INTOPIC + self::TOTOPIC, 'close' => self::INFORUM + self::INTOPIC + self::TOTOPIC, 'delete' => self::INFORUM + self::INTOPIC + self::IFTOTPC, 'move' => self::INFORUM + self::INTOPIC + self::IFTOTPC, 'merge' => self::INFORUM, 'cancel' => self::INFORUM + self::INTOPIC + self::TOTOPIC + self::IFTOTPC, 'unstick' => self::INTOPIC + self::TOTOPIC, 'stick' => self::INTOPIC + self::TOTOPIC, 'split' => self::INTOPIC, ]; public function __construct(Container $container) { parent::__construct($container); $this->nameTpl = 'moderate'; $this->onlinePos = 'moderate'; $this->robots = 'noindex, nofollow'; $this->hhsLevel = 'secure'; $container->Lang->load('validator'); $container->Lang->load('misc'); } /** * Составление списка категорий/разделов для выбора */ protected function calcList(int $curForum, bool $noUseCurForum = true): void { $cid = null; $options = []; $idxs = []; $root = $this->c->forums->get(0); if ($root instanceof Forum) { foreach ($this->c->forums->depthList($root, -1) as $f) { if ($cid !== $f->cat_id) { $cid = $f->cat_id; $options[] = [__('Category prefix') . $f->cat_name]; } $indent = \str_repeat(__('Forum indent'), $f->depth); if ( $f->redirect_url || ( $noUseCurForum && $f->id === $curForum ) ) { $options[] = [$f->id, $indent . __('Forum prefix') . $f->forum_name, true]; } else { $options[] = [$f->id, $indent . __('Forum prefix') . $f->forum_name]; $idxs[] = $f->id; } } } $this->listOfIndexes = $idxs; $this->listForOptions = $options; } /** * Определяет действие */ public function vActionProcess(Validator $v, ?string $action): ?string { if (empty($v->getErrors())) { $type = $v->topic ? self::INTOPIC : self::INFORUM; $sum = 0; foreach ($this->actions as $key => $val) { if (isset($v->{$key})) { $action = $key; ++$sum; } } // нажата не одна кнопка или недоступная кнопка if ( 1 !== $sum || ! ($type & $this->actions[$action]) ) { $v->addError('Action not available'); // не выбрано ни одного сообщения для действий прямо этого требующих } elseif ( $v->topic && 1 === \count($v->ids) && ! ((self::TOTOPIC + self::IFTOTPC) & $this->actions[$action]) ) { $v->addError('No object selected'); } // объединение тем if ( 'merge' === $action && \count($v->ids) < 2 ) { $v->addError('Not enough topics selected'); // перенос тем или разделение постов } elseif ( 'move' === $action || 'split' === $action ) { $this->calcList($v->forum, 'move' === $action); if (empty($this->listOfIndexes)) { $v->addError('Nowhere to move'); } elseif ( 1 === $v->confirm && ! \in_array($v->destination, $this->listOfIndexes, true) ) { $v->addError('Invalid destination'); } elseif ( 'split' === $action && 1 === $v->confirm && '' == $v->subject ) { $v->addError('No subject'); } } } return $action; } /** * Обрабатывает модерирование разделов */ public function action(array $args): Page { $v = $this->c->Validator->reset() ->addValidators([ 'action_process' => [$this, 'vActionProcess'], ])->addRules([ 'token' => 'token:Moderate', 'step' => 'required|integer|min:1', 'forum' => 'required|integer|min:1|max:9999999999', 'topic' => 'integer|min:1|max:9999999999', 'page' => 'integer|min:1', 'ids' => 'required|array', 'ids.*' => 'required|integer|min:1|max:9999999999', 'confirm' => 'integer', 'redirect' => 'integer', 'subject' => 'string:trim,spaces|min:1|max:70', 'destination' => 'integer', 'open' => 'string', 'close' => 'string', 'delete' => 'string', 'move' => 'string', 'merge' => 'string', 'cancel' => 'string', 'unstick' => 'string', 'stick' => 'string', 'split' => 'string', 'action' => 'action_process', ])->addAliases([ ])->addArguments([ ])->addMessages([ 'ids' => 'No object selected', ]); if (! $v->validation($_POST)) { $message = $this->c->Message; $message->fIswev = $v->getErrors(); return $message->message(''); } $this->curForum = $this->c->forums->loadTree($v->forum); if (! $this->curForum instanceof Forum) { return $this->c->Message->message('Bad request'); } elseif ( ! $this->user->isAdmin && ! $this->user->isModerator($this->curForum) ) { return $this->c->Message->message('No permission', true, 403); } $page = $v->page ?? 1; if ($v->topic) { $this->curTopic = $this->c->topics->load($v->topic); if ( ! $this->curTopic instanceof Topic || $this->curTopic->parent !== $this->curForum ) { return $this->c->Message->message('Bad request'); } $objects = null; $curType = $this->actions[$v->action]; $ids = $v->ids; $firstId = $this->curTopic->first_post_id; if (self::TOTOPIC & $curType) { $objects = [$this->curTopic]; } elseif (self::IFTOTPC & $curType) { if ( 1 === \count($ids) && \reset($ids) === $firstId ) { $objects = [$this->curTopic]; } } if (null === $objects) { $objects = $this->c->posts->loadByIds(\array_diff($ids, [$firstId]), false); foreach ($objects as $post) { if ( ! $post instanceof Post || $post->parent !== $this->curTopic ) { return $this->c->Message->message('Bad request'); } } $this->processAsPosts = true; } $this->backLink = $this->c->Router->link( 'Topic', [ 'id' => $this->curTopic->id, 'name' => $this->curTopic->name, 'page' => $page, ] ); } else { $objects = $this->c->topics->loadByIds($v->ids, false); foreach ($objects as $topic) { if ( ! $topic instanceof Topic || $topic->parent !== $this->curForum ) { return $this->c->Message->message('Bad request'); } } $this->backLink = $this->c->Router->link( 'Forum', [ 'id' => $this->curForum->id, 'name' => $this->curForum->forum_name, 'page' => $page, ] ); } $this->numObj = \count($objects); return $this->{'action' . \ucfirst($v->action)}($objects, $v); } protected function actionCancel(array $objects, Validator $v): Page { return $this->c->Redirect->url($this->backLink)->message('No confirm redirect'); } protected function actionOpen(array $topics, Validator $v): Page { switch ($v->step) { case 1: $this->formTitle = ['Open topic title', $this->numObj]; $this->buttonValue = ['Open topic btn', $this->numObj]; $this->crumbs = $this->crumbs( [null, $this->formTitle], 'Moderate', $v->topic ? $this->curTopic : $this->curForum ); $this->form = $this->formConfirm($topics, $v); return $this; case 2: if (1 === $v->confirm) { $this->c->topics->access(true, ...$topics); return $this->c->Redirect->url($this->backLink)->message(['Open topic redirect', $this->numObj]); } else { return $this->actionCancel($topics, $v); } default: return $this->c->Message->message('Bad request'); } } protected function actionClose(array $topics, Validator $v): Page { switch ($v->step) { case 1: $this->formTitle = ['Close topic title', $this->numObj]; $this->buttonValue = ['Close topic btn', $this->numObj]; $this->crumbs = $this->crumbs( [null, $this->formTitle], 'Moderate', $v->topic ? $this->curTopic : $this->curForum ); $this->form = $this->formConfirm($topics, $v); return $this; case 2: if (1 === $v->confirm) { $this->c->topics->access(false, ...$topics); return $this->c->Redirect->url($this->backLink)->message(['Close topic redirect', $this->numObj]); } else { return $this->actionCancel($topics, $v); } default: return $this->c->Message->message('Bad request'); } } protected function actionDelete(array $objects, Validator $v): Page { if (! $this->user->isAdmin) { //???? разобраться с правами на удаление foreach ($objects as $object) { if ( ( $object instanceof Topic && isset($this->c->admins->list[$object->poster_id]) ) || ( $object instanceof Post && ! $object->canDelete ) ) { return $this->c->Message->message('No permission', true, 403); //???? причина } } } switch ($v->step) { case 1: $this->formTitle = [ true === $this->processAsPosts ? 'Delete post title' : 'Delete topic title', $this->numObj, ]; $this->buttonValue = [ true === $this->processAsPosts ? 'Delete post btn' : 'Delete topic btn', $this->numObj, ]; $this->crumbs = $this->crumbs( [null, $this->formTitle], 'Moderate', $v->topic ? $this->curTopic : $this->curForum ); $this->form = $this->formConfirm($objects, $v); return $this; case 2: if (1 === $v->confirm) { if (true === $this->processAsPosts) { $this->c->posts->delete(...$objects); $message = 'Delete post redirect'; } else { $this->c->topics->delete(...$objects); $message = 'Delete topic redirect'; } return $this->c->Redirect->url($this->curForum->link)->message([$message, $this->numObj]); } else { return $this->actionCancel($objects, $v); } default: return $this->c->Message->message('Bad request'); } } protected function actionMove(array $topics, Validator $v): Page { switch ($v->step) { case 1: $this->formTitle = ['Move topic title', $this->numObj]; $this->buttonValue = ['Move topic btn', $this->numObj]; $this->crumbs = $this->crumbs( [null, $this->formTitle], 'Moderate', $v->topic ? $this->curTopic : $this->curForum ); $this->chkRedirect = true; $this->form = $this->formConfirm($topics, $v); return $this; case 2: if (1 === $v->confirm) { $forum = $this->c->forums->get($v->destination); $this->c->topics->move(1 === $v->redirect, $forum, ...$topics); return $this->c->Redirect->url($this->curForum->link)->message(['Move topic redirect', $this->numObj]); } else { return $this->actionCancel($topics, $v); } default: return $this->c->Message->message('Bad request'); } } protected function actionMerge(array $topics, Validator $v): Page { foreach ($topics as $topic) { if ($topic->moved_to) { return $this->c->Message->message('Topic links cannot be merged'); } if ( ! $this->firstTopic instanceof Topic || $topic->first_post_id < $this->firstTopic->first_post_id ) { $this->firstTopic = $topic; } } foreach ($topics as $topic) { if ( $this->firstTopic !== $topic && $topic->poll_type > 0 ) { return $this->c->Message->message('Poll cannot be attached'); } } switch ($v->step) { case 1: $this->formTitle = 'Merge topics title'; $this->buttonValue = 'Merge btn'; $this->crumbs = $this->crumbs($this->formTitle, 'Moderate', $this->curForum); $this->chkRedirect = true; $this->form = $this->formConfirm($topics, $v); return $this; case 2: if (1 === $v->confirm) { $this->c->topics->merge(1 === $v->redirect, ...$topics); return $this->c->Redirect->url($this->curForum->link)->message('Merge topics redirect'); } else { return $this->actionCancel($topics, $v); } default: return $this->c->Message->message('Bad request'); } } protected function actionUnstick(array $topics, Validator $v): Page { switch ($v->step) { case 1: $this->formTitle = ['Unstick topic title', $this->numObj]; $this->buttonValue = ['Unstick btn', $this->numObj]; $this->crumbs = $this->crumbs( [null, $this->formTitle], 'Moderate', $v->topic ? $this->curTopic : $this->curForum ); $this->form = $this->formConfirm($topics, $v); return $this; case 2: if (1 === $v->confirm) { foreach ($topics as $topic) { $topic->sticky = 0; $this->c->topics->update($topic); } return $this->c->Redirect->url($this->backLink)->message(['Unstick topic redirect', $this->numObj]); } else { return $this->actionCancel($topics, $v); } default: return $this->c->Message->message('Bad request'); } } protected function actionStick(array $topics, Validator $v): Page { switch ($v->step) { case 1: $this->formTitle = ['Stick topic title', $this->numObj]; $this->buttonValue = ['Stick btn', $this->numObj]; $this->crumbs = $this->crumbs( [null, $this->formTitle], 'Moderate', $v->topic ? $this->curTopic : $this->curForum ); $this->form = $this->formConfirm($topics, $v); return $this; case 2: if (1 === $v->confirm) { foreach ($topics as $topic) { $topic->sticky = 1; $this->c->topics->update($topic); } return $this->c->Redirect->url($this->backLink)->message(['Stick topic redirect', $this->numObj]); } else { return $this->actionCancel($topics, $v); } default: return $this->c->Message->message('Bad request'); } } protected function actionSplit(array $posts, Validator $v): Page { switch ($v->step) { case 1: $this->formTitle = 'Split posts title'; $this->buttonValue = 'Split btn'; $this->needSubject = true; $this->crumbs = $this->crumbs($this->formTitle, 'Moderate', $this->curTopic); $this->form = $this->formConfirm($posts, $v); return $this; case 2: if (1 === $v->confirm) { $newTopic = $this->c->topics->create(); $newTopic->subject = $v->subject; $newTopic->forum_id = $v->forum; $this->c->topics->insert($newTopic); $this->c->posts->move(false, $newTopic, ...$posts); return $this->c->Redirect->url($this->curForum->link)->message('Split posts redirect'); } else { return $this->actionCancel($posts, $v); } default: return $this->c->Message->message('Bad request'); } } /** * Подготавливает массив данных для формы подтверждения */ protected function formConfirm(array $objects, Validator $v): array { $form = [ 'action' => $this->c->Router->link('Moderate'), 'hidden' => [ 'token' => $this->c->Csrf->create('Moderate'), 'step' => $v->step + 1, 'forum' => $v->forum, 'ids' => $v->ids, ], 'sets' => [], 'btns' => [], ]; $autofocus = true; if ($v->topic) { $form['hidden']['topic'] = $v->topic; } $headers = []; foreach ($objects as $object) { if ($object instanceof Topic) { $headers[] = __(['Topic «%s»', $object->name]); } else { $headers[] = __(['Post «%1$s by %2$s»', dt($object->posted), $object->poster]); } } $form['sets']['info'] = [ 'info' => [ [ 'value' => \implode('
', $headers), 'html' => true, ], ], ]; if ($this->firstTopic instanceof Topic) { $form['sets']['info']['info'][] = [ 'value' => __(['All posts will be posted in the «%s» topic', $this->firstTopic->name]), 'html' => true, ]; } $fields = []; if ($this->needSubject) { $fields['subject'] = [ 'type' => 'text', 'maxlength' => '70', 'caption' => 'New subject', 'required' => true, 'value' => '' == $v->subject ? $this->curTopic->subject : $v->subject, 'autofocus' => $autofocus, ]; $autofocus = null; } if ($this->listForOptions) { $fields['destination'] = [ 'type' => 'select', 'options' => $this->listForOptions, 'value' => null, 'caption' => 'Move to', 'autofocus' => $autofocus, ]; $autofocus = null; } if (true === $this->chkRedirect) { $fields['redirect'] = [ 'type' => 'checkbox', 'label' => 'Leave redirect', 'checked' => true, ]; } $fields['confirm'] = [ 'type' => 'checkbox', 'label' => 'Confirm action', 'checked' => false, ]; $form['sets']['moderate']['fields'] = $fields; $form['btns'][$v->action] = [ 'type' => 'submit', 'value' => __($this->buttonValue), ]; $form['btns']['cancel'] = [ 'type' => 'submit', 'value' => __('Cancel'), ]; return $form; } }