users-edit.php 16.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: User management (application level) for creating/modifying users


	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
	exit;
}
Scott committed
26

27
if (!defined('QA_MIN_PASSWORD_LEN')) {
28
	define('QA_MIN_PASSWORD_LEN', 8);
29 30 31 32 33 34 35 36
}

if (!defined('QA_NEW_PASSWORD_LEN')){
	/**
	 * @deprecated This was the length of the reset password generated by Q2A. No longer used.
	 */
	define('QA_NEW_PASSWORD_LEN', 8);
}
Scott committed
37 38


Scott committed
39 40 41 42 43 44 45 46 47 48 49 50
/**
 * Return $errors fields for any invalid aspect of user-entered $handle (username) and $email. Works by calling through
 * to all filter modules and also rejects existing values in database unless they belongs to $olduser (if set).
 * @param $handle
 * @param $email
 * @param $olduser
 * @return array
 */
function qa_handle_email_filter(&$handle, &$email, $olduser = null)
{
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'util/string.php';
Scott committed
51

Scott committed
52
	$errors = array();
Scott committed
53

Scott committed
54 55
	// sanitise 4-byte Unicode
	$handle = qa_remove_utf8mb4($handle);
56

Scott committed
57
	$filtermodules = qa_load_modules_with('filter', 'filter_handle');
Scott committed
58

Scott committed
59 60 61 62 63
	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->filter_handle($handle, $olduser);
		if (isset($error)) {
			$errors['handle'] = $error;
			break;
Scott committed
64
		}
Scott committed
65
	}
Scott committed
66

Scott committed
67 68 69 70 71
	if (!isset($errors['handle'])) { // first test through filters, then check for duplicates here
		$handleusers = qa_db_user_find_by_handle($handle);
		if (count($handleusers) && ((!isset($olduser['userid'])) || (array_search($olduser['userid'], $handleusers) === false)))
			$errors['handle'] = qa_lang('users/handle_exists');
	}
Scott committed
72

Scott committed
73
	$filtermodules = qa_load_modules_with('filter', 'filter_email');
Scott committed
74

Scott committed
75 76 77 78 79 80
	$error = null;
	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->filter_email($email, $olduser);
		if (isset($error)) {
			$errors['email'] = $error;
			break;
Scott committed
81 82 83
		}
	}

Scott committed
84 85 86 87 88
	if (!isset($errors['email'])) {
		$emailusers = qa_db_user_find_by_email($email);
		if (count($emailusers) && ((!isset($olduser['userid'])) || (array_search($olduser['userid'], $emailusers) === false)))
			$errors['email'] = qa_lang('users/email_exists');
	}
Scott committed
89

Scott committed
90 91
	return $errors;
}
Scott committed
92 93


Scott committed
94 95 96 97 98 99 100 101 102 103
/**
 * Make $handle valid and unique in the database - if $allowuserid is set, allow it to match that user only
 * @param $handle
 * @return string
 */
function qa_handle_make_valid($handle)
{
	require_once QA_INCLUDE_DIR . 'util/string.php';
	require_once QA_INCLUDE_DIR . 'db/maxima.php';
	require_once QA_INCLUDE_DIR . 'db/users.php';
Scott committed
104

Scott committed
105 106
	if (!strlen($handle))
		$handle = qa_lang('users/registered_user');
Scott committed
107

Scott committed
108
	$handle = preg_replace('/[\\@\\+\\/]/', ' ', $handle);
Scott committed
109

Scott committed
110 111 112
	for ($attempt = 0; $attempt <= 99; $attempt++) {
		$suffix = $attempt ? (' ' . $attempt) : '';
		$tryhandle = qa_substr($handle, 0, QA_DB_MAX_HANDLE_LENGTH - strlen($suffix)) . $suffix;
Scott committed
113

Scott committed
114 115 116 117
		$filtermodules = qa_load_modules_with('filter', 'filter_handle');
		foreach ($filtermodules as $filtermodule) {
			// filter first without worrying about errors, since our goal is to get a valid one
			$filtermodule->filter_handle($tryhandle, null);
Scott committed
118 119
		}

Scott committed
120
		$haderror = false;
Scott committed
121 122

		foreach ($filtermodules as $filtermodule) {
Scott committed
123
			$error = $filtermodule->filter_handle($tryhandle, null); // now check for errors after we've filtered
Scott committed
124
			if (isset($error))
Scott committed
125
				$haderror = true;
Scott committed
126 127
		}

Scott committed
128 129 130 131
		if (!$haderror) {
			$handleusers = qa_db_user_find_by_handle($tryhandle);
			if (!count($handleusers))
				return $tryhandle;
Scott committed
132
		}
Scott committed
133
	}
Scott committed
134

Scott committed
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
	qa_fatal_error('Could not create a valid and unique handle from: ' . $handle);
}


