Poll.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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\Poll;
  10. use ForkBB\Core\Container;
  11. use ForkBB\Models\DataModel;
  12. use ForkBB\Models\Topic\Topic;
  13. use PDO;
  14. use RuntimeException;
  15. use function \ForkBB\__;
  16. class Poll extends DataModel
  17. {
  18. const JSON_OPTIONS = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR;
  19. /**
  20. * Ключ модели для контейнера
  21. * @var string
  22. */
  23. protected $cKey = 'Poll';
  24. /**
  25. * Возвращает родительскую тему
  26. */
  27. protected function getparent(): Topic
  28. {
  29. if ($this->tid < 1) {
  30. throw new RuntimeException('Parent is not defined');
  31. }
  32. $topic = $this->c->topics->get($this->tid);
  33. if (! $topic instanceof Topic) {
  34. throw new RuntimeException('Parent not found');
  35. }
  36. return $topic;
  37. }
  38. /**
  39. * Устанавливает родительскую тему
  40. */
  41. protected function setparent(Topic $topic): void
  42. {
  43. if ($topic->id < 1) {
  44. throw new RuntimeException('Parent has a bad id');
  45. }
  46. if (
  47. $this->tid > 0
  48. && $this->tid !== $topic->id
  49. ) {
  50. throw new RuntimeException('Alien parent');
  51. }
  52. $this->tid = $topic->id;
  53. }
  54. /**
  55. * Ссылка на обработчик опроса
  56. */
  57. protected function getlink(): ?string
  58. {
  59. if ($this->tid > 0) {
  60. return $this->c->Router->link('Poll', ['tid' => $this->tid]);
  61. } else {
  62. return null;
  63. }
  64. }
  65. /**
  66. * Значение токена для формы голосования
  67. */
  68. protected function gettoken(): ?string
  69. {
  70. if ($this->tid > 0) {
  71. return $this->c->Csrf->create('Poll', ['tid' => $this->tid]);
  72. } else {
  73. return null;
  74. }
  75. }
  76. /**
  77. * Статус голосования для текущего пользователя
  78. */
  79. protected function getuserVoted(): bool
  80. {
  81. if (
  82. $this->c->user->isGuest
  83. || $this->tid < 1
  84. ) {
  85. return false;
  86. }
  87. $vars = [
  88. ':tid' => $this->tid,
  89. ':uid' => $this->c->user->id,
  90. ];
  91. $query = 'SELECT 1
  92. FROM ::poll_voted
  93. WHERE tid=?i:tid AND uid=?i:uid';
  94. return ! empty($this->c->DB->query($query, $vars)->fetch());
  95. }
  96. /**
  97. * Статус открытости
  98. */
  99. protected function getisOpen(): bool
  100. {
  101. return $this->tid < 1
  102. || (
  103. 0 === $this->parent->closed
  104. && ($type = $this->parent->poll_type) > 0
  105. && (
  106. 1 === $type
  107. || (
  108. $type > 1000
  109. && ($type - 1000) * 86400 > \time() - $this->parent->poll_time
  110. )
  111. )
  112. );
  113. }
  114. /**
  115. * Статус возможности голосовать
  116. */
  117. protected function getcanVote(): bool
  118. {
  119. return $this->tid > 0
  120. && $this->c->user->usePoll
  121. && $this->isOpen
  122. && ! $this->userVoted;
  123. }
  124. /**
  125. * Статус возможности видеть результат
  126. */
  127. protected function getcanSeeResult(): bool
  128. {
  129. return (
  130. $this->c->user->usePoll
  131. || 1 === $this->c->config->b_poll_guest
  132. )
  133. && (
  134. 0 === $this->parent->poll_term
  135. || $this->parent->poll_term < \max($this->total)
  136. || ! $this->isOpen
  137. );
  138. }
  139. /**
  140. * Статус возможности редактировать опрос
  141. */
  142. protected function getcanEdit(): bool
  143. {
  144. return $this->c->user->usePoll
  145. && (
  146. 0 === $this->c->config->i_poll_time
  147. || $this->tid < 1
  148. || $this->c->user->isAdmin
  149. || $this->c->user->isModerator($this)
  150. || 60 * $this->c->config->i_poll_time > \time() - $this->parent->poll_time
  151. );
  152. }
  153. /**
  154. * Возвращает максимум голосов за один ответ по каждому вопросу
  155. */
  156. protected function getmaxVote(): array
  157. {
  158. $result = [];
  159. foreach (\array_keys($this->question) as $q) {
  160. $result[$q] = \min(\max($this->vote[$q]), $this->total[$q]);
  161. }
  162. return $result;
  163. }
  164. /**
  165. * Возвращает процент голосов по каждому ответу кажого вопроса
  166. */
  167. protected function getpercVote(): array
  168. {
  169. $result = [];
  170. foreach (\array_keys($this->question) as $q) {
  171. if ($this->total[$q] > 0) {
  172. $total = $this->total[$q] / 100;
  173. foreach (\array_keys($this->answer[$q]) as $a) {
  174. $result[$q][$a] = \min(100, \round($this->vote[$q][$a] / $total, 2));
  175. }
  176. } else {
  177. foreach (\array_keys($this->answer[$q]) as $a) {
  178. $result[$q][$a] = 0;
  179. }
  180. }
  181. }
  182. return $result;
  183. }
  184. /**
  185. * Возвращает ширину ответа в процентах
  186. */
  187. protected function getwidthVote(): array
  188. {
  189. $result = [];
  190. foreach (\array_keys($this->question) as $q) {
  191. if ($this->maxVote[$q] > 0) {
  192. $max = $this->maxVote[$q] / 100;
  193. foreach (\array_keys($this->answer[$q]) as $a) {
  194. $result[$q][$a] = \min(100, \round($this->vote[$q][$a] / $max));
  195. }
  196. } else {
  197. foreach (\array_keys($this->answer[$q]) as $a) {
  198. $result[$q][$a] = 0;
  199. }
  200. }
  201. }
  202. return $result;
  203. }
  204. /**
  205. * Возвращает статус опроса для текущего пользователя или null
  206. */
  207. protected function getstatus() /* : null|string|array */
  208. {
  209. if ($this->tid < 1) {
  210. return null;
  211. } elseif (
  212. $this->c->user->isGuest
  213. && 1 !== $this->c->config->b_poll_guest
  214. ) {
  215. return 'Poll results are hidden from the guests';
  216. } elseif (! $this->isOpen) {
  217. return 'This poll is closed';
  218. } elseif (! $this->canSeeResult) {
  219. return ['Poll results are hidden up to %s voters', $this->parent->poll_term];
  220. } elseif ($this->userVoted) {
  221. return 'You voted';
  222. } elseif ($this->c->user->isGuest) {
  223. return 'Guest cannot vote';
  224. } else {
  225. return 'Poll status is undefined';
  226. }
  227. }
  228. /**
  229. * Голосование
  230. * Возвращает текст ошибки или null
  231. */
  232. public function vote(array $vote): ?string
  233. {
  234. if (! $this->canVote) {
  235. return __('You cannot vote on this poll');
  236. }
  237. $data = [];
  238. foreach (\array_keys($this->question) as $q) {
  239. if ($this->type[$q] > 1) {
  240. $count = \count($vote[$q]);
  241. if (0 == $count) {
  242. return __(['No vote on question %s', $q]);
  243. } elseif ($count > $this->type[$q]) {
  244. return __(['Too many answers selected in question %s', $q]);
  245. }
  246. foreach (\array_keys($vote[$q]) as $a) {
  247. if (! isset($this->answer[$q][$a])) {
  248. return __(['The selected answer is not present in question %s', $q]);
  249. }
  250. $data[] = [$q, $a];
  251. }
  252. } else {
  253. if (! isset($vote[$q][0])) {
  254. return __(['No vote on question %s', $q]);
  255. } elseif (! isset($this->answer[$q][$vote[$q][0]])) {
  256. return __(['The selected answer is not present in question %s', $q]);
  257. }
  258. $data[] = [$q, $vote[$q][0]];
  259. }
  260. $data[] = [$q, 0];
  261. }
  262. $vars = [
  263. ':tid' => $this->tid,
  264. ':uid' => $this->c->user->id,
  265. ':rez' => \json_encode($data, self::JSON_OPTIONS),
  266. ];
  267. $query = 'INSERT INTO ::poll_voted (tid, uid, rez)
  268. VALUES (?i:tid, ?i:uid, ?s:rez)';
  269. $this->c->DB->exec($query, $vars);
  270. $vars = [
  271. ':tid' => $this->tid,
  272. ];
  273. $query = 'UPDATE ::poll
  274. SET votes=votes+1
  275. WHERE tid=?i:tid AND question_id=?i:qid AND field_id=?i:fid';
  276. foreach ($data as list($vars[':qid'], $vars[':fid'])) {
  277. $this->c->DB->exec($query, $vars);
  278. }
  279. $this->c->polls->reset($this->tid);
  280. return null;
  281. }
  282. }