users.php 11.8 KB
Newer Older
Scott committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php
/*
	Question2Answer by Gideon Greenspan and contributors
	http://www.question2answer.org/

	Description: Database-level access to user management tables (if not using single sign-on)


	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	More about this license: http://www.question2answer.org/license.php
*/

Scott committed
22
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
23
	header('Location: ../../');
Scott committed
24 25 26 27 28 29
	exit;
}


/**
 * Return the expected value for the passcheck column given the $password and password $salt
30 31 32
 * @param string $password
 * @param string $salt
 * @return string
Scott committed
33 34 35 36 37 38 39 40 41 42 43
 */
function qa_db_calc_passcheck($password, $salt)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	return sha1(substr($salt, 0, 8) . $password . substr($salt, 8));
}


/**
 * Create a new user in the database with $email, $password, $handle, privilege $level, and $ip address
44 45 46 47 48 49
 * @param string $email
 * @param string|null $password
 * @param string $handle
 * @param int $level
 * @param string $ip
 * @return string
Scott committed
50 51 52 53 54
 */
function qa_db_user_create($email, $password, $handle, $level, $ip)
{
	require_once QA_INCLUDE_DIR . 'util/string.php';

55
	$ipHex = bin2hex(@inet_pton($ip));
Scott committed
56 57

	if (QA_PASSWORD_HASH) {
Scott committed
58
		qa_db_query_sub(
Scott committed
59
			'INSERT INTO ^users (created, createip, email, passhash, level, handle, loggedin, loginip) ' .
60 61
			'VALUES (NOW(), UNHEX($), $, $, #, $, NOW(), UNHEX($))',
			$ipHex, $email, isset($password) ? password_hash($password, PASSWORD_BCRYPT) : null, (int)$level, $handle, $ipHex
Scott committed
62
		);
Scott committed
63 64
	} else {
		$salt = isset($password) ? qa_random_alphanum(16) : null;
Scott committed
65 66

		qa_db_query_sub(
Scott committed
67
			'INSERT INTO ^users (created, createip, email, passsalt, passcheck, level, handle, loggedin, loginip) ' .
68 69
			'VALUES (NOW(), UNHEX($), $, $, UNHEX($), #, $, NOW(), UNHEX($))',
			$ipHex, $email, $salt, isset($password) ? qa_db_calc_passcheck($password, $salt) : null, (int)$level, $handle, $ipHex
Scott committed
70 71 72 73
		);
	}


Scott committed
74 75 76 77 78 79
	return qa_db_last_insert_id();
}


/**
 * Delete user $userid from the database, along with everything they have ever done (to the extent that it's possible)
80
 * @param mixed $userid
Scott committed
81 82 83 84 85 86 87 88 89 90 91 92 93 94
 */
function qa_db_user_delete($userid)
{
	qa_db_query_sub('UPDATE ^posts SET lastuserid=NULL WHERE lastuserid=$', $userid);
	qa_db_query_sub('DELETE FROM ^userpoints WHERE userid=$', $userid);
	qa_db_query_sub('DELETE FROM ^blobs WHERE blobid=(SELECT avatarblobid FROM ^users WHERE userid=$)', $userid);
	qa_db_query_sub('DELETE FROM ^users WHERE userid=$', $userid);

	// All the queries below should be superfluous due to foreign key constraints, but just in case the user switched to MyISAM.
	// Note also that private messages to/from that user are kept since we don't have all the keys we need to delete efficiently.

	qa_db_query_sub('UPDATE ^posts SET userid=NULL WHERE userid=$', $userid);
	qa_db_query_sub('DELETE FROM ^userlogins WHERE userid=$', $userid);
	qa_db_query_sub('DELETE FROM ^userprofile WHERE userid=$', $userid);
95
	qa_db_query_sub('DELETE FROM ^userfavorites WHERE userid=$ OR entitytype=$ AND entityid=$', $userid, QA_ENTITY_USER, $userid);
Scott committed
96 97 98 99 100 101 102 103
	qa_db_query_sub('DELETE FROM ^userevents WHERE userid=$', $userid);
	qa_db_query_sub('DELETE FROM ^uservotes WHERE userid=$', $userid);
	qa_db_query_sub('DELETE FROM ^userlimits WHERE userid=$', $userid);
}


