PTopic.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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\PM;
  10. use ForkBB\Core\Container;
  11. use ForkBB\Models\DataModel;
  12. use ForkBB\Models\PM\Cnst;
  13. use ForkBB\Models\User\User;
  14. use PDO;
  15. use RuntimeException;
  16. class PTopic extends DataModel
  17. {
  18. /**
  19. * Ключ модели для контейнера
  20. * @var string
  21. */
  22. protected $cKey = 'PTopic';
  23. public function __construct(Container $container)
  24. {
  25. parent::__construct($container);
  26. $this->zDepend = [
  27. 'id' => ['link', 'hasNew', 'linkNew', 'firstNew', 'pagination', 'dataReply', 'linkReply'],
  28. 'first_post_id' => ['firstNew'],
  29. 'last_number' => ['last_poster'],
  30. 'last_post' => ['firstNew'],
  31. 'last_post_id' => ['linkLast', 'firstNew'],
  32. 'num_replies' => ['numPages', 'pagination'],
  33. 'poster' => ['last_poster', 'byOrFor', 'zpUser', 'ztUser', 'blockStatus'],
  34. 'poster_id' => ['closed', 'firstNew', 'zp', 'zt', 'zpUser', 'ztUser', 'actionsAllowed', 'canReply', 'canSend', 'blockStatus'],
  35. 'poster_status' => ['closed', 'actionsAllowed', 'canReply', 'isFullDeleted', 'canSend'],
  36. 'poster_visit' => ['firstNew'],
  37. 'subject' => ['name'],
  38. 'target' => ['last_poster', 'byOrFor', 'zpUser', 'ztUser', 'blockStatus'],
  39. 'target_id' => ['closed', 'firstNew', 'zp', 'zt', 'zpUser', 'ztUser', 'actionsAllowed', 'canReply', 'canSend', 'blockStatus'],
  40. 'target_status' => ['closed', 'actionsAllowed', 'canReply', 'isFullDeleted', 'canSend'],
  41. 'target_visit' => ['firstNew'],
  42. ];
  43. }
  44. /**
  45. * Префикс текущего пользователя
  46. */
  47. protected function getzp(): string
  48. {
  49. if ($this->poster_id === $this->c->user->id) {
  50. return 'poster';
  51. } elseif ($this->target_id === $this->c->user->id) {
  52. return 'target';
  53. } else {
  54. throw new RuntimeException('Bad current user');
  55. }
  56. }
  57. /**
  58. * Префикс второго пользователя
  59. */
  60. protected function getzt(): string
  61. {
  62. if ($this->poster_id === $this->c->user->id) {
  63. return 'target';
  64. } elseif ($this->target_id === $this->c->user->id) {
  65. return 'poster';
  66. } else {
  67. throw new RuntimeException('Bad current user');
  68. }
  69. }
  70. /**
  71. * Возвращает отцензурированное название темы
  72. */
  73. protected function getname(): ?string
  74. {
  75. return $this->censorSubject;
  76. }
  77. /**
  78. * Ссылка на тему
  79. */
  80. protected function getlink(): string
  81. {
  82. return $this->c->Router->link(
  83. 'PMAction',
  84. [
  85. 'second' => $this->c->pms->second,
  86. 'action' => Cnst::ACTION_TOPIC,
  87. 'more1' => $this->id,
  88. ]
  89. );
  90. }
  91. /**
  92. * Ссылка для перехода на последнее сообщение темы
  93. */
  94. protected function getlinkLast(): string
  95. {
  96. if ($this->last_post_id < 1) {
  97. return '';
  98. } else {
  99. return $this->c->Router->link(
  100. 'PMAction',
  101. [
  102. 'second' => $this->c->pms->second,
  103. 'action' => Cnst::ACTION_POST,
  104. 'more1' => $this->last_post_id,
  105. 'numPost' => $this->last_post_id,
  106. ]
  107. );
  108. }
  109. }
  110. /**
  111. * Ссылка для перехода на первое новое сообщение в теме
  112. */
  113. protected function getlinkNew(): string
  114. {
  115. return $this->c->Router->link(
  116. 'PMAction',
  117. [
  118. 'second' => $this->c->pms->second,
  119. 'action' => Cnst::ACTION_TOPIC,
  120. 'more1' => $this->id,
  121. 'more2' => Cnst::ACTION_NEW,
  122. ]
  123. );
  124. }
  125. /**
  126. * Ссылка на уделение темы
  127. */
  128. protected function getlinkDelete(): string
  129. {
  130. return $this->c->Router->link(
  131. 'PMAction',
  132. [
  133. 'second' => $this->c->pms->second,
  134. 'action' => Cnst::ACTION_DELETE,
  135. 'more1' => $this->id,
  136. 'more2' => Cnst::ACTION_TOPIC,
  137. ]
  138. );
  139. }
  140. /**
  141. * Номер первого нового сообщения в теме
  142. */
  143. protected function getfirstNew(): int
  144. {
  145. if (! $this->hasNew) {
  146. return 0;
  147. }
  148. $visit = $this->{"{$this->zp}_visit"};
  149. if ($visit < 1) {
  150. return $this->first_post_id;
  151. } elseif ($visit >= $this->last_post) {
  152. return $this->last_post_id;
  153. }
  154. $vars = [
  155. ':tid' => $this->id,
  156. ':visit' => $visit,
  157. ];
  158. $query = 'SELECT MIN(pp.id)
  159. FROM ::pm_posts AS pp
  160. WHERE pp.topic_id=?i:tid AND pp.posted>?i:visit';
  161. return (int) $this->c->DB->query($query, $vars)->fetchColumn();
  162. }
  163. protected function setsender(User $user): void
  164. {
  165. $this->poster = $user->username;
  166. $this->poster_id = $user->id;
  167. }
  168. protected function setrecipient(User $user): void
  169. {
  170. $this->target = $user->username;
  171. $this->target_id = $user->id;
  172. }
  173. protected function user(string $prx): User
  174. {
  175. if (
  176. $this->{"{$prx}_id"} < 1
  177. || ! ($user = $this->c->users->load($this->{"{$prx}_id"})) instanceof User
  178. ) {
  179. $user = $this->c->users->guest(['username' => $this->{$prx}]);
  180. }
  181. if (! $user instanceof User) {
  182. throw new RuntimeException('User model could not be loaded ');
  183. }
  184. return $user;
  185. }
  186. protected function getzpUser(): User
  187. {
  188. return $this->user($this->zp);
  189. }
  190. protected function getztUser(): User
  191. {
  192. return $this->user($this->zt);
  193. }
  194. protected function setstatus(int $status): void
  195. {
  196. if ('poster' === $this->zp) {
  197. $tStatus = $status;
  198. switch ($status) {
  199. case Cnst::PT_ARCHIVE:
  200. $tStatus = Cnst::PT_NOTSENT;
  201. case Cnst::PT_DELETED:
  202. case Cnst::PT_NORMAL:
  203. $this->poster_status = $status;
  204. if (null === $this->target_status) {
  205. $this->target_status = $tStatus;
  206. }
  207. return;
  208. }
  209. } else {
  210. switch ($status) {
  211. case Cnst::PT_ARCHIVE:
  212. case Cnst::PT_DELETED:
  213. $this->target_status = $status;
  214. return;
  215. }
  216. }
  217. throw new RuntimeException("Bad status: {$status}");
  218. }
  219. /**
  220. * Возвращает имя автора последнего поста или пустую строку
  221. */
  222. protected function getlast_poster(): string
  223. {
  224. if (0 === $this->last_number) {
  225. return $this->poster;
  226. } elseif (1 === $this->last_number) {
  227. return $this->target;
  228. } else {
  229. return '';
  230. }
  231. }
  232. /**
  233. * Статус наличия новых сообщений в теме
  234. */
  235. protected function gethasNew(): bool
  236. {
  237. return isset($this->c->pms->idsNew[$this->id]);
  238. }
  239. protected function getbyOrFor(): array
  240. {
  241. if ('poster' === $this->zp) {
  242. return ['for %s', $this->target];
  243. } else {
  244. return ['by %s', $this->poster];
  245. }
  246. }
  247. /**
  248. * Количество страниц в теме
  249. */
  250. protected function getnumPages(): int
  251. {
  252. if (null === $this->num_replies) {
  253. throw new RuntimeException('The model does not have the required data');
  254. }
  255. return (int) \ceil(($this->num_replies + 1) / $this->c->user->disp_posts);
  256. }
  257. /**
  258. * Статус наличия установленной страницы в теме
  259. */
  260. public function hasPage(): bool
  261. {
  262. return $this->page > 0 && $this->page <= $this->numPages;
  263. }
  264. /**
  265. * Массив страниц темы
  266. */
  267. protected function getpagination(): array
  268. {
  269. $page = (int) $this->page;
  270. if (
  271. $page < 1
  272. && 1 === $this->numPages
  273. ) {
  274. // 1 страницу в списке тем раздела не отображаем
  275. return [];
  276. } else {
  277. return $this->c->Func->paginate(
  278. $this->numPages,
  279. $page,
  280. 'PMAction',
  281. [
  282. 'second' => $this->c->pms->second,
  283. 'action' => Cnst::ACTION_TOPIC,
  284. 'more1' => $this->id,
  285. 'page' => 'more2', // нестандарная переменная для page
  286. ]
  287. );
  288. }
  289. }
  290. /**
  291. * Вычисляет страницу темы на которой находится данное сообщение
  292. */
  293. public function calcPage(int $pid): void
  294. {
  295. $vars = [
  296. ':tid' => $this->id,
  297. ':pid' => $pid,
  298. ];
  299. $query = 'SELECT COUNT(pp.id) AS pnum, MAX(pp.id) as pmax
  300. FROM ::pm_posts AS pp
  301. WHERE pp.topic_id=?i:tid AND pp.id<=?i:pid';
  302. $result = $this->c->DB->query($query, $vars)->fetch();
  303. if (
  304. empty($result['pmax'])
  305. || $result['pmax'] !== $pid
  306. ) {
  307. $this->page = null;
  308. } else {
  309. $this->page = (int) \ceil($result['pnum'] / $this->c->user->disp_posts);
  310. }
  311. }
  312. /**
  313. * Возвращает массив сообщений с установленной страницы
  314. */
  315. public function pageData(): array
  316. {
  317. if (! $this->hasPage()) {
  318. throw new InvalidArgumentException('Bad number of displayed page');
  319. }
  320. $count = ($this->page - 1) * $this->c->user->disp_posts;
  321. $vars = [
  322. ':tid' => $this->id,
  323. ':offset' => $count,
  324. ':rows' => $this->c->user->disp_posts,
  325. ];
  326. $query = 'SELECT pp.id
  327. FROM ::pm_posts AS pp
  328. WHERE pp.topic_id=?i:tid
  329. ORDER BY pp.id
  330. LIMIT ?i:rows OFFSET ?i:offset';
  331. $list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
  332. $posts = $this->c->pms->loadByIds(Cnst::PPOST, $list);
  333. foreach ($posts as $post) {
  334. ++$count;
  335. if ($post instanceof PPost) {
  336. $post->__postNumber = $count;
  337. }
  338. }
  339. return $posts;
  340. }
  341. /**
  342. * Обновляет метку визита
  343. */
  344. public function updateVisit(): void
  345. {
  346. $visit = $this->{"{$this->zp}_visit"};
  347. if ($visit >= $this->last_post) {
  348. return;
  349. }
  350. $this->{"{$this->zp}_visit"} = $this->last_post;
  351. $this->c->pms->update(Cnst::PTOPIC, $this);
  352. $this->c->pms->recalculate($this->zpUser);
  353. }
  354. /**
  355. * Возвращает массив сообщений обзора темы
  356. */
  357. public function review(): array
  358. {
  359. if ($this->c->config->i_topic_review < 1) {
  360. return [];
  361. }
  362. $count = $this->num_replies + 1;
  363. $vars = [
  364. ':tid' => $this->id,
  365. ':rows' => $this->c->config->i_topic_review,
  366. ];
  367. $query = 'SELECT pp.id
  368. FROM ::pm_posts AS pp
  369. WHERE pp.topic_id=?i:tid
  370. ORDER BY pp.id DESC
  371. LIMIT ?i:rows';
  372. $list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
  373. $posts = $this->c->pms->loadByIds(Cnst::PPOST, $list);
  374. foreach ($posts as $post) {
  375. if ($post instanceof PPost) {
  376. $post->__postNumber = $count;
  377. }
  378. --$count;
  379. }
  380. return $posts;
  381. }
  382. /**
  383. * Аргументы для ссылки для ответа в теме
  384. */
  385. protected function getdataReply(): array
  386. {
  387. return [
  388. 'second' => $this->c->pms->second,
  389. 'action' => Cnst::ACTION_SEND,
  390. 'more1' => $this->id,
  391. ];
  392. }
  393. /**
  394. * Ссылка для ответа в теме
  395. */
  396. protected function getlinkReply(): string
  397. {
  398. return $this->c->Router->link('PMAction', $this->dataReply);
  399. }
  400. /**
  401. * Статус закрытия темы
  402. */
  403. protected function getclosed(): bool
  404. {
  405. $p = $this->{"{$this->zp}_status"};
  406. $t = $this->{"{$this->zt}_status"};
  407. return Cnst::PT_DELETED === $t
  408. || Cnst::PT_ARCHIVE === $t
  409. || (
  410. Cnst::PT_ARCHIVE === $p
  411. && Cnst::PT_NOTSENT !== $t
  412. );
  413. }
  414. /**
  415. * Статус возможности действий
  416. */
  417. protected function getactionsAllowed(): bool
  418. {
  419. return ! $this->closed
  420. && $this->zpUser->usePM
  421. && $this->ztUser->usePM;
  422. }
  423. /**
  424. * Статус блокировки между пользователями
  425. * 2 - отправитель заблокирвоали получателя
  426. * 1 - получатель заблокировал отправителя
  427. * 0 - блокировки нет
  428. */
  429. protected function getblockStatus(): int
  430. {
  431. if ($this->c->pms->block->isBlock($this->ztUser) && ! $this->ztUser->isAdmin) {
  432. return 2;
  433. } elseif ($this->c->pms->block->inBlock($this->ztUser) && ! $this->zpUser->isAdmin) {
  434. return 1;
  435. } else {
  436. return 0;
  437. }
  438. }
  439. /**
  440. * Статус возможности ответа в теме
  441. */
  442. protected function getcanReply(): bool
  443. {
  444. return $this->actionsAllowed
  445. && 0 === $this->blockStatus
  446. && (
  447. (
  448. 1 === $this->zpUser->u_pm
  449. && 1 === $this->ztUser->u_pm
  450. )
  451. || (
  452. Cnst::PT_ARCHIVE === $this->{"{$this->zp}_status"}
  453. && Cnst::PT_NOTSENT === $this->{"{$this->zt}_status"}
  454. )
  455. || $this->zpUser->isAdmin
  456. || $this->ztUser->isAdmin
  457. );
  458. }
  459. /**
  460. * Статус возможности отправить архивный диалог получателю
  461. */
  462. protected function getcanSend(): bool
  463. {
  464. return Cnst::PT_ARCHIVE === $this->poster_status
  465. && $this->actionsAllowed
  466. && 0 === $this->blockStatus
  467. && 1 === $this->zpUser->u_pm
  468. && (
  469. 1 === $this->ztUser->u_pm
  470. || $this->zpUser->isAdmin
  471. );
  472. }
  473. /**
  474. * Ссылка для отправки архивного диалога
  475. */
  476. protected function getlinkSend(): string
  477. {
  478. return $this->c->Router->link(
  479. 'PMAction',
  480. [
  481. 'second' => $this->c->pms->second,
  482. 'action' => Cnst::ACTION_TOPIC,
  483. 'more1' => $this->id,
  484. 'more2' => Cnst::ACTION_SEND,
  485. ]
  486. );
  487. }
  488. /**
  489. * Статус удаления диалога у обоих собеседников
  490. */
  491. protected function getisFullDeleted(): bool
  492. {
  493. return (
  494. Cnst::PT_DELETED === $this->poster_status
  495. || Cnst::PT_NOTSENT === $this->poster_status
  496. )
  497. && (
  498. Cnst::PT_DELETED === $this->target_status
  499. || Cnst::PT_NOTSENT === $this->target_status
  500. );
  501. }
  502. }