Model.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. <?php
  2. namespace ForkBB\Models\Topic;
  3. use ForkBB\Core\Container;
  4. use ForkBB\Models\DataModel;
  5. use ForkBB\Models\Forum\Model as Forum;
  6. use PDO;
  7. use RuntimeException;
  8. class Model extends DataModel
  9. {
  10. /**
  11. * Конструктор
  12. *
  13. * @param Container $container
  14. */
  15. # public function __construct(Container $container)
  16. # {
  17. # parent::__construct($container);
  18. #
  19. # $this->zDepend = [
  20. # 'subject' => ['censSubject'],
  21. # ];
  22. # }
  23. /**
  24. * Получение родительского раздела
  25. *
  26. * @throws RuntimeException
  27. *
  28. * @return Forum|null
  29. */
  30. protected function getparent(): ?Forum
  31. {
  32. if ($this->forum_id < 1) {
  33. throw new RuntimeException('Parent is not defined');
  34. }
  35. $forum = $this->c->forums->get($this->forum_id);
  36. if (
  37. ! $forum instanceof Forum
  38. || $forum->redirect_url
  39. ) {
  40. return null;
  41. } else {
  42. return $forum;
  43. }
  44. }
  45. /**
  46. * Возвращает отцензурированное название темы
  47. */
  48. protected function getname(): ?string
  49. {
  50. return $this->censorSubject;
  51. }
  52. /**
  53. * Статус возможности ответа в теме
  54. *
  55. * @return bool
  56. */
  57. protected function getcanReply(): bool
  58. {
  59. if ($this->c->user->isAdmin) {
  60. return true;
  61. } elseif (
  62. $this->closed
  63. || $this->c->user->isBot
  64. ) {
  65. return false;
  66. } elseif (
  67. '1' == $this->parent->post_replies
  68. || (
  69. null === $this->parent->post_replies
  70. && '1' == $this->c->user->g_post_replies
  71. )
  72. || $this->c->user->isModerator($this)
  73. ) {
  74. return true;
  75. } else {
  76. return false;
  77. }
  78. }
  79. /**
  80. * Статус возможности использования подписок
  81. */
  82. protected function getcanSubscription(): bool
  83. {
  84. return '1' == $this->c->config->o_topic_subscriptions
  85. && $this->id > 0
  86. && ! $this->c->user->isGuest
  87. && ! $this->c->user->isUnverified;
  88. }
  89. /**
  90. * Ссылка на тему
  91. *
  92. * @return string
  93. */
  94. protected function getlink(): string
  95. {
  96. return $this->c->Router->link(
  97. 'Topic',
  98. [
  99. 'id' => $this->moved_to ?: $this->id,
  100. 'name' => $this->censorSubject,
  101. ]
  102. );
  103. }
  104. /**
  105. * Ссылка для ответа в теме
  106. *
  107. * @return string
  108. */
  109. protected function getlinkReply(): string
  110. {
  111. return $this->c->Router->link(
  112. 'NewReply',
  113. [
  114. 'id' => $this->id,
  115. ]
  116. );
  117. }
  118. /**
  119. * Ссылка для перехода на последнее сообщение темы
  120. *
  121. * @return null|string
  122. */
  123. protected function getlinkLast(): ?string
  124. {
  125. if ($this->moved_to) {
  126. return null;
  127. } else {
  128. return $this->c->Router->link(
  129. 'ViewPost',
  130. [
  131. 'id' => $this->last_post_id,
  132. ]
  133. );
  134. }
  135. }
  136. /**
  137. * Ссылка для перехода на первое новое сообщение в теме
  138. *
  139. * @return string
  140. */
  141. protected function getlinkNew(): string
  142. {
  143. return $this->c->Router->link(
  144. 'TopicViewNew',
  145. [
  146. 'id' => $this->id,
  147. ]
  148. );
  149. }
  150. /**
  151. * Ссылка для перехода на первое не прочитанное сообщение в теме
  152. */
  153. protected function getlinkUnread(): string
  154. {
  155. return $this->c->Router->link(
  156. 'TopicViewUnread',
  157. [
  158. 'id' => $this->id,
  159. ]
  160. );
  161. }
  162. /**
  163. * Ссылка на подписку
  164. */
  165. protected function getlinkSubscribe(): ?string
  166. {
  167. return $this->c->Router->link(
  168. 'TopicSubscription',
  169. [
  170. 'tid' => $this->id,
  171. 'type' => 'subscribe',
  172. 'token' => $this->c->Csrf->create(
  173. 'TopicSubscription',
  174. [
  175. 'tid' => $this->id,
  176. 'type' => 'subscribe',
  177. ]
  178. ),
  179. ]
  180. );
  181. }
  182. /**
  183. * Ссылка на отписку
  184. */
  185. protected function getlinkUnsubscribe(): ?string
  186. {
  187. return $this->c->Router->link(
  188. 'TopicSubscription',
  189. [
  190. 'tid' => $this->id,
  191. 'type' => 'unsubscribe',
  192. 'token' => $this->c->Csrf->create(
  193. 'TopicSubscription',
  194. [
  195. 'tid' => $this->id,
  196. 'type' => 'unsubscribe',
  197. ]
  198. ),
  199. ]
  200. );
  201. }
  202. /**
  203. * Статус наличия новых сообщений в теме
  204. *
  205. * @return false|int
  206. */
  207. protected function gethasNew()
  208. {
  209. if (
  210. $this->c->user->isGuest
  211. || $this->moved_to
  212. ) {
  213. return false;
  214. }
  215. $time = \max(
  216. (int) $this->c->user->u_mark_all_read,
  217. (int) $this->parent->mf_mark_all_read,
  218. (int) $this->c->user->last_visit,
  219. (int) $this->mt_last_visit
  220. );
  221. return $this->last_post > $time ? $time : false;
  222. }
  223. /**
  224. * Статус наличия непрочитанных сообщений в теме
  225. *
  226. * @return false|int
  227. */
  228. protected function gethasUnread()
  229. {
  230. if (
  231. $this->c->user->isGuest
  232. || $this->moved_to
  233. ) {
  234. return false;
  235. }
  236. $time = \max(
  237. (int) $this->c->user->u_mark_all_read,
  238. (int) $this->parent->mf_mark_all_read,
  239. (int) $this->mt_last_read
  240. );
  241. return $this->last_post > $time ? $time : false;
  242. }
  243. /**
  244. * Номер первого нового сообщения в теме
  245. *
  246. * @return int
  247. */
  248. protected function getfirstNew(): int
  249. {
  250. if (false === $this->hasNew) {
  251. return 0;
  252. }
  253. $vars = [
  254. ':tid' => $this->id,
  255. ':visit' => $this->hasNew,
  256. ];
  257. $query = 'SELECT MIN(p.id)
  258. FROM ::posts AS p
  259. WHERE p.topic_id=?i:tid AND p.posted>?i:visit';
  260. $pid = $this->c->DB->query($query, $vars)->fetchColumn();
  261. return $pid ?: 0;
  262. }
  263. /**
  264. * Номер первого не прочитанного сообщения в теме
  265. *
  266. * @return int
  267. */
  268. protected function getfirstUnread(): int
  269. {
  270. if (false === $this->hasUnread) {
  271. return 0;
  272. }
  273. $vars = [
  274. ':tid' => $this->id,
  275. ':visit' => $this->hasUnread,
  276. ];
  277. $query = 'SELECT MIN(p.id)
  278. FROM ::posts AS p
  279. WHERE p.topic_id=?i:tid AND p.posted>?i:visit';
  280. $pid = $this->c->DB->query($query, $vars)->fetchColumn();
  281. return $pid ?: 0;
  282. }
  283. /**
  284. * Количество страниц в теме
  285. *
  286. * @throws RuntimeException
  287. *
  288. * @return int
  289. */
  290. protected function getnumPages(): int
  291. {
  292. if (null === $this->num_replies) {
  293. throw new RuntimeException('The model does not have the required data');
  294. }
  295. return (int) \ceil(($this->num_replies + 1) / $this->c->user->disp_posts);
  296. }
  297. /**
  298. * Массив страниц темы
  299. *
  300. * @return array
  301. */
  302. protected function getpagination(): array
  303. {
  304. $page = (int) $this->page;
  305. if (
  306. $page < 1
  307. && 1 === $this->numPages
  308. ) {
  309. // 1 страницу в списке тем раздела не отображаем
  310. return [];
  311. } else { //????
  312. return $this->c->Func->paginate(
  313. $this->numPages,
  314. $page,
  315. 'Topic',
  316. [
  317. 'id' => $this->id,
  318. 'name' => $this->censorSubject,
  319. ]
  320. );
  321. }
  322. }
  323. /**
  324. * Статус наличия установленной страницы в теме
  325. *
  326. * @return bool
  327. */
  328. public function hasPage(): bool
  329. {
  330. return $this->page > 0 && $this->page <= $this->numPages;
  331. }
  332. /**
  333. * Возвращает массив сообщений с установленной страницы
  334. *
  335. * @throws InvalidArgumentException
  336. *
  337. * @return array
  338. */
  339. public function pageData(): array
  340. {
  341. if (! $this->hasPage()) {
  342. throw new InvalidArgumentException('Bad number of displayed page');
  343. }
  344. $vars = [
  345. ':tid' => $this->id,
  346. ':offset' => ($this->page - 1) * $this->c->user->disp_posts,
  347. ':rows' => $this->c->user->disp_posts,
  348. ];
  349. $query = 'SELECT p.id
  350. FROM ::posts AS p
  351. WHERE p.topic_id=?i:tid
  352. ORDER BY p.id
  353. LIMIT ?i:offset, ?i:rows';
  354. $list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
  355. if (
  356. ! empty($list)
  357. && (
  358. $this->stick_fp
  359. || $this->poll_type
  360. )
  361. && ! \in_array($this->first_post_id, $list)
  362. ) {
  363. \array_unshift($list, $this->first_post_id);
  364. }
  365. $this->idsList = $list;
  366. return empty($this->idsList) ? [] : $this->c->posts->view($this);
  367. }
  368. /**
  369. * Возвращает массив сообщений обзора темы
  370. *
  371. * @return array
  372. */
  373. public function review(): array
  374. {
  375. if ($this->c->config->o_topic_review < 1) {
  376. return [];
  377. }
  378. $this->page = 1;
  379. $vars = [
  380. ':tid' => $this->id,
  381. ':rows' => $this->c->config->o_topic_review,
  382. ];
  383. $query = 'SELECT p.id
  384. FROM ::posts AS p
  385. WHERE p.topic_id=?i:tid
  386. ORDER BY p.id DESC
  387. LIMIT 0, ?i:rows';
  388. $this->idsList = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
  389. return empty($this->idsList) ? [] : $this->c->posts->view($this, true);
  390. }
  391. /**
  392. * Вычисляет страницу темы на которой находится данное сообщение
  393. *
  394. * @param int $pid
  395. */
  396. public function calcPage(int $pid): void
  397. {
  398. $vars = [
  399. ':tid' => $this->id,
  400. ':pid' => $pid,
  401. ];
  402. $query = 'SELECT COUNT(p.id) AS num
  403. FROM ::posts AS p
  404. INNER JOIN ::posts AS j ON (j.topic_id=?i:tid AND j.id=?i:pid)
  405. WHERE p.topic_id=?i:tid AND p.id<?i:pid'; //???? может на два запроса разбить?
  406. $result = $this->c->DB->query($query, $vars)->fetch();
  407. $this->page = empty($result) ? null : (int) \ceil(($result['num'] + 1) / $this->c->user->disp_posts);
  408. }
  409. /**
  410. * Статус показа/подсчета просмотров темы
  411. *
  412. * @return bool
  413. */
  414. protected function getshowViews(): bool
  415. {
  416. return '1' == $this->c->config->o_topic_views;
  417. }
  418. /**
  419. * Увеличивает на 1 количество просмотров темы
  420. */
  421. public function incViews(): void
  422. {
  423. $vars = [
  424. ':tid' => $this->id,
  425. ];
  426. $query = 'UPDATE ::topics
  427. SET num_views=num_views+1
  428. WHERE id=?i:tid';
  429. $this->c->DB->exec($query, $vars);
  430. }
  431. /**
  432. * Обновление меток последнего визита и последнего прочитанного сообщения
  433. */
  434. public function updateVisits(): void
  435. {
  436. if ($this->c->user->isGuest) {
  437. return;
  438. }
  439. $vars = [
  440. ':uid' => $this->c->user->id,
  441. ':tid' => $this->id,
  442. ':read' => (int) $this->mt_last_read,
  443. ':visit' => (int) $this->mt_last_visit,
  444. ];
  445. $flag = false;
  446. if (false !== $this->hasNew) {
  447. $flag = true;
  448. $vars[':visit'] = $this->last_post;
  449. }
  450. if (
  451. false !== $this->hasUnread
  452. && $this->timeMax > $this->hasUnread
  453. ) {
  454. $flag = true;
  455. $vars[':read'] = $this->timeMax;
  456. $vars[':visit'] = $this->last_post;
  457. }
  458. if ($flag) {
  459. if (
  460. empty($this->mt_last_read)
  461. && empty($this->mt_last_visit)
  462. ) {
  463. $query = 'INSERT INTO ::mark_of_topic (uid, tid, mt_last_visit, mt_last_read)
  464. SELECT ?i:uid, ?i:tid, ?i:visit, ?i:read
  465. FROM ::groups
  466. WHERE NOT EXISTS (
  467. SELECT 1
  468. FROM ::mark_of_topic
  469. WHERE uid=?i:uid AND tid=?i:tid
  470. )
  471. LIMIT 1';
  472. } else {
  473. $query = 'UPDATE ::mark_of_topic
  474. SET mt_last_visit=?i:visit, mt_last_read=?i:read
  475. WHERE uid=?i:uid AND tid=?i:tid';
  476. }
  477. $this->c->DB->exec($query, $vars);
  478. }
  479. }
  480. }