hashover.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <?php
  2. // Copyright (C) 2015-2017 Jacob Barkdull
  3. // This file is part of HashOver.
  4. //
  5. // HashOver is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Affero General Public License as
  7. // published by the Free Software Foundation, either version 3 of the
  8. // License, or (at your option) any later version.
  9. //
  10. // HashOver is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Affero General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Affero General Public License
  16. // along with HashOver. If not, see <http://www.gnu.org/licenses/>.
  17. // Display source code
  18. if (basename ($_SERVER['PHP_SELF']) === basename (__FILE__)) {
  19. if (isset ($_GET['source'])) {
  20. header ('Content-type: text/plain; charset=UTF-8');
  21. exit (file_get_contents (basename (__FILE__)));
  22. } else {
  23. exit ('<b>HashOver</b>: This is a class file.');
  24. }
  25. }
  26. class HashOver
  27. {
  28. public $usage = array ();
  29. public $statistics;
  30. public $misc;
  31. public $setup;
  32. public $readComments;
  33. public $locale;
  34. public $commentParser;
  35. public $markdown;
  36. public $cookies;
  37. public $commentCount;
  38. public $popularList = array ();
  39. public $rawComments = array ();
  40. public $comments = array ();
  41. public $html;
  42. public $templater;
  43. public function __construct ($mode = 'php', $context = 'normal')
  44. {
  45. // Store usage context information
  46. $this->usage['mode'] = $mode;
  47. $this->usage['context'] = $context;
  48. // Instantiate and start statistics
  49. $this->statistics = new HashOver\Statistics ($mode);
  50. $this->statistics->executionStart ();
  51. // Instantiate general setup class
  52. $this->setup = new HashOver\Setup ($this->usage);
  53. // Instantiate class of miscellaneous functions
  54. $this->misc = new HashOver\Misc ($mode);
  55. }
  56. public function getCommentCount ($locale_key = 'showing-comments')
  57. {
  58. // Shorter variables
  59. $primary_count = $this->readComments->primaryCount;
  60. $total_count = $this->readComments->totalCount;
  61. // Subtract deleted comment counts
  62. if ($this->setup->countIncludesDeleted === false) {
  63. $primary_count -= $this->readComments->primaryDeletedCount;
  64. $total_count -= $this->readComments->totalDeletedCount;
  65. }
  66. // Decide which locale to use; Exclude "Showing" in API usages
  67. $locale_key = ($this->usage['context'] === 'api') ? 'count-link' : $locale_key;
  68. // Decide if comment count is pluralized
  69. $prime_plural = ($primary_count !== 2) ? 1 : 0;
  70. // Get appropriate locale
  71. $showing_comments_locale = $this->locale->get ($locale_key);
  72. $showing_comments = $showing_comments_locale[$prime_plural];
  73. // Whether to show reply count separately
  74. if ($this->setup->showsReplyCount === true) {
  75. // If so, inject top level comment count into count locale string
  76. $comment_count = sprintf ($showing_comments, $primary_count - 1);
  77. // Check if there are any replies
  78. if ($total_count !== $primary_count) {
  79. // If so, decide if reply count is pluralized
  80. $count_diff = $total_count - $primary_count;
  81. $reply_plural = ($count_diff !== 1) ? 1 : 0;
  82. // Get appropriate locale
  83. $reply_locale = $this->locale->text['count-replies'][$reply_plural];
  84. // Inject total comment count into reply count locale string
  85. $reply_count = sprintf ($reply_locale, $total_count - 1);
  86. // And append reply count
  87. $comment_count .= ' (' . $reply_count . ')';
  88. }
  89. // And return count with separate reply count
  90. return $comment_count;
  91. }
  92. // Otherwise inject total comment count into count locale string
  93. return sprintf ($showing_comments, $total_count - 1);
  94. }
  95. // Begin initialization work
  96. public function initiate ()
  97. {
  98. // Instantiate class for reading comments
  99. $this->readComments = new HashOver\ReadComments ($this->setup);
  100. // Where to stop reading comments
  101. if ($this->usage['mode'] === 'javascript'
  102. and $this->setup->collapsesComments !== false
  103. and $this->setup->popularityLimit <= 0
  104. and $this->setup->usesAJAX !== false)
  105. {
  106. // Use collapse limit when collapsing and AJAX is enabled
  107. $end = $this->setup->collapseLimit;
  108. } else {
  109. // Otherwise read all comments
  110. $end = null;
  111. }
  112. // TODO: Fix structure when using starting point
  113. $this->rawComments = $this->readComments->read (0, $end);
  114. // Instantiate locales class
  115. $this->locale = new HashOver\Locale ($this->setup);
  116. // Instantiate cookies class
  117. $this->cookies = new HashOver\Cookies ($this->setup);
  118. // Instantiate login class
  119. $this->login = new HashOver\Login ($this->setup);
  120. // Instantiate comment parser class
  121. $this->commentParser = new HashOver\CommentParser ($this->setup);
  122. // Generate comment count
  123. $this->commentCount = $this->getCommentCount ();
  124. // Instantiate markdown class
  125. $this->markdown = new HashOver\Markdown ();
  126. }
  127. // Get reply array from comments via key
  128. protected function &getRepliesLevel (&$level, $level_count, &$key_parts)
  129. {
  130. for ($i = 1; $i < $level_count; $i++) {
  131. if (!isset ($level)) {
  132. break;
  133. }
  134. $level =& $level['replies'][$key_parts[$i] - 1];
  135. }
  136. return $level;
  137. }
  138. // Parse primary comments
  139. public function parsePrimary ($start = 0)
  140. {
  141. // Initial comments array
  142. $this->comments['comments'] = array ();
  143. // If no comments were found, setup a default message comment
  144. if ($this->readComments->totalCount <= 1) {
  145. $this->comments['comments'][] = array (
  146. 'title' => $this->locale->text['be-first-name'],
  147. 'avatar' => $this->setup->httpImages . '/first-comment.' . $this->setup->imageFormat,
  148. 'permalink' => 'c1',
  149. 'notice' => $this->locale->text['be-first-note'],
  150. 'notice-class' => 'hashover-first'
  151. );
  152. return;
  153. }
  154. // Last existing comment date for sorting deleted comments
  155. $last_date = 0;
  156. // Allowed comment count
  157. $allowed_count = 0;
  158. // Where to stop reading comments
  159. if ($this->usage['mode'] === 'javascript'
  160. and $this->setup->collapsesComments !== false
  161. and $this->setup->usesAJAX !== false)
  162. {
  163. // Use collapse limit when collapsing and AJAX is enabled
  164. $end = $this->setup->collapseLimit;
  165. } else {
  166. // Otherwise read all comments
  167. $end = null;
  168. }
  169. // Run all comments through parser
  170. foreach ($this->rawComments as $key => $comment) {
  171. $key_parts = explode ('-', $key);
  172. $indentions = count ($key_parts);
  173. $status = 'approved';
  174. if ($this->setup->popularityLimit > 0) {
  175. $popularity = 0;
  176. // Add number of likes to popularity value
  177. if (!empty ($comment['likes'])) {
  178. $popularity += $comment['likes'];
  179. }
  180. // Subtract number of dislikes to popularity value
  181. if ($this->setup->allowsDislikes === true) {
  182. if (!empty ($comment['dislikes'])) {
  183. $popularity -= $comment['dislikes'];
  184. }
  185. }
  186. // Add comment to popular comments list if popular enough
  187. if ($popularity >= $this->setup->popularityThreshold) {
  188. $this->popularList[] = array (
  189. 'popularity' => $popularity,
  190. 'key' => $key,
  191. 'parts' => $key_parts
  192. );
  193. }
  194. }
  195. // Stop parsing after end point
  196. if ($end !== null and $allowed_count >= $end) {
  197. continue;
  198. }
  199. if ($indentions > 1 and $this->setup->streamDepth > 0) {
  200. $level =& $this->comments['comments'][$key_parts[0] - 1];
  201. if ($this->setup->replyMode === 'stream'
  202. and $indentions > $this->setup->streamDepth)
  203. {
  204. $level =& $this->getRepliesLevel ($level, $this->setup->streamDepth, $key_parts);
  205. $level =& $level['replies'][];
  206. } else {
  207. $level =& $this->getRepliesLevel ($level, $indentions, $key_parts);
  208. }
  209. } else {
  210. $level =& $this->comments['comments'][];
  211. }
  212. // Set status to what's stored in the comment
  213. if (!empty ($comment['status'])) {
  214. $status = $comment['status'];
  215. }
  216. switch ($status) {
  217. // Parse as pending notice, viewable and editable by owner and admin
  218. case 'pending': {
  219. $parsed = $this->commentParser->parse ($comment, $key, $key_parts, false);
  220. if (!isset ($parsed['user-owned'])) {
  221. $level = $this->commentParser->notice ('pending', $key, $last_date);
  222. break;
  223. }
  224. $last_date = $parsed['sort-date'];
  225. $level = $parsed;
  226. break;
  227. }
  228. // Parse as deletion notice, viewable and editable by admin
  229. case 'deleted': {
  230. if ($this->login->userIsAdmin === true) {
  231. $level = $this->commentParser->parse ($comment, $key, $key_parts, false);
  232. $last_date = $level['sort-date'];
  233. } else {
  234. $level = $this->commentParser->notice ('deleted', $key, $last_date);
  235. }
  236. break;
  237. }
  238. // Parse as deletion notice, non-existent comment
  239. case 'missing': {
  240. $level = $this->commentParser->notice ('deleted', $key, $last_date);
  241. break;
  242. }
  243. // Parse as an unknown/error notice
  244. case 'read-error': {
  245. $level = $this->commentParser->notice ('error', $key, $last_date);
  246. break;
  247. }
  248. // Otherwise parse comment normally
  249. default: {
  250. $comment['status'] = 'approved';
  251. $level = $this->commentParser->parse ($comment, $key, $key_parts);
  252. $last_date = $level['sort-date'];
  253. break;
  254. }
  255. }
  256. $allowed_count++;
  257. }
  258. // Reset array keys
  259. $this->comments['comments'] = array_values ($this->comments['comments']);
  260. }
  261. // Parse popular comments
  262. public function parsePopular ()
  263. {
  264. // Initial popular comments array
  265. $this->comments['popularComments'] = array ();
  266. // If no comments or popularity limit is 0, return void
  267. if ($this->readComments->totalCount <= 1
  268. or $this->setup->popularityLimit <= 0)
  269. {
  270. return;
  271. }
  272. // Sort popular comments
  273. usort ($this->popularList, function ($a, $b) {
  274. return ($b['popularity'] > $a['popularity']);
  275. });
  276. // Calculate how many popular comments will be shown
  277. $limit = $this->setup->popularityLimit;
  278. $count = min ($limit, count ($this->popularList));
  279. // Read, parse, and add popular comments to output
  280. for ($i = 0; $i < $count; $i++) {
  281. $item =& $this->popularList[$i];
  282. $comment = $this->readComments->data->read ($item['key']);
  283. $parsed = $this->commentParser->parse ($comment, $item['key'], $item['parts'], true);
  284. $this->comments['popularComments'][$i] = $parsed;
  285. }
  286. }
  287. // Do final initialization work
  288. public function finalize ()
  289. {
  290. // Expire various temporary cookies
  291. $this->cookies->clear ();
  292. // Various comment count numbers
  293. $commentCounts = array (
  294. 'show-count' => $this->commentCount,
  295. 'primary' => $this->readComments->primaryCount,
  296. 'total' => $this->readComments->totalCount,
  297. );
  298. // Instantiate HTML output class
  299. $this->html = new HashOver\HTMLOutput (
  300. $this->setup,
  301. $commentCounts
  302. );
  303. // Instantiate comment theme templater class
  304. $this->templater = new HashOver\Templater (
  305. $this->usage['mode'],
  306. $this->setup
  307. );
  308. }
  309. // Display all comments as HTML
  310. public function displayComments ()
  311. {
  312. // Instantiate PHP mode class
  313. $phpmode = new HashOver\PHPMode (
  314. $this->setup,
  315. $this->html,
  316. $this->comments
  317. );
  318. // Run popular comments through parser
  319. if (!empty ($this->comments['popularComments'])) {
  320. foreach ($this->comments['popularComments'] as $comment) {
  321. $this->html->popularComments .= $phpmode->parseComment ($comment, null, true) . PHP_EOL;
  322. }
  323. }
  324. // Run primary comments through parser
  325. if (!empty ($this->comments['comments'])) {
  326. foreach ($this->comments['comments'] as $comment) {
  327. $this->html->comments .= $phpmode->parseComment ($comment, null) . PHP_EOL;
  328. }
  329. }
  330. // Start HTML output with initial HTML
  331. $html = $this->html->initialHTML ($this->popularList);
  332. // End statistics and add them as code comment
  333. $html .= $this->statistics->executionEnd ();
  334. // Return final HTML
  335. return $html;
  336. }
  337. }