/**
 * Return an array with a single element (key 'password') if user-entered $password is valid, otherwise an empty array.
 * Works by calling through to all filter modules.
 * @param $password
 * @param $olduser
 * @return array
 */
function qa_password_validate($password, $olduser = null)
{
	$error = null;
	$filtermodules = qa_load_modules_with('filter', 'validate_password');

	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->validate_password($password, $olduser);
Scott committed
153
		if (isset($error))
Scott committed
154 155
			break;
	}
Scott committed
156

Scott committed
157 158 159 160
	if (!isset($error)) {
		$minpasslen = max(QA_MIN_PASSWORD_LEN, 1);
		if (qa_strlen($password) < $minpasslen)
			$error = qa_lang_sub('users/password_min', $minpasslen);
Scott committed
161 162
	}

Scott committed
163 164
	if (isset($error))
		return array('password' => $error);
Scott committed
165

Scott committed
166 167
	return array();
}
Scott committed
168 169


Scott committed
170 171 172 173 174 175 176 177 178 179 180 181 182 183
/**
 * Create a new user (application level) with $email, $password, $handle and $level.
 * Set $confirmed to true if the email address has been confirmed elsewhere.
 * Handles user points, notification and optional email confirmation.
 * @param $email
 * @param $password
 * @param $handle
 * @param int $level
 * @param bool $confirmed
 * @return mixed
 */
function qa_create_new_user($email, $password, $handle, $level = QA_USER_LEVEL_BASIC, $confirmed = false)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
184

Scott committed
185 186 187 188 189
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'db/points.php';
	require_once QA_INCLUDE_DIR . 'app/options.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';
	require_once QA_INCLUDE_DIR . 'app/cookies.php';
Scott committed
190

Scott committed
191 192 193
	$userid = qa_db_user_create($email, $password, $handle, $level, qa_remote_ip_address());
	qa_db_points_update_ifuser($userid, null);
	qa_db_uapprovecount_update();
Scott committed
194

Scott committed
195 196
	if ($confirmed)
		qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, true);
Scott committed
197

Scott committed
198 199
	if (qa_opt('show_notice_welcome'))
		qa_db_user_set_flag($userid, QA_USER_FLAGS_WELCOME_NOTICE, true);
Scott committed
200

Scott committed
201
	$custom = qa_opt('show_custom_welcome') ? trim(qa_opt('custom_welcome')) : '';
Scott committed
202

Scott committed
203 204 205 206
	if (qa_opt('confirm_user_emails') && $level < QA_USER_LEVEL_EXPERT && !$confirmed) {
		$confirm = strtr(qa_lang('emails/welcome_confirm'), array(
			'^url' => qa_get_new_confirm_url($userid, $handle),
		));
Scott committed
207

Scott committed
208 209
		if (qa_opt('confirm_user_required'))
			qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_CONFIRM, true);
Scott committed
210

Scott committed
211 212
	} else
		$confirm = '';
Scott committed
213

214
	// we no longer use the 'approve_user_required' option to set QA_USER_FLAGS_MUST_APPROVE; this can be handled by the Permissions settings
Scott committed
215

Scott committed
216 217 218 219 220 221
	qa_send_notification($userid, $email, $handle, qa_lang('emails/welcome_subject'), qa_lang('emails/welcome_body'), array(
		'^password' => isset($password) ? qa_lang('main/hidden') : qa_lang('users/password_to_set'), // v 1.6.3: no longer email out passwords
		'^url' => qa_opt('site_url'),
		'^custom' => strlen($custom) ? ($custom . "\n\n") : '',
		'^confirm' => $confirm,
	));