/**
 * Return the ids of all users in the database which match $email (should be one or none)
104
 * @param string $email
Scott committed
105 106 107 108 109 110 111 112 113 114 115 116 117
 * @return array
 */
function qa_db_user_find_by_email($email)
{
	return qa_db_read_all_values(qa_db_query_sub(
		'SELECT userid FROM ^users WHERE email=$',
		$email
	));
}


/**
 * Return the ids of all users in the database which match $handle (=username), should be one or none
118
 * @param string $handle
Scott committed
119 120 121 122 123 124 125 126 127 128 129 130 131
 * @return array
 */
function qa_db_user_find_by_handle($handle)
{
	return qa_db_read_all_values(qa_db_query_sub(
		'SELECT userid FROM ^users WHERE handle=$',
		$handle
	));
}


/**
 * Return an array mapping each userid in $userids that can be found to that user's handle
132
 * @param array $userids
Scott committed
133 134 135 136
 * @return array
 */
function qa_db_user_get_userid_handles($userids)
{
137
	if (!empty($userids)) {
Scott committed
138 139 140 141
		return qa_db_read_all_assoc(qa_db_query_sub(
			'SELECT userid, handle FROM ^users WHERE userid IN (#)',
			$userids
		), 'userid', 'handle');
Scott committed
142 143
	}

Scott committed
144 145
	return array();
}
Scott committed
146 147


Scott committed
148 149
/**
 * Return an array mapping mapping each handle in $handle that can be found to that user's userid
150
 * @param array $handles
Scott committed
151 152 153 154
 * @return array
 */
function qa_db_user_get_handle_userids($handles)
{
155
	if (!empty($handles)) {
Scott committed
156 157 158 159
		return qa_db_read_all_assoc(qa_db_query_sub(
			'SELECT handle, userid FROM ^users WHERE handle IN ($)',
			$handles
		), 'handle', 'userid');
Scott committed
160 161
	}

Scott committed
162 163 164 165 166
	return array();
}


/**
167 168 169 170 171
 * Set $field of $userid to $value in the database users table. If the $fields parameter is an array, the $value
 * parameter is ignored and each element of the array is treated as a key-value pair of user fields and values.
 * @param mixed $userid
 * @param string|array $fields
 * @param string|null $value
172
 * @throws \Q2A\Database\Exceptions\QueryException
Scott committed
173
 */
174
function qa_db_user_set($userid, $fields, $value = null)
Scott committed
175
{
176
	if (!is_array($fields)) {
177
		$fields = [$fields => $value];
178 179
	}

180
	$columnRegex = '/^[A-Za-z0-9_]+$/'; // simple and secure way to ensure only valid fields are used
181 182
	$sql = 'UPDATE ^users SET ';
	foreach ($fields as $field => $fieldValue) {
183 184 185 186
		if (!preg_match($columnRegex, $field)) {
			throw new \Q2A\Database\Exceptions\QueryException('Invalid field supplied to qa_db_user_set');
		}
		$sql .= "`$field` = ?, ";
187
	}
188
	$sql = substr($sql, 0, -2) . ' WHERE userid = ?';
189 190 191 192

	$params = array_values($fields);
	$params[] = $userid;

193
	qa_service('database')->query($sql, $params);
Scott committed
194 195 196 197 198
}


/**
 * Set the password of $userid to $password, and reset their salt at the same time
199 200
 * @param mixed $userid
 * @param string $password
Scott committed
201 202 203 204 205 206 207 208 209
 * @return mixed
 */
