Edit.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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\Pages\PostFormTrait;
  13. use ForkBB\Models\Pages\PostValidatorTrait;
  14. use ForkBB\Models\Poll\Poll;
  15. use ForkBB\Models\Post\Post;
  16. use ForkBB\Models\Topic\Topic;
  17. use ForkBB\Models\User\User;
  18. use function \ForkBB\__;
  19. class Edit extends Page
  20. {
  21. use PostFormTrait;
  22. use PostValidatorTrait;
  23. const SILENT = 1200;
  24. /**
  25. * Редактирование сообщения
  26. */
  27. public function edit(array $args, string $method): Page
  28. {
  29. $post = $this->c->posts->load($args['id']);
  30. if (
  31. ! $post instanceof Post
  32. || ! $post->canEdit
  33. ) {
  34. return $this->c->Message->message('Bad request');
  35. }
  36. $topic = $post->parent;
  37. $firstPost = $post->id === $topic->first_post_id;
  38. $this->c->Lang->load('post');
  39. if (1 === $this->c->config->b_poll_enabled) {
  40. $this->c->Lang->load('poll');
  41. }
  42. if ('POST' === $method) {
  43. $v = $this->messageValidator($post, 'EditPost', $args, true, $firstPost);
  44. if (
  45. $v->validation($_POST)
  46. && null === $v->preview
  47. && null !== $v->submit
  48. ) {
  49. return $this->endEdit($post, $v);
  50. }
  51. $this->fIswev = $v->getErrors();
  52. $args['_vars'] = $v->getData();
  53. if (
  54. null !== $v->preview
  55. && ! $v->getErrors()
  56. ) {
  57. $this->previewHtml = $this->c->censorship->censor(
  58. $this->c->Parser->parseMessage(null, (bool) $v->hide_smilies)
  59. );
  60. $this->useMediaJS = true;
  61. if (
  62. $firstPost
  63. && $this->userRules->usePoll
  64. && $v->poll_enable
  65. ) {
  66. $this->poll = $this->c->polls->create($v->poll);
  67. $this->c->polls->revision($this->poll, true);
  68. }
  69. }
  70. } else {
  71. $args['_vars'] = [
  72. 'message' => $post->message,
  73. 'subject' => $topic->subject,
  74. 'hide_smilies' => $post->hide_smilies,
  75. 'stick_topic' => $topic->sticky,
  76. 'stick_fp' => $topic->stick_fp,
  77. 'edit_post' => $post->edit_post,
  78. ];
  79. }
  80. if (
  81. $firstPost
  82. && $this->userRules->usePoll
  83. ) {
  84. $poll = $topic->poll;
  85. if (
  86. $poll instanceof Poll
  87. && (
  88. ! $poll->canEdit
  89. || 'POST' !== $method
  90. )
  91. ) {
  92. $args['_vars'] = \array_merge($args['_vars'], [
  93. 'pollNoEdit' => ! $poll->canEdit,
  94. 'poll_enable' => $topic->poll_type > 0,
  95. 'poll' => [
  96. 'duration' => $topic->poll_type > 1000 ? $topic->poll_type - 1000 : 0, // ???? перенести в модель poll?
  97. 'hide_result' => $topic->poll_term > 0,
  98. 'question' => $poll->question,
  99. 'type' => $poll->type,
  100. 'answer' => $poll->answer,
  101. ],
  102. ]);
  103. }
  104. if (
  105. null !== $this->previewHtml
  106. && $args['_vars']['poll_enable']
  107. ) {
  108. $this->poll = $this->c->polls->create($args['_vars']['poll']);
  109. $this->c->polls->revision($this->poll, true);
  110. }
  111. }
  112. $this->nameTpl = 'post';
  113. $this->onlinePos = 'topic-' . $topic->id;
  114. // $this->canonical = $post->linkEdit;
  115. $this->robots = 'noindex';
  116. $this->formTitle = $firstPost ? 'Edit topic' : 'Edit post';
  117. $this->crumbs = $this->crumbs($this->formTitle, $topic);
  118. $this->form = $this->messageForm($post, 'EditPost', $args, true, $firstPost, false);
  119. return $this;
  120. }
  121. /**
  122. * Сохранение сообщения
  123. */
  124. protected function endEdit(Post $post, Validator $v): Page
  125. {
  126. $now = \time();
  127. $executive = $this->user->isAdmin || $this->user->isModerator($post);
  128. $topic = $post->parent;
  129. $firstPost = $post->id === $topic->first_post_id;
  130. $calcPost = false;
  131. $calcTopic = false;
  132. $calcForum = false;
  133. $calcAttch = false;
  134. // текст сообщения
  135. if ($post->message !== $v->message) {
  136. $post->message = $v->message;
  137. $calcAttch = true;
  138. if (
  139. $post->poster_id !== $this->user->id
  140. || $now - $post->posted > self::SILENT
  141. || (
  142. $post->editor_id > 0
  143. && $post->editor_id !== $this->user->id
  144. )
  145. ) {
  146. $post->edited = $now;
  147. $post->editor = $this->user->username;
  148. $post->editor_id = $this->user->id;
  149. $calcPost = true;
  150. if ($post->id === $topic->last_post_id) {
  151. $calcTopic = true;
  152. $calcForum = true;
  153. }
  154. }
  155. }
  156. // показ смайлов
  157. if (
  158. 1 === $this->c->config->b_smilies
  159. && (bool) $post->hide_smilies !== (bool) $v->hide_smilies
  160. ) {
  161. $post->hide_smilies = $v->hide_smilies ? 1 : 0;
  162. }
  163. // редактирование без ограничений
  164. if (
  165. $executive
  166. && (bool) $post->edit_post !== (bool) $v->edit_post
  167. ) {
  168. $post->edit_post = $v->edit_post ? 1 : 0;
  169. }
  170. if ($firstPost) {
  171. // заголовок темы
  172. if ($topic->subject !== $v->subject) {
  173. $topic->subject = $v->subject;
  174. $post->edited = $now;
  175. $post->editor = $this->user->username;
  176. $post->editor_id = $this->user->id;
  177. $calcForum = true;
  178. }
  179. // выделение темы
  180. if (
  181. $executive
  182. && (bool) $topic->sticky !== (bool) $v->stick_topic
  183. ) {
  184. $topic->sticky = $v->stick_topic ? 1 : 0;
  185. }
  186. // закрепление первого сообшения
  187. if (
  188. $executive
  189. && (bool) $topic->stick_fp !== (bool) $v->stick_fp
  190. ) {
  191. $topic->stick_fp = $v->stick_fp ? 1 : 0;
  192. }
  193. // опрос
  194. if ($this->userRules->usePoll) {
  195. $this->changePoll($topic, $v);
  196. }
  197. }
  198. // обновление сообщения
  199. $this->c->posts->update($post);
  200. // обновление темы
  201. if ($calcTopic) {
  202. $topic->calcStat();
  203. }
  204. $this->c->topics->update($topic);
  205. // обновление раздела
  206. if ($calcForum) {
  207. $topic->parent->calcStat();
  208. }
  209. $this->c->forums->update($topic->parent);
  210. // синхронизация вложений
  211. if (
  212. $calcAttch
  213. && $this->userRules->useUpload
  214. ) {
  215. $this->c->attachments->syncWithPost($post, true);
  216. }
  217. // антифлуд
  218. if (
  219. $calcPost
  220. || $calcForum
  221. ) {
  222. $this->user->last_post = $now;
  223. $this->c->users->update($this->user);
  224. }
  225. $this->c->search->index($post, 'edit');
  226. return $this->c->Redirect->url($post->link)->message('Edit redirect', FORK_MESS_SUCC);
  227. }
  228. /**
  229. * Изменяет(удаляет/добавляет) данные опроса
  230. */
  231. protected function changePoll(Topic $topic, Validator $v): void
  232. {
  233. if ($topic->poll_type > 0 ) {
  234. $poll = $topic->poll;
  235. if (! $poll->canEdit) {
  236. return;
  237. }
  238. // редактирование
  239. if ($v->poll_enable) {
  240. $topic->poll_type = $v->poll['duration'] > 0 ? 1000 + $v->poll['duration'] : 1; // ???? перенести в модель poll?
  241. # $topic->poll_time = 0;
  242. $topic->poll_term = $v->poll['hide_result']
  243. ? ($topic->poll_term ?: $this->c->config->i_poll_term)
  244. : 0;
  245. $poll->__question = $v->poll['question'];
  246. $poll->__answer = $v->poll['answer'];
  247. $poll->__type = $v->poll['type'];
  248. $this->c->polls->update($poll);
  249. // удаление
  250. } else {
  251. $topic->poll_type = 0;
  252. $topic->poll_time = 0;
  253. $topic->poll_term = 0;
  254. $this->c->polls->delete($poll);
  255. }
  256. // добавление
  257. } elseif ($v->poll_enable) {
  258. $topic->poll_type = $v->poll['duration'] > 0 ? 1000 + $v->poll['duration'] : 1; // ???? перенести в модель poll?
  259. $topic->poll_time = \time();
  260. $topic->poll_term = $v->poll['hide_result'] ? $this->c->config->i_poll_term : 0;
  261. $poll = $this->c->polls->create([
  262. 'tid' => $topic->id,
  263. 'question' => $v->poll['question'],
  264. 'answer' => $v->poll['answer'],
  265. 'type' => $v->poll['type'],
  266. ]);
  267. $this->c->polls->insert($poll);
  268. }
  269. }
  270. /**
  271. * Переводит метку времени в дату/время с учетом часового пояса пользователя
  272. */
  273. protected function timeToDate(int $timestamp): string
  274. {
  275. return \gmdate('Y-m-d\TH:i:s', $timestamp + $this->c->Func->offset());
  276. }
  277. /**
  278. * Переводит дату/время в метку времени с учетом часового пояса пользователя
  279. */
  280. protected function dateToTime(string $date): int
  281. {
  282. return \strtotime("{$date} UTC") - $this->c->Func->offset();
  283. }
  284. /**
  285. * Изменение автора и даты
  286. */
  287. public function change(array $args, string $method): Page
  288. {
  289. $post = $this->c->posts->load($args['id']);
  290. if (
  291. ! $post instanceof Post
  292. || ! $post->canEdit
  293. ) {
  294. return $this->c->Message->message('Bad request');
  295. }
  296. $topic = $post->parent;
  297. $firstPost = $post->id === $topic->first_post_id;
  298. $lastPost = $post->id === $topic->last_post_id;
  299. $this->c->Lang->load('post');
  300. $this->c->Lang->load('validator');
  301. if ('POST' === $method) {
  302. $v = $this->c->Validator->reset()
  303. ->addValidators([
  304. 'username_check' => [$this, 'vUsernameCheck'],
  305. ])->addRules([
  306. 'token' => 'token:ChangeAnD',
  307. 'username' => 'required|string|username_check',
  308. 'posted' => 'required|date',
  309. 'change_and' => 'required|string',
  310. ])->addAliases([
  311. 'username' => 'Username',
  312. 'posted' => 'Posted',
  313. ])->addArguments([
  314. 'token' => $args,
  315. 'username.username_check' => $post->user,
  316. ]);
  317. if ($v->validation($_POST)) {
  318. $ids = [];
  319. $upPost = false;
  320. // изменить имя автора
  321. if (
  322. $this->newUser instanceof User
  323. && $this->newUser->id !== $post->user->id
  324. ) {
  325. if (! $post->user->isGuest) {
  326. $ids[] = $post->user->id;
  327. }
  328. if (! $this->newUser->isGuest) {
  329. $ids[] = $this->newUser->id;
  330. }
  331. $post->poster = $this->newUser->username;
  332. $post->poster_id = $this->newUser->id;
  333. $upPost = true;
  334. }
  335. // изменит время создания
  336. if (\abs($post->posted - $this->dateToTime($v->posted)) >= 60) {
  337. $post->posted = $this->dateToTime($v->posted);
  338. $upPost = true;
  339. }
  340. if ($upPost) {
  341. $post->edited = \time();
  342. $post->editor = $this->user->username;
  343. $post->editor_id = $this->user->id;
  344. $this->c->posts->update($post);
  345. if (
  346. $firstPost
  347. || $lastPost
  348. ) {
  349. $topic->calcStat();
  350. $this->c->topics->update($topic);
  351. if ($lastPost) {
  352. $topic->parent->calcStat();
  353. $this->c->forums->update($topic->parent);
  354. }
  355. }
  356. }
  357. if ($ids) {
  358. $this->c->users->updateCountPosts(...$ids);
  359. if ($firstPost) {
  360. $this->c->users->updateCountTopics(...$ids);
  361. }
  362. }
  363. return $this->c->Redirect->url($post->link)->message('Change redirect', FORK_MESS_SUCC);
  364. }
  365. $this->fIswev = $v->getErrors();
  366. $data = [
  367. 'username' => $v->username ?: $post->poster,
  368. 'posted' => $v->posted ?: $this->timeToDate($post->posted),
  369. ];
  370. } else {
  371. $data = [
  372. 'username' => $post->poster,
  373. 'posted' => $this->timeToDate($post->posted),
  374. ];
  375. }
  376. $this->nameTpl = 'post';
  377. $this->onlinePos = 'topic-' . $topic->id;
  378. $this->robots = 'noindex';
  379. $this->formTitle = $firstPost ? 'Change AnD topic' : 'Change AnD post';
  380. $this->crumbs = $this->crumbs($this->formTitle, $topic);
  381. $this->form = $this->formAuthorAndDate($data, $args);
  382. return $this;
  383. }
  384. public function vUsernameCheck(Validator $v, string $username, $attr, User $user): string
  385. {
  386. if ($username !== $user->username) {
  387. $newUser = $this->c->users->loadByName($username, true);
  388. if ($newUser instanceof User) {
  389. $username = $newUser->username;
  390. $this->newUser = $newUser;
  391. } else {
  392. $v->addError(['User %s does not exist', $username]);
  393. }
  394. }
  395. return $username;
  396. }
  397. /**
  398. * Возвращает данные для построения формы изменения автора поста и времени создания
  399. */
  400. protected function formAuthorAndDate(array $data, array $args): ?array
  401. {
  402. if (! $this->user->isAdmin) {
  403. return null;
  404. }
  405. return [
  406. 'action' => $this->c->Router->link('ChangeAnD', $args),
  407. 'hidden' => [
  408. 'token' => $this->c->Csrf->create('ChangeAnD', $args),
  409. ],
  410. 'sets' => [
  411. 'author-and-date' => [
  412. 'fields' => [
  413. 'username'=> [
  414. 'type' => 'text',
  415. 'minlength' => $this->c->USERNAME['min'],
  416. 'maxlength' => $this->c->USERNAME['max'],
  417. 'caption' => 'Username',
  418. 'required' => true,
  419. 'pattern' => $this->c->USERNAME['jsPattern'],
  420. 'value' => $data['username'] ?? null,
  421. 'autofocus' => true,
  422. ],
  423. 'posted'=> [
  424. 'type' => 'datetime-local',
  425. 'caption' => 'Posted',
  426. 'required' => true,
  427. 'value' => $data['posted'] ?? null,
  428. ],
  429. ],
  430. ],
  431. ],
  432. 'btns' => [
  433. 'change_and' => [
  434. 'type' => 'submit',
  435. 'value' => __('Change'),
  436. ],
  437. ],
  438. ];
  439. }
  440. }