Scott committed
222

Scott committed
223 224 225 226
	qa_report_event('u_register', $userid, $handle, qa_cookie_get(), array(
		'email' => $email,
		'level' => $level,
	));
Scott committed
227

Scott committed
228 229 230 231 232 233 234 235 236 237 238 239 240
	return $userid;
}


/**
 * Delete $userid and all their votes and flags. Their posts will become anonymous.
 * Handles recalculations of votes and flags for posts this user has affected.
 * @param $userid
 * @return mixed
 */
function qa_delete_user($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
241

Scott committed
242 243 244 245
	require_once QA_INCLUDE_DIR . 'db/votes.php';
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'db/post-update.php';
	require_once QA_INCLUDE_DIR . 'db/points.php';
Scott committed
246

Scott committed
247
	$postids = qa_db_uservoteflag_user_get($userid); // posts this user has flagged or voted on, whose counts need updating
Scott committed
248

Scott committed
249 250 251
	qa_db_user_delete($userid);
	qa_db_uapprovecount_update();
	qa_db_userpointscount_update();
Scott committed
252

Scott committed
253 254 255 256
	foreach ($postids as $postid) { // hoping there aren't many of these - saves a lot of new SQL code...
		qa_db_post_recount_votes($postid);
		qa_db_post_recount_flags($postid);
	}
Scott committed
257

Scott committed
258
	$postuserids = qa_db_posts_get_userids($postids);
Scott committed
259

Scott committed
260 261
	foreach ($postuserids as $postuserid) {
		qa_db_points_update_ifuser($postuserid, array('avoteds', 'qvoteds', 'upvoteds', 'downvoteds'));
Scott committed
262
	}
Scott committed
263
}
Scott committed
264 265


Scott committed
266 267 268 269 270 271 272 273
/**
 * Set a new email confirmation code for the user and send it out
 * @param $userid
 * @return mixed
 */
function qa_send_new_confirm($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
274

Scott committed
275 276 277
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'db/selects.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';
Scott committed
278

Scott committed
279
	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));
Scott committed
280

Scott committed
281
	$emailcode = qa_db_user_rand_emailcode();
282

Scott committed
283
	if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/confirm_subject'), qa_lang('emails/confirm_body'), array(
284 285
			'^url' => qa_get_new_confirm_url($userid, $userinfo['handle'], $emailcode),
			'^code' => $emailcode,
Scott committed
286 287
	))) {
		qa_fatal_error('Could not send email confirmation');
Scott committed
288
	}
Scott committed
289
}
Scott committed
290 291


Scott committed
292 293 294 295 296 297 298 299 300 301 302
/**
 * Set a new email confirmation code for the user and return the corresponding link. If the email code is also sent then that value
 * is used. Otherwise, a new email code is generated
 * @param $userid
 * @param $handle
 * @param $emailcode
 * @return mixed|string
 */
function qa_get_new_confirm_url($userid, $handle, $emailcode = null)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
303

Scott committed
304
	require_once QA_INCLUDE_DIR . 'db/users.php';
Scott committed
305

Scott committed
306 307
	if (!isset($emailcode)) {
		$emailcode = qa_db_user_rand_emailcode();
Scott committed
308
	}
Scott committed
309
	qa_db_user_set($userid, 'emailcode', $emailcode);
Scott committed
310

Scott committed
311 312
	return qa_path_absolute('confirm', array('c' => $emailcode, 'u' => $handle));
}
Scott committed
313 314


Scott committed
315 316 317 318 319 320 321 322 323 324
/**
 * Complete the email confirmation process for the user
 * @param $userid
 * @param $email
 * @param $handle
 * @return mixed
 */
function qa_complete_confirm($userid, $email, $handle)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
325

Scott committed
326 327
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'app/cookies.php';
Scott committed
328

Scott committed
329 330 331
	qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, true);
	qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_CONFIRM, false);
	qa_db_user_set($userid, 'emailcode', ''); // to prevent re-use of the code
Scott committed
332

Scott committed
333 334 335 336
	qa_report_event('u_confirmed', $userid, $handle, qa_cookie_get(), array(
		'email' => $email,
	));
}
Scott committed
337 338


