PM.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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\Model;
  13. use ForkBB\Models\PM\Cnst;
  14. use ForkBB\Models\PM\PBlock;
  15. use ForkBB\Models\PM\PPost;
  16. use ForkBB\Models\PM\PTopic;
  17. use ForkBB\Models\User\User;
  18. use InvalidArgumentException;
  19. use RuntimeException;
  20. class PM extends Model
  21. {
  22. /**
  23. * Ключ модели для контейнера
  24. * @var string
  25. */
  26. protected $cKey = 'PMS';
  27. /**
  28. * @var array
  29. */
  30. protected $repository;
  31. public function __construct(Container $container)
  32. {
  33. parent::__construct($container);
  34. $this->zDepend = [
  35. 'area' => ['numPages', 'pagination'],
  36. 'page' => ['pagination'],
  37. ];
  38. $this->repository = [
  39. Cnst::PTOPIC => [],
  40. Cnst::PPOST => [],
  41. ];
  42. }
  43. protected function checkType(int $type, DataModel $model = null): void
  44. {
  45. switch ($type) {
  46. case Cnst::PTOPIC:
  47. if (
  48. null === $model
  49. || $model instanceof PTopic
  50. ) {
  51. return;
  52. }
  53. break;
  54. case Cnst::PPOST:
  55. if (
  56. null === $model
  57. || $model instanceof PPost
  58. ) {
  59. return;
  60. }
  61. break;
  62. }
  63. throw new InvalidArgumentException("Wrong type: {$type}");
  64. }
  65. public function get(int $type, int $key): ?DataModel
  66. {
  67. $this->checkType($type);
  68. return $this->repository[$type][$key] ?? null;
  69. }
  70. public function set(int $type, int $key, /* mixed */ $value): self
  71. {
  72. $this->checkType($type);
  73. $this->repository[$type][$key] = $value;
  74. return $this;
  75. }
  76. public function isset(int $type, int $key): bool
  77. {
  78. $this->checkType($type);
  79. return \array_key_exists($key, $this->repository[$type]);
  80. }
  81. public function create(int $type, array $attrs = []): DataModel
  82. {
  83. switch ($type) {
  84. case Cnst::PTOPIC:
  85. return $this->c->PTopicModel->setAttrs($attrs);
  86. case Cnst::PPOST:
  87. return $this->c->PPostModel->setAttrs($attrs);
  88. default:
  89. throw new InvalidArgumentException("Wrong type: {$type}");
  90. }
  91. }
  92. public function accessTopic(int $id): bool
  93. {
  94. return isset($this->idsCurrent[$id]) || isset($this->idsArchive[$id]);
  95. }
  96. public function inArea(PTopic $topic): ?string
  97. {
  98. if (isset($this->idsArchive[$topic->id])) {
  99. return Cnst::ACTION_ARCHIVE;
  100. } elseif (isset($this->idsNew[$topic->id])) {
  101. return Cnst::ACTION_NEW;
  102. } elseif (isset($this->idsCurrent[$topic->id])) {
  103. return Cnst::ACTION_CURRENT;
  104. } else {
  105. return null;
  106. }
  107. }
  108. public function load(int $type, int $id): ?DataModel
  109. {
  110. $this->checkType($type);
  111. if ($this->isset($type, $id)) {
  112. return $this->get($type, $id);
  113. }
  114. switch ($type) {
  115. case Cnst::PTOPIC:
  116. if ($this->accessTopic($id)) {
  117. $model = $this->Load->load($type, $id);
  118. } else {
  119. $model = null;
  120. }
  121. break;
  122. case Cnst::PPOST:
  123. $model = $this->Load->load($type, $id);
  124. if (
  125. $model instanceof PPost
  126. && ! $this->accessTopic($model->topic_id)
  127. ) {
  128. $model = null;
  129. }
  130. break;
  131. }
  132. $this->set($type, $id, $model);
  133. return $model;
  134. }
  135. public function loadByIds(int $type, array $ids): array
  136. {
  137. $this->checkType($type);
  138. $result = [];
  139. $data = [];
  140. foreach ($ids as $id) {
  141. if ($this->isset($type, $id)) {
  142. $result[$id] = $this->get($type, $id);
  143. } else {
  144. switch ($type) {
  145. case Cnst::PTOPIC:
  146. if (! $this->accessTopic($id)) {
  147. break;
  148. }
  149. default:
  150. $data[] = $id;
  151. }
  152. $result[$id] = null;
  153. $this->set($type, $id, null);
  154. }
  155. }
  156. if (empty($data)) {
  157. return $result;
  158. }
  159. foreach ($this->Load->loadByIds($type, $data) as $model) {
  160. if ($model instanceof PPost) {
  161. if (! $this->accessTopic($model->topic_id)) {
  162. continue;
  163. }
  164. } elseif (! $model instanceof PTopic) {
  165. continue;
  166. }
  167. $result[$model->id] = $model;
  168. $this->set($type, $model->id, $model);
  169. }
  170. return $result;
  171. }
  172. public function update(int $type, DataModel $model): DataModel
  173. {
  174. $this->checkType($type, $model);
  175. return $this->Save->update($model);
  176. }
  177. public function insert(int $type, DataModel $model): int
  178. {
  179. $this->checkType($type, $model);
  180. $id = $this->Save->insert($model);
  181. $this->set($type, $id, $model);
  182. return $id;
  183. }
  184. /**
  185. * Инициализирует массивы индексов приватных тем текущего пользователя
  186. * Инициализирует число приватных тем без фильтра
  187. * Может использовать фильтр по второму пользователю: id или "username" (именно в кавычках)
  188. */
  189. public function init(/* null|int|string */ $second = null): self
  190. {
  191. list(
  192. $this->idsNew,
  193. $this->idsCurrent,
  194. $this->idsArchive,
  195. $this->totalNew,
  196. $this->totalCurrent,
  197. $this->totalArchive
  198. ) = $this->infoForUser($this->c->user, $second);
  199. $this->second = $second;
  200. $this->numNew = \count($this->idsNew);
  201. $this->numCurrent = \count($this->idsCurrent);
  202. $this->numArchive = \count($this->idsArchive);
  203. return $this;
  204. }
  205. /**
  206. * Возвращает данные по приватным темам (индексы) любого пользователя
  207. */
  208. public function infoForUser(User $user, /* null|int|string */ $second = null): array
  209. {
  210. // deleted // pt_status = PT_DELETED
  211. // unsent // pt_status = PT_NOTSENT
  212. $idsNew = []; // pt_status = PT_NORMAL and last_post > ..._visit
  213. $idsCur = []; // pt_status = PT_NORMAL or last_post > ..._visit
  214. $idsArc = []; // pt_status = PT_ARCHIVE
  215. $totalNew = 0;
  216. $totalCur = 0;
  217. $totalArc = 0;
  218. if (
  219. $user->isGuest
  220. || $user->isUnverified
  221. ) {
  222. return [$idsNew, $idsCur, $idsArc, $totalNew, $totalCur, $totalArc];
  223. }
  224. $vars = [
  225. ':id' => $user->id,
  226. ':norm' => Cnst::PT_NORMAL,
  227. ':arch' => Cnst::PT_ARCHIVE,
  228. ];
  229. $query = 'SELECT pt.poster, pt.poster_id, pt.poster_status, pt.poster_visit,
  230. pt.target, pt.target_id, pt.target_status, pt.target_visit,
  231. pt.id, pt.last_post
  232. FROM ::pm_topics AS pt
  233. WHERE (pt.poster_id=?i:id AND pt.poster_status=?i:norm)
  234. OR (pt.poster_id=?i:id AND pt.poster_status=?i:arch)
  235. OR (pt.target_id=?i:id AND pt.target_status=?i:norm)
  236. OR (pt.target_id=?i:id AND pt.target_status=?i:arch)
  237. ORDER BY pt.last_post DESC';
  238. $stmt = $this->c->DB->query($query, $vars);
  239. while ($row = $stmt->fetch()) {
  240. $id = $row['id'];
  241. $lp = $row['last_post'];
  242. if ($row['poster_id'] === $user->id) {
  243. switch ($row['poster_status']) {
  244. case Cnst::PT_ARCHIVE:
  245. if (
  246. null === $second
  247. || $row['target_id'] === $second
  248. || '"' . $row['target'] . '"' === $second
  249. ) {
  250. $idsArc[$id] = $lp;
  251. }
  252. ++$totalArc;
  253. break;
  254. case Cnst::PT_NORMAL:
  255. if (
  256. null === $second
  257. || $row['target_id'] === $second
  258. || '"' . $row['target'] . '"' === $second
  259. ) {
  260. if ($lp > $row['poster_visit']) {
  261. $idsNew[$id] = $lp;
  262. }
  263. $idsCur[$id] = $lp;
  264. }
  265. if ($lp > $row['poster_visit']) {
  266. ++$totalNew;
  267. }
  268. ++$totalCur;
  269. break;
  270. }
  271. } elseif ($row['target_id'] === $user->id) {
  272. switch ($row['target_status']) {
  273. case Cnst::PT_ARCHIVE:
  274. if (
  275. null === $second
  276. || $row['poster_id'] === $second
  277. || '"' . $row['poster'] . '"' === $second
  278. ) {
  279. $idsArc[$id] = $lp;
  280. }
  281. ++$totalArc;
  282. break;
  283. case Cnst::PT_NORMAL:
  284. if (
  285. null === $second
  286. || $row['poster_id'] === $second
  287. || '"' . $row['poster'] . '"' === $second
  288. ) {
  289. if ($lp > $row['target_visit']) {
  290. $idsNew[$id] = $lp;
  291. }
  292. $idsCur[$id] = $lp;
  293. }
  294. if ($lp > $row['target_visit']) {
  295. ++$totalNew;
  296. }
  297. ++$totalCur;
  298. break;
  299. }
  300. }
  301. }
  302. return [$idsNew, $idsCur, $idsArc, $totalNew, $totalCur, $totalArc];
  303. }
  304. /**
  305. * Возвращает список приватных тем в зависимости от активной папки
  306. * Номер темы в индексе, а не в значении
  307. */
  308. protected function idsList(): array
  309. {
  310. switch ($this->area) {
  311. case Cnst::ACTION_NEW:
  312. $list = $this->idsNew;
  313. break;
  314. case Cnst::ACTION_ARCHIVE:
  315. $list = $this->idsArchive;
  316. break;
  317. default:
  318. $list = $this->idsCurrent;
  319. }
  320. if (\is_array($list)) {
  321. return $list;
  322. }
  323. throw new RuntimeException('Init() method was not executed');
  324. }
  325. /**
  326. * $this->area = ...
  327. */
  328. protected function setarea(string $area): self
  329. {
  330. switch ($area) {
  331. case Cnst::ACTION_NEW:
  332. case Cnst::ACTION_CURRENT:
  333. case Cnst::ACTION_ARCHIVE:
  334. break;
  335. default:
  336. $area = Cnst::ACTION_CURRENT;
  337. }
  338. $this->setAttr('area', $area);
  339. return $this;
  340. }
  341. /**
  342. * ... = $this->numPages;
  343. */
  344. protected function getnumPages(): int
  345. {
  346. return (int) \ceil((\count($this->idsList()) ?: 1) / $this->c->user->disp_topics);
  347. }
  348. /**
  349. * Статус наличия установленной страницы
  350. */
  351. public function hasPage(): bool
  352. {
  353. return \is_int($this->page) && $this->page > 0 && $this->page <= $this->numPages;
  354. }
  355. /**
  356. * ... = $this->pagination;
  357. */
  358. protected function getpagination(): array
  359. {
  360. return $this->c->Func->paginate(
  361. $this->numPages,
  362. $this->page,
  363. 'PMAction',
  364. [
  365. 'second' => $this->second,
  366. 'action' => $this->area,
  367. 'page' => 'more1', // нестандарная переменная для page
  368. ]
  369. );
  370. }
  371. /**
  372. * Возвращает массив приватных тем с установленной страницы
  373. */
  374. public function pmListCurPage(): array
  375. {
  376. if (! $this->hasPage()) {
  377. throw new InvalidArgumentException('Bad number of displayed page');
  378. }
  379. $ids = \array_slice(
  380. $this->idsList(),
  381. ($this->page - 1) * $this->c->user->disp_topics,
  382. $this->c->user->disp_topics,
  383. true
  384. );
  385. return $this->loadByIds(Cnst::PTOPIC, \array_keys($ids));
  386. }
  387. /**
  388. * Перечитывает данные приватных сообщений пользователя
  389. */
  390. public function recalculate(User $user): void
  391. {
  392. if ($user->isGuest) {
  393. return;
  394. }
  395. list($idsNew, $idsCurrent, $idsArchive, $new, $current, $archive) = $this->infoForUser($user);
  396. $user->u_pm_num_new = $new;
  397. $user->u_pm_num_all = $current;
  398. $this->c->users->update($user);
  399. }
  400. protected function getblock(): PBlock
  401. {
  402. return $this->c->PBlockModel;
  403. }
  404. protected function setblock(): void
  405. {
  406. throw new RuntimeException('Read-only block property');
  407. }
  408. }