PTopic.php 15 KB

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