Scott committed
339
/**
340
 * Set the user level of user $userid with $handle to $level (one of the QA_USER_LEVEL_* constraints in /qa-include/app/users.php)
Scott committed
341 342 343 344 345 346 347 348 349
 * Pass the previous user level in $oldlevel. Reports the appropriate event, assumes change performed by the logged in user.
 * @param $userid
 * @param $handle
 * @param $level
 * @param $oldlevel
 */
function qa_set_user_level($userid, $handle, $level, $oldlevel)
{
	require_once QA_INCLUDE_DIR . 'db/users.php';
Scott committed
350

Scott committed
351 352
	qa_db_user_set($userid, 'level', $level);
	qa_db_uapprovecount_update();
Scott committed
353

354 355
	if ($level >= QA_USER_LEVEL_APPROVED) {
		// no longer necessary as QA_USER_FLAGS_MUST_APPROVE is deprecated, but kept for posterity
Scott committed
356
		qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_APPROVE, false);
357
	}
Scott committed
358

Scott committed
359 360 361 362 363 364 365
	qa_report_event('u_level', qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), array(
		'userid' => $userid,
		'handle' => $handle,
		'level' => $level,
		'oldlevel' => $oldlevel,
	));
}
Scott committed
366 367


Scott committed
368 369 370 371 372 373 374 375 376 377
/**
 * Set the status of user $userid with $handle to blocked if $blocked is true, otherwise to unblocked. Reports the appropriate
 * event, assumes change performed by the logged in user.
 * @param $userid
 * @param $handle
 * @param $blocked
 */
function qa_set_user_blocked($userid, $handle, $blocked)
{
	require_once QA_INCLUDE_DIR . 'db/users.php';
Scott committed
378

Scott committed
379 380
	qa_db_user_set_flag($userid, QA_USER_FLAGS_USER_BLOCKED, $blocked);
	qa_db_uapprovecount_update();
Scott committed
381

Scott committed
382 383 384 385 386
	qa_report_event($blocked ? 'u_block' : 'u_unblock', qa_get_logged_in_userid(), qa_get_logged_in_handle(), qa_cookie_get(), array(
		'userid' => $userid,
		'handle' => $handle,
	));
}
Scott committed
387 388


Scott committed
389 390 391 392 393 394 395 396
/**
 * Start the 'I forgot my password' process for $userid, sending reset code
 * @param $userid
 * @return mixed
 */
function qa_start_reset_user($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
397

Scott committed
398 399 400 401
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'app/options.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';
	require_once QA_INCLUDE_DIR . 'db/selects.php';
Scott committed
402

Scott committed
403
	qa_db_user_set($userid, 'emailcode', qa_db_user_rand_emailcode());
Scott committed
404

Scott committed
405 406 407 408 409 410 411
	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));

	if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/reset_subject'), qa_lang('emails/reset_body'), array(
		'^code' => $userinfo['emailcode'],
		'^url' => qa_path_absolute('reset', array('c' => $userinfo['emailcode'], 'e' => $userinfo['email'])),
	))) {
		qa_fatal_error('Could not send reset password email');
Scott committed
412
	}
Scott committed
413
}
Scott committed
414 415


416 417 418 419
/**
 * Successfully finish the 'I forgot my password' process for $userid, sending new password
 *
 * @deprecated This function has been replaced by qa_finish_reset_user since Q2A 1.8
Scott committed
420 421
 * @param $userid
 * @return mixed
422
 */
Scott committed
423 424 425
function qa_complete_reset_user($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
426

Scott committed
427 428 429 430 431
	require_once QA_INCLUDE_DIR . 'util/string.php';
	require_once QA_INCLUDE_DIR . 'app/options.php';
	require_once QA_INCLUDE_DIR . 'app/emails.php';
	require_once QA_INCLUDE_DIR . 'app/cookies.php';
	require_once QA_INCLUDE_DIR . 'db/selects.php';
Scott committed
432

Scott committed
433
	$password = qa_random_alphanum(max(QA_MIN_PASSWORD_LEN, QA_NEW_PASSWORD_LEN));
Scott committed
434

Scott committed
435
	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));
Scott committed
436