function qa_db_user_set_password($userid, $password)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'util/string.php';

	if (QA_PASSWORD_HASH) {
Scott committed
210
		qa_db_query_sub(
Scott committed
211 212
			'UPDATE ^users SET passhash=$, passsalt=NULL, passcheck=NULL WHERE userid=$',
			password_hash($password, PASSWORD_BCRYPT), $userid
Scott committed
213
		);
Scott committed
214 215
	} else {
		$salt = qa_random_alphanum(16);
Scott committed
216 217

		qa_db_query_sub(
Scott committed
218 219
			'UPDATE ^users SET passsalt=$, passcheck=UNHEX($) WHERE userid=$',
			$salt, qa_db_calc_passcheck($password, $salt), $userid
Scott committed
220 221
		);
	}
Scott committed
222 223 224 225 226
}


/**
 * Switch on the $flag bit of the flags column for $userid if $set is true, or switch off otherwise
227 228 229
 * @param mixed $userid
 * @param int $flag
 * @param bool $set
Scott committed
230 231 232 233 234 235 236 237 238 239 240 241
 */
function qa_db_user_set_flag($userid, $flag, $set)
{
	qa_db_query_sub(
		'UPDATE ^users SET flags=flags' . ($set ? '|' : '&~') . '# WHERE userid=$',
		$flag, $userid
	);
}


/**
 * Return a random string to be used for a user's emailcode column
242
 * @return string
Scott committed
243 244 245 246 247 248 249 250 251 252 253 254 255
 */
function qa_db_user_rand_emailcode()
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'util/string.php';

	return qa_random_alphanum(8);
}


/**
 * Return a random string to be used for a user's sessioncode column (for browser session cookies)
256
 * @return string
Scott committed
257 258 259 260 261 262 263 264 265 266 267 268 269
 */
function qa_db_user_rand_sessioncode()
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }

	require_once QA_INCLUDE_DIR . 'util/string.php';

	return qa_random_alphanum(8);
}


/**
 * Set a row in the database user profile table to store $value for $field for $userid
270 271 272
 * @param mixed $userid
 * @param string $field
 * @param string $value
Scott committed
273 274 275 276 277 278 279 280 281 282 283 284 285
 */
function qa_db_user_profile_set($userid, $field, $value)
{
	qa_db_query_sub(
		'INSERT INTO ^userprofile (userid, title, content) VALUES ($, $, $) ' .
		'ON DUPLICATE KEY UPDATE content = VALUES(content)',
		$userid, $field, $value
	);
}


/**
 * Note in the database that $userid just logged in from $ip address
286 287
 * @param mixed $userid
 * @param string $ip
Scott committed
288 289 290 291
 */
function qa_db_user_logged_in($userid, $ip)
{
	qa_db_query_sub(
292 293
		'UPDATE ^users SET loggedin=NOW(), loginip=UNHEX($) WHERE userid=$',
		bin2hex(@inet_pton($ip)), $userid
Scott committed
294 295 296 297 298 299
	);
}


/**
 * Note in the database that $userid just performed a write operation from $ip address
300 301
 * @param mixed $userid
 * @param string $ip
Scott committed
302 303 304 305
 */
function qa_db_user_written($userid, $ip)
{
	qa_db_query_sub(
306 307
		'UPDATE ^users SET written=NOW(), writeip=UNHEX($) WHERE userid=$',
		bin2hex(@inet_pton($ip)), $userid
Scott committed
308 309 310 311 312 313
	);
}


/**
 * Add an external login in the database for $source and $identifier for user $userid
314 315 316
 * @param mixed $userid
 * @param string $source
 * @param string $identifier
Scott committed
317 318 319 320 321 322 323 324 325 326 327 328 329
 */
function qa_db_user_login_add($userid, $source, $identifier)
{
	qa_db_query_sub(
		'INSERT INTO ^userlogins (userid, source, identifier, identifiermd5) ' .
		'VALUES ($, $, $, UNHEX($))',
		$userid, $source, $identifier, md5($identifier)
	);
}


