PTopic.php 14 KB

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