Scott committed
437 438 439 440 441 442
	if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/new_password_subject'), qa_lang('emails/new_password_body'), array(
		'^password' => $password,
		'^url' => qa_opt('site_url'),
	))) {
		qa_fatal_error('Could not send new password - password not reset');
	}
Scott committed
443

Scott committed
444 445
	qa_db_user_set_password($userid, $password); // do this last, to be safe
	qa_db_user_set($userid, 'emailcode', ''); // so can't be reused
Scott committed
446

Scott committed
447 448 449 450
	qa_report_event('u_reset', $userid, $userinfo['handle'], qa_cookie_get(), array(
		'email' => $userinfo['email'],
	));
}
Scott committed
451 452


453 454 455 456 457 458
/**
 * Successfully finish the 'I forgot my password' process for $userid, cleaning the emailcode field and logging in the user
 * @param mixed $userId The userid identifiying the user who will have the password reset
 * @param string $newPassword The new password for the user
 * @return void
 */
Scott committed
459 460 461
function qa_finish_reset_user($userId, $newPassword)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
462

Scott committed
463 464
	// For qa_db_user_set_password(), qa_db_user_set()
	require_once QA_INCLUDE_DIR . 'db/users.php';
465

Scott committed
466 467
	// For qa_set_logged_in_user()
	require_once QA_INCLUDE_DIR . 'app/options.php';
468

Scott committed
469 470
	// For qa_cookie_get()
	require_once QA_INCLUDE_DIR . 'app/cookies.php';
471

Scott committed
472 473
	// For qa_db_select_with_pending(), qa_db_user_account_selectspec()
	require_once QA_INCLUDE_DIR . 'db/selects.php';
474

Scott committed
475 476
	// For qa_set_logged_in_user()
	require_once QA_INCLUDE_DIR . 'app/users.php';
477

Scott committed
478
	qa_db_user_set_password($userId, $newPassword);
479

Scott committed
480
	qa_db_user_set($userId, 'emailcode', ''); // to prevent re-use of the code
481

Scott committed
482
	$userInfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userId, true));
483

Scott committed
484
	qa_set_logged_in_user($userId, $userInfo['handle'], false, $userInfo['sessionsource']); // reinstate this specific session
485

Scott committed
486 487 488 489
	qa_report_event('u_reset', $userId, $userInfo['handle'], qa_cookie_get(), array(
		'email' => $userInfo['email'],
	));
}
490

Scott committed
491 492 493 494 495 496
/**
 * Flush any information about the currently logged in user, so it is retrieved from database again
 */
function qa_logged_in_user_flush()
{
	global $qa_cached_logged_in_user;
Scott committed
497

Scott committed
498 499
	$qa_cached_logged_in_user = null;
}
Scott committed
500 501


Scott committed
502 503 504 505 506 507 508 509 510 511
/**
 * Set the avatar of $userid to the image in $imagedata, and remove $oldblobid from the database if not null
 * @param $userid
 * @param $imagedata
 * @param $oldblobid
 * @return bool
 */
function qa_set_user_avatar($userid, $imagedata, $oldblobid = null)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
512

Scott committed
513
	require_once QA_INCLUDE_DIR . 'util/image.php';
Scott committed
514

Scott committed
515
	$imagedata = qa_image_constrain_data($imagedata, $width, $height, qa_opt('avatar_store_size'));
Scott committed
516

Scott committed
517 518
	if (isset($imagedata)) {
		require_once QA_INCLUDE_DIR . 'app/blobs.php';
Scott committed
519

Scott committed
520
		$newblobid = qa_create_blob($imagedata, 'jpeg', null, $userid, null, qa_remote_ip_address());
Scott committed
521

Scott committed
522
		if (isset($newblobid)) {
523 524 525 526 527 528
			qa_db_user_set($userid, array(
				'avatarblobid' => $newblobid,
				'avatarwidth' => $width,
				'avatarheight' => $height,
			));

Scott committed
529 530
			qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_AVATAR, true);
			qa_db_user_set_flag($userid, QA_USER_FLAGS_SHOW_GRAVATAR, false);
Scott committed
531

Scott committed
532 533
			if (isset($oldblobid))
				qa_delete_blob($oldblobid);
Scott committed
534

Scott committed
535
			return true;
Scott committed
536 537 538
		}
	}

Scott committed
539 540
	return false;
}