/**
 * Return some information about the user with external login $source and $identifier in the database, if a match is found
330 331
 * @param string $source
 * @param string $identifier
Scott committed
332 333 334 335 336 337 338 339 340 341 342 343 344 345
 * @return array
 */
function qa_db_user_login_find($source, $identifier)
{
	return qa_db_read_all_assoc(qa_db_query_sub(
		'SELECT ^userlogins.userid, handle, email FROM ^userlogins LEFT JOIN ^users ON ^userlogins.userid=^users.userid ' .
		'WHERE source=$ AND identifiermd5=UNHEX($) AND identifier=$',
		$source, md5($identifier), $identifier
	));
}


/**
 * Lock all tables if $sync is true, otherwise unlock them. Used to synchronize creation of external login mappings.
346
 * @param bool $sync
Scott committed
347 348 349 350 351 352 353 354 355 356 357 358 359 360
 */
function qa_db_user_login_sync($sync)
{
	if ($sync) { // need to lock all tables since any could be used by a plugin's event module
		$tables = qa_db_list_tables();

		$locks = array();
		foreach ($tables as $table)
			$locks[] = $table . ' WRITE';

		qa_db_query_sub('LOCK TABLES ' . implode(', ', $locks));

	} else {
		qa_db_query_sub('UNLOCK TABLES');
Scott committed
361
	}
Scott committed
362 363 364 365 366 367
}


/**
 * Reset the full set of context-specific (currently, per category) user levels for user $userid to $userlevels, where
 * $userlevels is an array of arrays, the inner arrays containing items 'entitytype', 'entityid' and 'level'.
368 369
 * @param mixed $userid
 * @param array $userlevels
Scott committed
370 371 372 373 374 375 376 377 378
 */
function qa_db_user_levels_set($userid, $userlevels)
{
	qa_db_query_sub(
		'DELETE FROM ^userlevels WHERE userid=$',
		$userid
	);

	foreach ($userlevels as $userlevel) {
Scott committed
379
		qa_db_query_sub(
Scott committed
380 381 382
			'INSERT INTO ^userlevels (userid, entitytype, entityid, level) VALUES ($, $, #, #) ' .
			'ON DUPLICATE KEY UPDATE level = VALUES(level)',
			$userid, $userlevel['entitytype'], $userlevel['entityid'], $userlevel['level']
Scott committed
383 384
		);
	}
Scott committed
385 386 387 388 389
}


/**
 * Get the information required for sending a mailing to the next $count users with userids greater than $lastuserid
390 391
 * @param mixed $lastuserid
 * @param int $count
Scott committed
392 393 394 395 396
 * @return array
 */
function qa_db_users_get_mailing_next($lastuserid, $count)
{
	return qa_db_read_all_assoc(qa_db_query_sub(
397
		'SELECT userid, email, handle, emailcode, flags, level FROM ^users WHERE userid># ORDER BY userid LIMIT #',
Scott committed
398 399 400 401 402 403 404 405 406 407 408
		$lastuserid, $count
	));
}


/**
 * Update the cached count of the number of users who are awaiting approval after registration
 */
function qa_db_uapprovecount_update()
{
	if (qa_should_update_counts() && !QA_FINAL_EXTERNAL_USERS) {
Scott committed
409
		qa_db_query_sub(
Scott committed
410 411 412 413 414
			"INSERT INTO ^options (title, content) " .
			"SELECT 'cache_uapprovecount', COUNT(*) FROM ^users " .
			"WHERE level < # AND NOT (flags & #) " .
			"ON DUPLICATE KEY UPDATE content = VALUES(content)",
			QA_USER_LEVEL_APPROVED, QA_USER_FLAGS_USER_BLOCKED
Scott committed
415 416
		);
	}
Scott committed
417
}