database.php 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  1. <?php
  2. require_once 'config.php';
  3. class Database
  4. {
  5. /***** STATIC *****/
  6. // How many items to display in homepage
  7. const HOMEPAGE_RESULTS = 50;
  8. protected static function get_random_string ($length = 10) {
  9. $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
  10. $characters_length = strlen ($characters);
  11. $hash_id = '';
  12. for ($i = 0; $i < $length; $i++)
  13. $hash_id .= $characters[mt_rand (0, $characters_length - 1)];
  14. return $hash_id;
  15. }
  16. /***** INSTANCE *****/
  17. // Reference to a database connection
  18. protected $database;
  19. function __construct ()
  20. {
  21. $this->database = NULL;
  22. }
  23. function connect ()
  24. {
  25. try {
  26. $this->database = new PDO (
  27. Config::$DATABASE['dsn'],
  28. Config::$DATABASE['username'],
  29. Config::$DATABASE['password']);
  30. $this->database->setAttribute (
  31. PDO::ATTR_ERRMODE,
  32. PDO::ERRMODE_EXCEPTION);
  33. return true;
  34. } catch(PDOException $e) {
  35. return false;
  36. }
  37. }
  38. function disconnect ()
  39. {
  40. $this->database = NULL;
  41. }
  42. /**
  43. * Retrieve a user
  44. */
  45. function get_user ($username)
  46. {
  47. $user = NULL;
  48. if (is_null ($this->database))
  49. return $user;
  50. $query = $this->database->prepare (
  51. 'SELECT * ' .
  52. 'FROM `user` ' .
  53. 'WHERE `username` = ?');
  54. $query->execute (array ($username));
  55. $user = $query->fetch(PDO::FETCH_ASSOC);
  56. return $user;
  57. }
  58. /**
  59. * Check if user exists
  60. */
  61. function user_exists ($username)
  62. {
  63. $user = self::get_user ($username);
  64. return !is_null ($user) && !empty ($user);
  65. }
  66. /**
  67. * Check user login credentials
  68. *
  69. * @return NULL if bad credentials, otherwise return the user
  70. */
  71. function check_user_credentials ($username, $password)
  72. {
  73. try {
  74. $this->database->beginTransaction();
  75. $user = NULL;
  76. if (is_null ($this->database))
  77. return $user;
  78. $query = $this->database->prepare (
  79. 'SELECT * ' .
  80. 'FROM `user`' .
  81. 'WHERE ' .
  82. '`username` = ? AND ' .
  83. '`password` = SHA2(CONCAT(?, `salt`), 512) AND ' .
  84. '`isActive` = 1');
  85. $query->execute (array ($username, $password));
  86. $user = $query->fetch (PDO::FETCH_ASSOC);
  87. /* This bit of code is only needed to maintain interoperability
  88. * with the old version of freepost, such that users are not
  89. * required to create a new account.
  90. *
  91. * Once all hash/salt have been updated for all users, this code can
  92. * be safely removed.
  93. *
  94. * The old credentials are OK; update with a new
  95. * hash/salt to update users with the new freepost!
  96. */
  97. if (!is_null ($user) && !empty ($user) && array_key_exists ('salt', $user) && $user['salt'] == '')
  98. {
  99. // Create a salt for user password
  100. $salt = self::get_random_string (16);
  101. // Update hash/salt for the user
  102. $query = $this->database->prepare (
  103. 'UPDATE `user`' .
  104. 'SET `password` = SHA2(?, 512), `salt` = ? ' .
  105. 'WHERE `id` = ?');
  106. $query->execute (array ($password . $salt, $salt, $user['id']));
  107. // Refetch the user again
  108. $user = self::get_user ($username);
  109. }
  110. /////////////////////////////////////////////////////////////////////
  111. $this->database->commit ();
  112. return $user;
  113. } catch(PDOException $ex) {
  114. $this->database->rollBack();
  115. return NULL;
  116. }
  117. }
  118. /**
  119. * Retrieve a $user from database using remember_me token
  120. */
  121. function get_remember_me ($token)
  122. {
  123. $user = array();
  124. if (is_null ($this->database))
  125. return $user;
  126. $query = $this->database->prepare(
  127. 'SELECT U.* ' .
  128. 'FROM `user` AS U ' .
  129. 'JOIN `remember_me` AS R ON R.`userId` = U.`id`' .
  130. 'WHERE R.`token` = ? AND R.`expires` > NOW()');
  131. $query->execute (array (hash ('sha512', $token)));
  132. $user = $query->fetch (PDO::FETCH_ASSOC);
  133. return $user;
  134. }
  135. /**
  136. * Set a new remember_me token to database
  137. *
  138. * @return secret token (cleartext)
  139. */
  140. function set_remember_me ($user_id)
  141. {
  142. /* Set remember me token.
  143. * The cleartext token is stored as a user cookie, while in our
  144. * database we store hash(token).
  145. */
  146. // Delete all previous remember_me tokens for $user
  147. self::delete_remember_me ($user_id);
  148. // Create a new secret token
  149. $token = self::get_random_string (128);
  150. $query = $this->database->prepare (
  151. 'INSERT INTO `remember_me` (`token`, `userId`, `expires`)' .
  152. 'VALUES (?, ?, NOW() + INTERVAL 30 DAY)');
  153. $query->execute (array (hash('sha512', $token), $user_id));
  154. return $token;
  155. }
  156. /**
  157. * Delete $user "remember_me" token
  158. */
  159. function delete_remember_me ($user_id)
  160. {
  161. // Delete all previous remember_me tokens for $user
  162. $query = $this->database->prepare(
  163. 'DELETE FROM `remember_me`' .
  164. 'WHERE `userId` = ?');
  165. $query->execute (array ($user_id));
  166. }
  167. /**
  168. * Retrieve a post
  169. */
  170. function get_post ($hash_id)
  171. {
  172. $post = array();
  173. if (is_null($this->database))
  174. return $post;
  175. $query = $this->database->prepare(
  176. 'SELECT P.*, U.`username` ' .
  177. 'FROM `post` AS P ' .
  178. 'JOIN `user` AS U ON P.`userId` = U.`id` ' .
  179. 'WHERE P.`hashId` = ?');
  180. $query->execute (array ($hash_id));
  181. $post = $query->fetch(PDO::FETCH_ASSOC);
  182. return $post;
  183. }
  184. /**
  185. * Retrieve the user of a specific post
  186. */
  187. function get_post_op ($post_hash_id)
  188. {
  189. $user = array();
  190. if (is_null ($this->database))
  191. return $user;
  192. $query = $this->database->prepare(
  193. 'SELECT U.* ' .
  194. 'FROM `user` AS U ' .
  195. 'JOIN `post` AS P ON P.`userId` = U.`id` ' .
  196. 'WHERE P.`hashId` = ?');
  197. $query->execute (array ($post_hash_id));
  198. $user = $query->fetch (PDO::FETCH_ASSOC);
  199. return $user;
  200. }
  201. /**
  202. * Retrieve all comments for a specific post
  203. */
  204. function get_post_comments ($post_id)
  205. {
  206. $comments = array ();
  207. if (is_null ($this->database))
  208. return $comments;
  209. $query = $this->database->prepare (
  210. 'SELECT C.*, U.`username`' .
  211. 'FROM `comment` AS C ' .
  212. 'JOIN `user` AS U ON C.`userId` = U.`id`' .
  213. 'WHERE C.`postId` = ? ' .
  214. 'ORDER BY C.`vote` DESC, C.`created` ASC');
  215. $query->execute (array ($post_id));
  216. $comments = $query->fetchAll (PDO::FETCH_ASSOC);
  217. // Group comments by parentId
  218. $comments_group = array();
  219. foreach ($comments as $comment)
  220. if (is_null ($comment['parentId']))
  221. {
  222. $comments_group[0][] = $comment;
  223. }
  224. else
  225. {
  226. $comments_group[$comment['parentId']][] = $comment;
  227. }
  228. return $comments_group;
  229. }
  230. /**
  231. * Retrieve a single comment
  232. */
  233. function get_comment ($hash_id)
  234. {
  235. $comment = array ();
  236. if (is_null ($this->database))
  237. return $comment;
  238. $query = $this->database->prepare (
  239. 'SELECT ' .
  240. 'C.*,' .
  241. 'P.`hashId` AS `postHashId`,' .
  242. 'P.`title` AS `postTitle`,' .
  243. 'U.`username`' .
  244. 'FROM `comment` AS C ' .
  245. 'JOIN `user` AS U ON C.`userId` = U.`id` ' .
  246. 'JOIN `post` AS P ON P.`id` = C.`postId` ' .
  247. 'WHERE C.`hashId` = ?');
  248. $query->execute (array($hash_id));
  249. $comment = $query->fetch (PDO::FETCH_ASSOC);
  250. return $comment;
  251. }
  252. /**
  253. * Retrieve the user of a specific comment
  254. */
  255. function get_comment_op ($comment_hash_id)
  256. {
  257. $user = array();
  258. if (is_null ($this->database))
  259. return $user;
  260. $query = $this->database->prepare(
  261. 'SELECT U.* ' .
  262. 'FROM `user` AS U ' .
  263. 'JOIN `comment` AS C ON C.`userId` = U.`id` ' .
  264. 'WHERE C.`hashId` = ?');
  265. $query->execute (array ($comment_hash_id));
  266. $user = $query->fetch (PDO::FETCH_ASSOC);
  267. return $user;
  268. }
  269. /**
  270. * Return the number of unread messages
  271. */
  272. function count_unread_messages ($user_id)
  273. {
  274. if (is_null ($this->database))
  275. return 0;
  276. $query = $this->database->prepare (
  277. 'SELECT COUNT(1)' .
  278. 'FROM `comment`' .
  279. 'WHERE `parentUserId` = :user_id AND `userId` != :user_id AND `read` = 0');
  280. $query->execute (array ('user_id' => $user_id));
  281. return $query->fetchColumn();
  282. }
  283. /**
  284. * Get posts by rating (for homepage)
  285. */
  286. function get_hot_posts ($page = 0)
  287. {
  288. $submissions = array();
  289. $page = intval ($page);
  290. if (is_null ($this->database))
  291. return $submissions;
  292. if ($page < 0)
  293. $page = 0;
  294. $query = $this->database->prepare (
  295. 'SELECT P.*, U.`username`' .
  296. 'FROM `post` AS P ' .
  297. 'JOIN `user` AS U ON P.`userId` = U.`id`' .
  298. 'ORDER BY P.`dateCreated` DESC, P.`vote` DESC, P.`commentsCount` DESC ' .
  299. 'LIMIT :limit OFFSET :offset');
  300. $query->bindValue (':limit', Database::HOMEPAGE_RESULTS, PDO::PARAM_INT);
  301. $query->bindValue (':offset', $page * Database::HOMEPAGE_RESULTS, PDO::PARAM_INT);
  302. $query->execute ();
  303. $submissions = $query->fetchAll (PDO::FETCH_ASSOC);
  304. return $submissions;
  305. }
  306. /**
  307. * Get posts by date (for homepage)
  308. */
  309. function get_new_posts ($page = 0)
  310. {
  311. $submissions = array();
  312. $page = intval ($page);
  313. if (is_null ($this->database))
  314. return $submissions;
  315. if ($page < 0)
  316. $page = 0;
  317. $query = $this->database->prepare (
  318. 'SELECT P.*, U.`username`' .
  319. 'FROM `post` AS P ' .
  320. 'JOIN `user` AS U ON P.`userId` = U.`id`' .
  321. 'ORDER BY P.`created` DESC ' .
  322. 'LIMIT :limit OFFSET :offset');
  323. $query->bindValue (':limit', Database::HOMEPAGE_RESULTS, PDO::PARAM_INT);
  324. $query->bindValue (':offset', $page * Database::HOMEPAGE_RESULTS, PDO::PARAM_INT);
  325. $query->execute ();
  326. $submissions = $query->fetchAll (PDO::FETCH_ASSOC);
  327. return $submissions;
  328. }
  329. /**
  330. * Get user posts (used to show user activity)
  331. */
  332. function get_user_posts ($user_id)
  333. {
  334. $posts = array();
  335. if (is_null ($this->database))
  336. return $posts;
  337. $query = $this->database->prepare (
  338. 'SELECT * ' .
  339. 'FROM `post`' .
  340. 'WHERE `userId` = ? ' .
  341. 'ORDER BY `created` DESC ' .
  342. 'LIMIT 50');
  343. $query->execute (array($user_id));
  344. $posts = $query->fetchAll(PDO::FETCH_ASSOC);
  345. return $posts;
  346. }
  347. /**
  348. * Get user comments (used to show user activity)
  349. */
  350. function get_user_comments ($user_id)
  351. {
  352. $comments = array();
  353. if (is_null ($this->database))
  354. return $comments;
  355. $query = $this->database->prepare (
  356. 'SELECT ' .
  357. 'C.*,' .
  358. 'P.`title` AS `postTitle`,' .
  359. 'P.`hashId` AS `postHashId`' .
  360. 'FROM `comment` AS C ' .
  361. 'JOIN `post` AS P ON P.`id` = C.`postId`' .
  362. 'WHERE C.`userId` = ? ' .
  363. 'ORDER BY C.`created` DESC ' .
  364. 'LIMIT 50');
  365. $query->execute (array($user_id));
  366. $comments = $query->fetchAll(PDO::FETCH_ASSOC);
  367. return $comments;
  368. }
  369. /**
  370. * Get user replies (used to show user activity)
  371. */
  372. function get_user_replies ($user_id)
  373. {
  374. $replies = array();
  375. if (is_null ($this->database))
  376. return $replies;
  377. $query = $this->database->prepare (
  378. 'SELECT ' .
  379. 'C.*,' .
  380. 'P.`title` AS `postTitle`,' .
  381. 'P.`hashId` AS `postHashId`,' .
  382. 'U.`username` AS `username`' .
  383. 'FROM `comment` AS C ' .
  384. 'JOIN `post` AS P ON P.`id` = C.`postId`' .
  385. 'JOIN `user` AS U ON U.`id` = C.`userId`' .
  386. 'WHERE C.`parentUserId` = :user_id AND C.`userId` != :user_id ' .
  387. 'ORDER BY C.`created` DESC ' .
  388. 'LIMIT 50');
  389. $query->execute (array ('user_id' => $user_id));
  390. $replies = $query->fetchAll(PDO::FETCH_ASSOC);
  391. return $replies;
  392. }
  393. /**
  394. * Set user replies as read
  395. */
  396. function set_replies_as_read ($user_id)
  397. {
  398. $query = $this->database->prepare (
  399. 'UPDATE `comment`' .
  400. 'SET `read` = 1 ' .
  401. 'WHERE `parentUserId` = ? AND `read` = 0');
  402. $query->execute (array ($user_id));
  403. }
  404. /**
  405. * Retrieve a list of votes for a range of posts.
  406. *
  407. * @param posts_id list of IDs (eg. "2,4,5").
  408. * NOTE: Because arrays can't be used with PDO, $posts_id
  409. * is a string that's concatenated to the SQL query. For
  410. * this reason is the responsibility of the caller to
  411. * check that $posts_id is a valid string of integers
  412. * separated by commans (beware of SQL injection).
  413. */
  414. function get_posts_votes ($posts_id, $user_id)
  415. {
  416. $votes = array();
  417. if (is_null ($this->database) || is_null ($posts_id) || is_null ($user_id))
  418. return $votes;
  419. // Run a test anyway to make sure $posts_id is a valid string
  420. $posts_id_array = explode (',', $posts_id);
  421. foreach ($posts_id_array as $post_id)
  422. if (!is_numeric ($post_id))
  423. return $votes;
  424. // Retrieve the votes
  425. $query = $this->database->prepare (
  426. 'SELECT * ' .
  427. 'FROM `vote_post`' .
  428. 'WHERE `postId` IN(' . $posts_id . ') AND `userId` = ?');
  429. $query->execute (array ($user_id));
  430. $votes = $query->fetchAll(PDO::FETCH_ASSOC);
  431. // Create an array of votes with `postId` as key
  432. $sorted_votes = array();
  433. foreach ($votes as $vote)
  434. $sorted_votes[$vote['postId']] = $vote;
  435. return $sorted_votes;
  436. }
  437. /**
  438. * Retrieve a list of votes for a range of comments.
  439. *
  440. * @param comments_id list of IDs (eg. "2,4,5").
  441. * NOTE: Because arrays can't be used with PDO, $comments_id
  442. * is a string that's concatenated to the SQL query. For
  443. * this reason is the responsibility of the caller to
  444. * check that $comments_id is a valid string of integers
  445. * separated by commans (beware of SQL injection).
  446. */
  447. function get_comments_votes ($comments_id, $user_id)
  448. {
  449. $votes = array();
  450. if (is_null ($this->database) || is_null ($comments_id) || is_null ($user_id))
  451. return $votes;
  452. // Run a test anyway to make sure $posts_id is a valid string
  453. $comments_id_array = explode (',', $comments_id);
  454. foreach ($comments_id_array as $comment_id)
  455. if (!is_numeric ($comment_id))
  456. return $votes;
  457. // Retrieve the votes
  458. $query = $this->database->prepare (
  459. 'SELECT * ' .
  460. 'FROM `vote_comment`' .
  461. 'WHERE `commentId` IN(' . $comments_id . ') AND `userId` = ?');
  462. $query->execute (array ($user_id));
  463. $votes = $query->fetchAll (PDO::FETCH_ASSOC);
  464. // Create an array of votes with `commentId` as key
  465. $sorted_votes = array();
  466. foreach ($votes as $vote)
  467. $sorted_votes[$vote['commentId']] = $vote;
  468. return $sorted_votes;
  469. }
  470. /**
  471. * Create new user account
  472. */
  473. function new_user ($username, $password)
  474. {
  475. $user = NULL;
  476. try {
  477. // Create a hash_id for the new post
  478. $new_hash_id = self::get_random_string();
  479. // Create a salt for user password
  480. $salt = self::get_random_string (16);
  481. $this->database->beginTransaction();
  482. // Create the new user
  483. $query = $this->database->prepare(
  484. 'INSERT INTO `user` (`hashId`, `isActive`, `password`, `registered`, `salt`, `username`)' .
  485. 'VALUES (?, 1, SHA2(?, 512), NOW(), ?, ?)');
  486. $query->execute(array($new_hash_id, $password . $salt, $salt, $username));
  487. // Retrieve the new user
  488. $user = self::get_user ($username);
  489. $this->database->commit();
  490. return $user;
  491. } catch(PDOException $ex) {
  492. $this->database->rollBack();
  493. }
  494. }
  495. /**
  496. * Submit a new post/link
  497. */
  498. function new_post ($title, $link, $text, $user_id)
  499. {
  500. // Create a hash_id for the new post
  501. $new_hash_id = self::get_random_string();
  502. try {
  503. $this->database->beginTransaction();
  504. // Create the new post
  505. $query = $this->database->prepare(
  506. 'INSERT INTO `post` (`hashId`, `created`, `dateCreated`, `title`, `link`, `text`, `vote`, `commentsCount`, `userId`)' .
  507. 'VALUES (?, NOW(), CURDATE(), ?, ?, ?, 0, 0, ?)');
  508. $query->execute(array($new_hash_id, $title, $link, $text, $user_id));
  509. // Retrieve the id of the new post
  510. $post_id = $this->database->lastInsertId();
  511. $this->database->commit();
  512. } catch(PDOException $ex) {
  513. $this->database->rollBack();
  514. return NULL;
  515. }
  516. // Automatically upvote this post (start from +1)
  517. self::upvote_post ($new_hash_id, $user_id);
  518. return $new_hash_id;
  519. }
  520. /**
  521. * Submit a new comment to a post
  522. */
  523. function new_comment ($comment, $post_hash_id, $user_id)
  524. {
  525. $new_hash_id = NULL;
  526. try {
  527. $new_hash_id = self::get_random_string();
  528. $this->database->beginTransaction();
  529. // Retrieve the post
  530. $query = $this->database->prepare(
  531. 'SELECT * ' .
  532. 'FROM `post`' .
  533. 'WHERE `hashId` = ?');
  534. $query->execute(array($post_hash_id));
  535. $post = $query->fetch(PDO::FETCH_ASSOC);
  536. // Add new comment
  537. $query = $this->database->prepare(
  538. 'INSERT INTO `comment` (`hashId`, `created`, `dateCreated`, `read`, `text`, `vote`, `parentId`, `parentUserId`, `postId`, `userId`)' .
  539. 'VALUES (?, NOW(), CURDATE(), 0, ?, 1, ?, ?, ?, ?)');
  540. $query->execute(array($new_hash_id, $comment, NULL, $post['userId'], $post['id'], $user_id));
  541. $comment_id = $this->database->lastInsertId();
  542. // Add vote
  543. $query = $this->database->prepare(
  544. 'INSERT INTO `vote_comment` (`vote`, `datetime`, `commentId`, `userId`)' .
  545. 'VALUES (1, NOW(), ?, ?)');
  546. $query->execute(array($comment_id, $user_id));
  547. // Increase comments count for post
  548. $query = $this->database->prepare (
  549. 'UPDATE `post`' .
  550. 'SET `commentsCount` = `commentsCount` + 1 ' .
  551. 'WHERE `hashId` = ?');
  552. $query->execute (array ($post_hash_id));
  553. $this->database->commit();
  554. return $new_hash_id;
  555. } catch(PDOException $ex) {
  556. $this->database->rollBack();
  557. return NULL;
  558. }
  559. }
  560. /**
  561. * Submit a new reply to a comment
  562. */
  563. function new_reply ($comment, $parent_comment_hash_id, $user_id)
  564. {
  565. try {
  566. $this->database->beginTransaction();
  567. // Retrieve the parent comment
  568. $query = $this->database->prepare (
  569. 'SELECT * ' .
  570. 'FROM `comment`' .
  571. 'WHERE `hashId` = ?');
  572. $query->execute (array ($parent_comment_hash_id));
  573. $parent_comment = $query->fetch (PDO::FETCH_ASSOC);
  574. // Retrieve the post
  575. $query = $this->database->prepare (
  576. 'SELECT * ' .
  577. 'FROM `post`' .
  578. 'WHERE `id` = ?');
  579. $query->execute (array ($parent_comment['postId']));
  580. $post = $query->fetch(PDO::FETCH_ASSOC);
  581. // Add the new comment
  582. $comment_hash_id = self::get_random_string();
  583. $query = $this->database->prepare (
  584. 'INSERT INTO `comment` (`hashId`, `created`, `dateCreated`, `read`, `text`, `vote`, `parentId`, `parentUserId`, `postId`, `userId`)' .
  585. 'VALUES (?, NOW(), CURDATE(), 0, ?, 1, ?, ?, ?, ?)');
  586. $query->execute (array ($comment_hash_id, $comment, $parent_comment['id'], $parent_comment['userId'], $post['id'], $user_id));
  587. $comment_id = $this->database->lastInsertId();
  588. // Add first vote for user comment
  589. $query = $this->database->prepare (
  590. 'INSERT INTO `vote_comment` (`vote`, `datetime`, `commentId`, `userId`)' .
  591. 'VALUES (1, NOW(), ?, ?)');
  592. $query->execute (array ($comment_id, $user_id));
  593. // Increase comments count for post
  594. $query = $this->database->prepare (
  595. 'UPDATE `post`' .
  596. 'SET `commentsCount` = `commentsCount` + 1 ' .
  597. 'WHERE `hashId` = ?');
  598. $query->execute (array ($post['hashId']));
  599. $this->database->commit ();
  600. // Return the hash_id of both post and comment
  601. return array (
  602. 'post' => $post['hashId'],
  603. 'comment' => $comment_hash_id);
  604. } catch(PDOException $ex) {
  605. $this->database->rollBack();
  606. return NULL;
  607. }
  608. }
  609. /**
  610. * Update a post text
  611. */
  612. function edit_post ($title, $link, $text, $post_hash_id, $user_id)
  613. {
  614. $query = $this->database->prepare (
  615. 'UPDATE `post`' .
  616. 'SET ' .
  617. '`title` = ?, ' .
  618. '`link` = ?, ' .
  619. '`text` = ? ' .
  620. 'WHERE `hashId` = ? AND `userId` = ?');
  621. $query->execute ([$title, $link, $text, $post_hash_id, $user_id]);
  622. $affected_rows = $query->rowCount();
  623. return $affected_rows;
  624. }
  625. /**
  626. * Update a comment text
  627. */
  628. function edit_comment ($text, $comment_hash_id, $user_id)
  629. {
  630. $query = $this->database->prepare (
  631. 'UPDATE `comment`' .
  632. 'SET `text` = ? ' .
  633. 'WHERE `hashId` = ? AND `userId` = ?');
  634. $query->execute (array ($text, $comment_hash_id, $user_id));
  635. $affected_rows = $query->rowCount();
  636. return $affected_rows;
  637. }
  638. /**
  639. * Update user information
  640. */
  641. function edit_user ($about, $email, $email_notifications, $user_id)
  642. {
  643. $query = $this->database->prepare (
  644. 'UPDATE `user`' .
  645. 'SET `about` = :about, `email` = :email, `email_notifications` = :email_notifications ' .
  646. 'WHERE `id` = :user_id');
  647. $query->bindValue (':about', $about, PDO::PARAM_STR);
  648. $query->bindValue (':user_id', $user_id, PDO::PARAM_INT);
  649. $query->bindValue (':email_notifications', $email_notifications, PDO::PARAM_INT);
  650. if (NULL == $email || '' == $email)
  651. $query->bindValue (':email', NULL, PDO::PARAM_NULL);
  652. else
  653. $query->bindValue (':email', $email, PDO::PARAM_STR);
  654. $query->execute ();
  655. }
  656. /**
  657. * Tell if a user has voted a post
  658. */
  659. function voted_post ($post_id, $user_id)
  660. {
  661. if (is_null($this->database))
  662. return false;
  663. $query = $this->database->prepare(
  664. 'SELECT * ' .
  665. 'FROM `vote_post`' .
  666. 'WHERE `postId` = ? and `userId` = ?');
  667. $query->execute (array ($post_id, $user_id));
  668. $vote = $query->fetch (PDO::FETCH_ASSOC);
  669. if (is_null ($vote) || empty ($vote))
  670. return false;
  671. return $vote;
  672. }
  673. /**
  674. * Tell if a user has voted a comment
  675. */
  676. function voted_comment ($comment_id, $user_id)
  677. {
  678. if (is_null($this->database))
  679. return false;
  680. $query = $this->database->prepare(
  681. 'SELECT * ' .
  682. 'FROM `vote_comment`' .
  683. 'WHERE `commentId` = ? and `userId` = ?');
  684. $query->execute (array ($comment_id, $user_id));
  685. $vote = $query->fetch (PDO::FETCH_ASSOC);
  686. if (is_null ($vote) || empty ($vote))
  687. return false;
  688. return $vote;
  689. }
  690. /**
  691. * Upvote a post
  692. */
  693. function upvote_post ($post_hash_id, $user_id)
  694. {
  695. try {
  696. $this->database->beginTransaction();
  697. // Get the post
  698. $post = self::get_post ($post_hash_id);
  699. // Already voted?
  700. $vote = self::voted_post ($post['id'], $user_id);
  701. if (false == $vote)
  702. {
  703. // Cast upvote
  704. $query = $this->database->prepare(
  705. 'INSERT INTO `vote_post` (`vote`, `datetime`, `postId`, `userId`)' .
  706. 'VALUES (1, NOW(), ?, ?)');
  707. $query->execute (array ($post['id'], $user_id));
  708. // Add +1 to post
  709. $query = $this->database->prepare (
  710. 'UPDATE `post`' .
  711. 'SET `vote` = `vote` + 1 ' .
  712. 'WHERE `id` = ?');
  713. $query->execute (array ($post['id']));
  714. } elseif ($vote['vote'] == 1) {
  715. // Already upvoted before. Remove upvote.
  716. $query = $this->database->prepare(
  717. 'DELETE FROM `vote_post`' .
  718. 'WHERE `postId` = ? AND `userId` = ?');
  719. $query->execute (array ($post['id'], $user_id));
  720. // Remove upvote from post
  721. $query = $this->database->prepare (
  722. 'UPDATE `post`' .
  723. 'SET `vote` = `vote` - 1 ' .
  724. 'WHERE `id` = ?');
  725. $query->execute (array ($post['id']));
  726. } elseif ($vote['vote'] == -1) {
  727. // Already downvoted before. Change to upvote.
  728. $query = $this->database->prepare(
  729. 'UPDATE `vote_post`' .
  730. 'SET `vote` = 1 ' .
  731. 'WHERE `postId` = ? AND `userId` = ?');
  732. $query->execute (array ($post['id'], $user_id));
  733. /* Update post vote count
  734. * +2 because of the previous downvote
  735. */
  736. $query = $this->database->prepare (
  737. 'UPDATE `post`' .
  738. 'SET `vote` = `vote` + 2 ' .
  739. 'WHERE `id` = ?');
  740. $query->execute (array ($post['id']));
  741. }
  742. $this->database->commit ();
  743. } catch(PDOException $ex) {
  744. $this->database->rollBack();
  745. }
  746. }
  747. /**
  748. * Downvote a post
  749. */
  750. function downvote_post ($post_hash_id, $user_id)
  751. {
  752. try {
  753. $this->database->beginTransaction();
  754. // Get the post
  755. $post = self::get_post ($post_hash_id);
  756. // Already voted?
  757. $vote = self::voted_post ($post['id'], $user_id);
  758. if (false == $vote)
  759. {
  760. // Cast downvote
  761. $query = $this->database->prepare(
  762. 'INSERT INTO `vote_post` (`vote`, `datetime`, `postId`, `userId`)' .
  763. 'VALUES (-1, NOW(), ?, ?)');
  764. $query->execute (array ($post['id'], $user_id));
  765. // Add -1 to post
  766. $query = $this->database->prepare (
  767. 'UPDATE `post`' .
  768. 'SET `vote` = `vote` - 1 ' .
  769. 'WHERE `id` = ?');
  770. $query->execute (array ($post['id']));
  771. } elseif ($vote['vote'] == -1) {
  772. // Already downvoted before. Remove downvote.
  773. $query = $this->database->prepare(
  774. 'DELETE FROM `vote_post`' .
  775. 'WHERE `postId` = ? AND `userId` = ?');
  776. $query->execute (array ($post['id'], $user_id));
  777. // Remove downvote from post
  778. $query = $this->database->prepare (
  779. 'UPDATE `post`' .
  780. 'SET `vote` = `vote` + 1 ' .
  781. 'WHERE `id` = ?');
  782. $query->execute (array ($post['id']));
  783. } elseif ($vote['vote'] == 1) {
  784. // Already upvoted before. Change to downvote.
  785. $query = $this->database->prepare(
  786. 'UPDATE `vote_post`' .
  787. 'SET `vote` = -1 ' .
  788. 'WHERE `postId` = ? AND `userId` = ?');
  789. $query->execute (array ($post['id'], $user_id));
  790. /* Update post vote count
  791. * -2 because of the previous upvote
  792. */
  793. $query = $this->database->prepare (
  794. 'UPDATE `post`' .
  795. 'SET `vote` = `vote` - 2 ' .
  796. 'WHERE `id` = ?');
  797. $query->execute (array ($post['id']));
  798. }
  799. $this->database->commit ();
  800. } catch(PDOException $ex) {
  801. $this->database->rollBack();
  802. }
  803. }
  804. /**
  805. * Upvote a comment
  806. */
  807. function upvote_comment ($comment_hash_id, $user_id)
  808. {
  809. try {
  810. $this->database->beginTransaction();
  811. // Get the comment
  812. $comment = self::get_comment ($comment_hash_id);
  813. // Already voted?
  814. $vote = self::voted_comment ($comment['id'], $user_id);
  815. if (false == $vote)
  816. {
  817. // Cast upvote
  818. $query = $this->database->prepare(
  819. 'INSERT INTO `vote_comment` (`vote`, `datetime`, `commentId`, `userId`)' .
  820. 'VALUES (1, NOW(), ?, ?)');
  821. $query->execute (array ($comment['id'], $user_id));
  822. // Add +1 to comment
  823. $query = $this->database->prepare (
  824. 'UPDATE `comment`' .
  825. 'SET `vote` = `vote` + 1 ' .
  826. 'WHERE `id` = ?');
  827. $query->execute (array ($comment['id']));
  828. } elseif ($vote['vote'] == 1) {
  829. // Already upvoted before. Remove upvote.
  830. $query = $this->database->prepare(
  831. 'DELETE FROM `vote_comment`' .
  832. 'WHERE `commentId` = ? AND `userId` = ?');
  833. $query->execute (array ($comment['id'], $user_id));
  834. // Remove upvote from comment
  835. $query = $this->database->prepare (
  836. 'UPDATE `comment`' .
  837. 'SET `vote` = `vote` - 1 ' .
  838. 'WHERE `id` = ?');
  839. $query->execute (array ($comment['id']));
  840. } elseif ($vote['vote'] == -1) {
  841. // Already downvoted before. Change to upvote.
  842. $query = $this->database->prepare(
  843. 'UPDATE `vote_comment`' .
  844. 'SET `vote` = 1 ' .
  845. 'WHERE `commentId` = ? AND `userId` = ?');
  846. $query->execute (array ($comment['id'], $user_id));
  847. /* Update comment vote count
  848. * +2 because of the previous downvote
  849. */
  850. $query = $this->database->prepare (
  851. 'UPDATE `comment`' .
  852. 'SET `vote` = `vote` + 2 ' .
  853. 'WHERE `id` = ?');
  854. $query->execute (array ($comment['id']));
  855. }
  856. $this->database->commit ();
  857. } catch(PDOException $ex) {
  858. $this->database->rollBack();
  859. }
  860. }
  861. /**
  862. * Downvote a comment
  863. */
  864. function downvote_comment ($comment_hash_id, $user_id)
  865. {
  866. try {
  867. $this->database->beginTransaction();
  868. // Get the comment
  869. $comment = self::get_comment ($comment_hash_id);
  870. // Already voted?
  871. $vote = self::voted_comment ($comment['id'], $user_id);
  872. if (false == $vote)
  873. {
  874. // Cast downvote
  875. $query = $this->database->prepare(
  876. 'INSERT INTO `vote_comment` (`vote`, `datetime`, `commentId`, `userId`)' .
  877. 'VALUES (-1, NOW(), ?, ?)');
  878. $query->execute (array ($comment['id'], $user_id));
  879. // Add -1 to comment
  880. $query = $this->database->prepare (
  881. 'UPDATE `comment`' .
  882. 'SET `vote` = `vote` - 1 ' .
  883. 'WHERE `id` = ?');
  884. $query->execute (array ($comment['id']));
  885. } elseif ($vote['vote'] == -1) {
  886. // Already downvoted before. Remove downvote.
  887. $query = $this->database->prepare(
  888. 'DELETE FROM `vote_comment`' .
  889. 'WHERE `commentId` = ? AND `userId` = ?');
  890. $query->execute (array ($comment['id'], $user_id));
  891. // Remove downvote from comment
  892. $query = $this->database->prepare (
  893. 'UPDATE `comment`' .
  894. 'SET `vote` = `vote` + 1 ' .
  895. 'WHERE `id` = ?');
  896. $query->execute (array ($comment['id']));
  897. } elseif ($vote['vote'] == 1) {
  898. // Already upvoted before. Change to downvote.
  899. $query = $this->database->prepare(
  900. 'UPDATE `vote_comment`' .
  901. 'SET `vote` = -1 ' .
  902. 'WHERE `commentId` = ? AND `userId` = ?');
  903. $query->execute (array ($comment['id'], $user_id));
  904. /* Update comment vote count
  905. * -2 because of the previous upvote
  906. */
  907. $query = $this->database->prepare (
  908. 'UPDATE `comment`' .
  909. 'SET `vote` = `vote` - 2 ' .
  910. 'WHERE `id` = ?');
  911. $query->execute (array ($comment['id']));
  912. }
  913. $this->database->commit ();
  914. } catch(PDOException $ex) {
  915. $this->database->rollBack();
  916. }
  917. }
  918. /**
  919. * Reset a user password. This function adds a new passwordResetCode
  920. * Lately, the new password is updated with password_reset_validate()
  921. */
  922. function password_reset ($user_hash_id)
  923. {
  924. try {
  925. $this->database->beginTransaction();
  926. // Generate a new secret token (used for validation)
  927. $token = self::get_random_string (32);
  928. /* Add the new secret token to the database.
  929. * Check that the last resetToken wasn't set less than 5 minutes
  930. * ago. This is to prevent spam from other users who might have
  931. * inserted the same email.
  932. *
  933. * 1435 = 24h x 60' - 5'
  934. */
  935. $query = $this->database->prepare (
  936. 'UPDATE `user`' .
  937. 'SET `passwordResetToken` = ?, `passwordResetTokenExpire` = NOW() + INTERVAL 1 DAY ' .
  938. 'WHERE' .
  939. '`hashId` = ? AND ' .
  940. '(`passwordResetTokenExpire` IS NULL OR TIMESTAMPDIFF(MINUTE, NOW(), `passwordResetTokenExpire`) < 1435)');
  941. $query->execute (array ($token, $user_hash_id));
  942. $affected_rows = $query->rowCount ();
  943. $this->database->commit ();
  944. return $affected_rows ? $token : NULL;
  945. } catch(PDOException $ex) {
  946. $this->database->rollBack();
  947. return NULL;
  948. }
  949. }
  950. /**
  951. * Check if a token (sent by email for password reset) is valid
  952. */
  953. function password_reset_validate ($token, $new_password)
  954. {
  955. if (is_null ($this->database))
  956. return false;
  957. $query = $this->database->prepare (
  958. 'UPDATE `user`' .
  959. 'SET `password` = SHA2(CONCAT(?, `salt`), 512), `passwordResetToken` = NULL, `passwordResetTokenExpire` = NULL ' .
  960. 'WHERE `passwordResetToken` = ? AND `passwordResetTokenExpire` > NOW()');
  961. $query->execute (array ($new_password, $token));
  962. return true;
  963. }
  964. /**
  965. * Search database
  966. *
  967. * TODO: When MySQL will be updated to v5.6, add fulltext search
  968. * with scores for better results. InnoDB doesn't support
  969. * fulltext search with older versions.
  970. */
  971. function search ($search_query)
  972. {
  973. if (is_null ($this->database))
  974. return false;
  975. $search_query = trim ($search_query);
  976. if (strlen ($search_query) == 0)
  977. return false;
  978. $search_query = str_ireplace (' ', '%', $search_query);
  979. $query = $this->database->prepare (
  980. 'SELECT P.*, U.`username` ' .
  981. 'FROM `post` AS P ' .
  982. 'JOIN `user` AS U ON P.`userId` = U.`id`' .
  983. 'WHERE P.`title` LIKE ? ' .
  984. 'ORDER BY P.`created` DESC ' .
  985. 'LIMIT 100');
  986. $query->execute (['%' . $search_query . '%']);
  987. $results = $query->fetchAll (PDO::FETCH_ASSOC);
  988. return $results;
  989. }
  990. }