PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/qa-include/db/users.php

http://github.com/q2a/question2answer
PHP | 417 lines | 215 code | 67 blank | 135 comment | 17 complexity | eaa0a7b512f89c9a8d0922e8fa083f6f MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /*
  3. Question2Answer by Gideon Greenspan and contributors
  4. http://www.question2answer.org/
  5. Description: Database-level access to user management tables (if not using single sign-on)
  6. This program is free software; you can redistribute it and/or
  7. modify it under the terms of the GNU General Public License
  8. as published by the Free Software Foundation; either version 2
  9. of the License, or (at your option) any later version.
  10. This program 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 General Public License for more details.
  14. More about this license: http://www.question2answer.org/license.php
  15. */
  16. if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
  17. header('Location: ../../');
  18. exit;
  19. }
  20. /**
  21. * Return the expected value for the passcheck column given the $password and password $salt
  22. * @param string $password
  23. * @param string $salt
  24. * @return string
  25. */
  26. function qa_db_calc_passcheck($password, $salt)
  27. {
  28. if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
  29. return sha1(substr($salt, 0, 8) . $password . substr($salt, 8));
  30. }
  31. /**
  32. * Create a new user in the database with $email, $password, $handle, privilege $level, and $ip address
  33. * @param string $email
  34. * @param string|null $password
  35. * @param string $handle
  36. * @param int $level
  37. * @param string $ip
  38. * @return string
  39. */
  40. function qa_db_user_create($email, $password, $handle, $level, $ip)
  41. {
  42. require_once QA_INCLUDE_DIR . 'util/string.php';
  43. $ipHex = bin2hex(@inet_pton($ip));
  44. if (QA_PASSWORD_HASH) {
  45. qa_db_query_sub(
  46. 'INSERT INTO ^users (created, createip, email, passhash, level, handle, loggedin, loginip) ' .
  47. 'VALUES (NOW(), UNHEX($), $, $, #, $, NOW(), UNHEX($))',
  48. $ipHex, $email, isset($password) ? password_hash($password, PASSWORD_BCRYPT) : null, (int)$level, $handle, $ipHex
  49. );
  50. } else {
  51. $salt = isset($password) ? qa_random_alphanum(16) : null;
  52. qa_db_query_sub(
  53. 'INSERT INTO ^users (created, createip, email, passsalt, passcheck, level, handle, loggedin, loginip) ' .
  54. 'VALUES (NOW(), UNHEX($), $, $, UNHEX($), #, $, NOW(), UNHEX($))',
  55. $ipHex, $email, $salt, isset($password) ? qa_db_calc_passcheck($password, $salt) : null, (int)$level, $handle, $ipHex
  56. );
  57. }
  58. return qa_db_last_insert_id();
  59. }
  60. /**
  61. * Delete user $userid from the database, along with everything they have ever done (to the extent that it's possible)
  62. * @param mixed $userid
  63. */
  64. function qa_db_user_delete($userid)
  65. {
  66. qa_db_query_sub('UPDATE ^posts SET lastuserid=NULL WHERE lastuserid=$', $userid);
  67. qa_db_query_sub('DELETE FROM ^userpoints WHERE userid=$', $userid);
  68. qa_db_query_sub('DELETE FROM ^blobs WHERE blobid=(SELECT avatarblobid FROM ^users WHERE userid=$)', $userid);
  69. qa_db_query_sub('DELETE FROM ^users WHERE userid=$', $userid);
  70. // All the queries below should be superfluous due to foreign key constraints, but just in case the user switched to MyISAM.
  71. // Note also that private messages to/from that user are kept since we don't have all the keys we need to delete efficiently.
  72. qa_db_query_sub('UPDATE ^posts SET userid=NULL WHERE userid=$', $userid);
  73. qa_db_query_sub('DELETE FROM ^userlogins WHERE userid=$', $userid);
  74. qa_db_query_sub('DELETE FROM ^userprofile WHERE userid=$', $userid);
  75. qa_db_query_sub('DELETE FROM ^userfavorites WHERE userid=$ OR entitytype=$ AND entityid=$', $userid, QA_ENTITY_USER, $userid);
  76. qa_db_query_sub('DELETE FROM ^userevents WHERE userid=$', $userid);
  77. qa_db_query_sub('DELETE FROM ^uservotes WHERE userid=$', $userid);
  78. qa_db_query_sub('DELETE FROM ^userlimits WHERE userid=$', $userid);
  79. }
  80. /**
  81. * Return the ids of all users in the database which match $email (should be one or none)
  82. * @param string $email
  83. * @return array
  84. */
  85. function qa_db_user_find_by_email($email)
  86. {
  87. return qa_db_read_all_values(qa_db_query_sub(
  88. 'SELECT userid FROM ^users WHERE email=$',
  89. $email
  90. ));
  91. }
  92. /**
  93. * Return the ids of all users in the database which match $handle (=username), should be one or none
  94. * @param string $handle
  95. * @return array
  96. */
  97. function qa_db_user_find_by_handle($handle)
  98. {
  99. return qa_db_read_all_values(qa_db_query_sub(
  100. 'SELECT userid FROM ^users WHERE handle=$',
  101. $handle
  102. ));
  103. }
  104. /**
  105. * Return an array mapping each userid in $userids that can be found to that user's handle
  106. * @param array $userids
  107. * @return array
  108. */
  109. function qa_db_user_get_userid_handles($userids)
  110. {
  111. if (!empty($userids)) {
  112. return qa_db_read_all_assoc(qa_db_query_sub(
  113. 'SELECT userid, handle FROM ^users WHERE userid IN (#)',
  114. $userids
  115. ), 'userid', 'handle');
  116. }
  117. return array();
  118. }
  119. /**
  120. * Return an array mapping mapping each handle in $handle that can be found to that user's userid
  121. * @param array $handles
  122. * @return array
  123. */
  124. function qa_db_user_get_handle_userids($handles)
  125. {
  126. if (!empty($handles)) {
  127. return qa_db_read_all_assoc(qa_db_query_sub(
  128. 'SELECT handle, userid FROM ^users WHERE handle IN ($)',
  129. $handles
  130. ), 'handle', 'userid');
  131. }
  132. return array();
  133. }
  134. /**
  135. * Set $field of $userid to $value in the database users table. If the $fields parameter is an array, the $value
  136. * parameter is ignored and each element of the array is treated as a key-value pair of user fields and values.
  137. * @param mixed $userid
  138. * @param string|array $fields
  139. * @param string|null $value
  140. * @throws \Q2A\Database\Exceptions\QueryException
  141. */
  142. function qa_db_user_set($userid, $fields, $value = null)
  143. {
  144. if (!is_array($fields)) {
  145. $fields = [$fields => $value];
  146. }
  147. $columnRegex = '/^[A-Za-z0-9_]+$/'; // simple and secure way to ensure only valid fields are used
  148. $sql = 'UPDATE ^users SET ';
  149. foreach ($fields as $field => $fieldValue) {
  150. if (!preg_match($columnRegex, $field)) {
  151. throw new \Q2A\Database\Exceptions\QueryException('Invalid field supplied to qa_db_user_set');
  152. }
  153. $sql .= "`$field` = ?, ";
  154. }
  155. $sql = substr($sql, 0, -2) . ' WHERE userid = ?';
  156. $params = array_values($fields);
  157. $params[] = $userid;
  158. qa_service('database')->query($sql, $params);
  159. }
  160. /**
  161. * Set the password of $userid to $password, and reset their salt at the same time
  162. * @param mixed $userid
  163. * @param string $password
  164. * @return mixed
  165. */
  166. function qa_db_user_set_password($userid, $password)
  167. {
  168. if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
  169. require_once QA_INCLUDE_DIR . 'util/string.php';
  170. if (QA_PASSWORD_HASH) {
  171. qa_db_query_sub(
  172. 'UPDATE ^users SET passhash=$, passsalt=NULL, passcheck=NULL WHERE userid=$',
  173. password_hash($password, PASSWORD_BCRYPT), $userid
  174. );
  175. } else {
  176. $salt = qa_random_alphanum(16);
  177. qa_db_query_sub(
  178. 'UPDATE ^users SET passsalt=$, passcheck=UNHEX($) WHERE userid=$',
  179. $salt, qa_db_calc_passcheck($password, $salt), $userid
  180. );
  181. }
  182. }
  183. /**
  184. * Switch on the $flag bit of the flags column for $userid if $set is true, or switch off otherwise
  185. * @param mixed $userid
  186. * @param int $flag
  187. * @param bool $set
  188. */
  189. function qa_db_user_set_flag($userid, $flag, $set)
  190. {
  191. qa_db_query_sub(
  192. 'UPDATE ^users SET flags=flags' . ($set ? '|' : '&~') . '# WHERE userid=$',
  193. $flag, $userid
  194. );
  195. }
  196. /**
  197. * Return a random string to be used for a user's emailcode column
  198. * @return string
  199. */
  200. function qa_db_user_rand_emailcode()
  201. {
  202. if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
  203. require_once QA_INCLUDE_DIR . 'util/string.php';
  204. return qa_random_alphanum(8);
  205. }
  206. /**
  207. * Return a random string to be used for a user's sessioncode column (for browser session cookies)
  208. * @return string
  209. */
  210. function qa_db_user_rand_sessioncode()
  211. {
  212. if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
  213. require_once QA_INCLUDE_DIR . 'util/string.php';
  214. return qa_random_alphanum(8);
  215. }
  216. /**
  217. * Set a row in the database user profile table to store $value for $field for $userid
  218. * @param mixed $userid
  219. * @param string $field
  220. * @param string $value
  221. */
  222. function qa_db_user_profile_set($userid, $field, $value)
  223. {
  224. qa_db_query_sub(
  225. 'INSERT INTO ^userprofile (userid, title, content) VALUES ($, $, $) ' .
  226. 'ON DUPLICATE KEY UPDATE content = VALUES(content)',
  227. $userid, $field, $value
  228. );
  229. }
  230. /**
  231. * Note in the database that $userid just logged in from $ip address
  232. * @param mixed $userid
  233. * @param string $ip
  234. */
  235. function qa_db_user_logged_in($userid, $ip)
  236. {
  237. qa_db_query_sub(
  238. 'UPDATE ^users SET loggedin=NOW(), loginip=UNHEX($) WHERE userid=$',
  239. bin2hex(@inet_pton($ip)), $userid
  240. );
  241. }
  242. /**
  243. * Note in the database that $userid just performed a write operation from $ip address
  244. * @param mixed $userid
  245. * @param string $ip
  246. */
  247. function qa_db_user_written($userid, $ip)
  248. {
  249. qa_db_query_sub(
  250. 'UPDATE ^users SET written=NOW(), writeip=UNHEX($) WHERE userid=$',
  251. bin2hex(@inet_pton($ip)), $userid
  252. );
  253. }
  254. /**
  255. * Add an external login in the database for $source and $identifier for user $userid
  256. * @param mixed $userid
  257. * @param string $source
  258. * @param string $identifier
  259. */
  260. function qa_db_user_login_add($userid, $source, $identifier)
  261. {
  262. qa_db_query_sub(
  263. 'INSERT INTO ^userlogins (userid, source, identifier, identifiermd5) ' .
  264. 'VALUES ($, $, $, UNHEX($))',
  265. $userid, $source, $identifier, md5($identifier)
  266. );
  267. }
  268. /**
  269. * Return some information about the user with external login $source and $identifier in the database, if a match is found
  270. * @param string $source
  271. * @param string $identifier
  272. * @return array
  273. */
  274. function qa_db_user_login_find($source, $identifier)
  275. {
  276. return qa_db_read_all_assoc(qa_db_query_sub(
  277. 'SELECT ^userlogins.userid, handle, email FROM ^userlogins LEFT JOIN ^users ON ^userlogins.userid=^users.userid ' .
  278. 'WHERE source=$ AND identifiermd5=UNHEX($) AND identifier=$',
  279. $source, md5($identifier), $identifier
  280. ));
  281. }
  282. /**
  283. * Lock all tables if $sync is true, otherwise unlock them. Used to synchronize creation of external login mappings.
  284. * @param bool $sync
  285. */
  286. function qa_db_user_login_sync($sync)
  287. {
  288. if ($sync) { // need to lock all tables since any could be used by a plugin's event module
  289. $tables = qa_db_list_tables();
  290. $locks = array();
  291. foreach ($tables as $table)
  292. $locks[] = $table . ' WRITE';
  293. qa_db_query_sub('LOCK TABLES ' . implode(', ', $locks));
  294. } else {
  295. qa_db_query_sub('UNLOCK TABLES');
  296. }
  297. }
  298. /**
  299. * Reset the full set of context-specific (currently, per category) user levels for user $userid to $userlevels, where
  300. * $userlevels is an array of arrays, the inner arrays containing items 'entitytype', 'entityid' and 'level'.
  301. * @param mixed $userid
  302. * @param array $userlevels
  303. */
  304. function qa_db_user_levels_set($userid, $userlevels)
  305. {
  306. qa_db_query_sub(
  307. 'DELETE FROM ^userlevels WHERE userid=$',
  308. $userid
  309. );
  310. foreach ($userlevels as $userlevel) {
  311. qa_db_query_sub(
  312. 'INSERT INTO ^userlevels (userid, entitytype, entityid, level) VALUES ($, $, #, #) ' .
  313. 'ON DUPLICATE KEY UPDATE level = VALUES(level)',
  314. $userid, $userlevel['entitytype'], $userlevel['entityid'], $userlevel['level']
  315. );
  316. }
  317. }
  318. /**
  319. * Get the information required for sending a mailing to the next $count users with userids greater than $lastuserid
  320. * @param mixed $lastuserid
  321. * @param int $count
  322. * @return array
  323. */
  324. function qa_db_users_get_mailing_next($lastuserid, $count)
  325. {
  326. return qa_db_read_all_assoc(qa_db_query_sub(
  327. 'SELECT userid, email, handle, emailcode, flags, level FROM ^users WHERE userid># ORDER BY userid LIMIT #',
  328. $lastuserid, $count
  329. ));
  330. }
  331. /**
  332. * Update the cached count of the number of users who are awaiting approval after registration
  333. */
  334. function qa_db_uapprovecount_update()
  335. {
  336. if (qa_should_update_counts() && !QA_FINAL_EXTERNAL_USERS) {
  337. qa_db_query_sub(
  338. "INSERT INTO ^options (title, content) " .
  339. "SELECT 'cache_uapprovecount', COUNT(*) FROM ^users " .
  340. "WHERE level < # AND NOT (flags & #) " .
  341. "ON DUPLICATE KEY UPDATE content = VALUES(content)",
  342. QA_USER_LEVEL_APPROVED, QA_USER_FLAGS_USER_BLOCKED
  343. );
  344. }
  345. }