users-edit.php 16.7 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 22
<?php
/*
	Question2Answer by Gideon Greenspan and contributors
	http://www.question2answer.org/

	File: qa-include/qa-app-users-edit.php
	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
23 24 25 26
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
	header('Location: ../');
	exit;
}
Scott committed
27

Scott committed
28 29
@define('QA_MIN_PASSWORD_LEN', 4);
@define('QA_NEW_PASSWORD_LEN', 8); // when resetting password
Scott committed
30 31


Scott committed
32 33 34 35 36 37 38 39 40 41 42 43
/**
 * 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
44

Scott committed
45
	$errors = array();
Scott committed
46

Scott committed
47 48
	// sanitise 4-byte Unicode
	$handle = qa_remove_utf8mb4($handle);
49

Scott committed
50
	$filtermodules = qa_load_modules_with('filter', 'filter_handle');
Scott committed
51

Scott committed
52 53 54 55 56
	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->filter_handle($handle, $olduser);
		if (isset($error)) {
			$errors['handle'] = $error;
			break;
Scott committed
57
		}
Scott committed
58
	}
Scott committed
59

Scott committed
60 61 62 63 64
	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
65

Scott committed
66
	$filtermodules = qa_load_modules_with('filter', 'filter_email');
Scott committed
67

Scott committed
68 69 70 71 72 73
	$error = null;
	foreach ($filtermodules as $filtermodule) {
		$error = $filtermodule->filter_email($email, $olduser);
		if (isset($error)) {
			$errors['email'] = $error;
			break;
Scott committed
74 75 76
		}
	}

Scott committed
77 78 79 80 81
	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
82

Scott committed
83 84
	return $errors;
}
Scott committed
85 86


Scott committed
87 88 89 90 91 92 93 94 95 96
/**
 * 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
97

Scott committed
98 99
	if (!strlen($handle))
		$handle = qa_lang('users/registered_user');
Scott committed
100

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

Scott committed
103 104 105
	for ($attempt = 0; $attempt <= 99; $attempt++) {
		$suffix = $attempt ? (' ' . $attempt) : '';
		$tryhandle = qa_substr($handle, 0, QA_DB_MAX_HANDLE_LENGTH - strlen($suffix)) . $suffix;
Scott committed
106

Scott committed
107 108 109 110
		$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
111 112
		}

Scott committed
113
		$haderror = false;
Scott committed
114 115

		foreach ($filtermodules as $filtermodule) {
Scott committed
116
			$error = $filtermodule->filter_handle($tryhandle, null); // now check for errors after we've filtered
Scott committed
117
			if (isset($error))
Scott committed
118
				$haderror = true;
Scott committed
119 120
		}

Scott committed
121 122 123 124
		if (!$haderror) {
			$handleusers = qa_db_user_find_by_handle($tryhandle);
			if (!count($handleusers))
				return $tryhandle;
Scott committed
125
		}
Scott committed
126
	}
Scott committed
127

Scott committed
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
	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
146
		if (isset($error))
Scott committed
147 148
			break;
	}
Scott committed
149

Scott committed
150 151 152 153
	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
154 155
	}

Scott committed
156 157
	if (isset($error))
		return array('password' => $error);
Scott committed
158

Scott committed
159 160
	return array();
}
Scott committed
161 162


Scott committed
163 164 165 166 167 168 169 170 171 172 173 174 175 176
/**
 * 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
177

Scott committed
178 179 180 181 182
	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
183

Scott committed
184 185 186
	$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
187

Scott committed
188 189
	if ($confirmed)
		qa_db_user_set_flag($userid, QA_USER_FLAGS_EMAIL_CONFIRMED, true);
Scott committed
190

Scott committed
191 192
	if (qa_opt('show_notice_welcome'))
		qa_db_user_set_flag($userid, QA_USER_FLAGS_WELCOME_NOTICE, true);
Scott committed
193

Scott committed
194
	$custom = qa_opt('show_custom_welcome') ? trim(qa_opt('custom_welcome')) : '';
Scott committed
195

Scott committed
196 197 198 199
	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
200

Scott committed
201 202
		if (qa_opt('confirm_user_required'))
			qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_CONFIRM, true);
Scott committed
203

Scott committed
204 205
	} else
		$confirm = '';
Scott committed
206

Scott committed
207 208
	if (qa_opt('moderate_users') && qa_opt('approve_user_required') && $level < QA_USER_LEVEL_EXPERT)
		qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_APPROVE, true);
Scott committed
209

Scott committed
210 211 212 213 214 215
	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
216

Scott committed
217 218 219 220
	qa_report_event('u_register', $userid, $handle, qa_cookie_get(), array(
		'email' => $email,
		'level' => $level,
	));
Scott committed
221

Scott committed
222 223 224 225 226 227 228 229 230 231 232 233 234
	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
235

Scott committed
236 237 238 239
	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
240

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

Scott committed
243 244 245
	qa_db_user_delete($userid);
	qa_db_uapprovecount_update();
	qa_db_userpointscount_update();
Scott committed
246

Scott committed
247 248 249 250
	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
251

Scott committed
252
	$postuserids = qa_db_posts_get_userids($postids);
Scott committed
253

Scott committed
254 255
	foreach ($postuserids as $postuserid) {
		qa_db_points_update_ifuser($postuserid, array('avoteds', 'qvoteds', 'upvoteds', 'downvoteds'));
Scott committed
256
	}
Scott committed
257
}
Scott committed
258 259


Scott committed
260 261 262 263 264 265 266 267
/**
 * 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
268

Scott committed
269 270 271
	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
272

Scott committed
273
	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));
Scott committed
274

Scott committed
275
	$emailcode = qa_db_user_rand_emailcode();
276

Scott committed
277
	if (!qa_send_notification($userid, $userinfo['email'], $userinfo['handle'], qa_lang('emails/confirm_subject'), qa_lang('emails/confirm_body'), array(
278 279
			'^url' => qa_get_new_confirm_url($userid, $userinfo['handle'], $emailcode),
			'^code' => $emailcode,
Scott committed
280 281
	))) {
		qa_fatal_error('Could not send email confirmation');
Scott committed
282
	}
Scott committed
283
}
Scott committed
284 285


Scott committed
286 287 288 289 290 291 292 293 294 295 296
/**
 * 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
297

Scott committed
298
	require_once QA_INCLUDE_DIR . 'db/users.php';
Scott committed
299

Scott committed
300 301
	if (!isset($emailcode)) {
		$emailcode = qa_db_user_rand_emailcode();
Scott committed
302
	}
Scott committed
303
	qa_db_user_set($userid, 'emailcode', $emailcode);
Scott committed
304

Scott committed
305 306
	return qa_path_absolute('confirm', array('c' => $emailcode, 'u' => $handle));
}
Scott committed
307 308


Scott committed
309 310 311 312 313 314 315 316 317 318
/**
 * 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
319

Scott committed
320 321
	require_once QA_INCLUDE_DIR . 'db/users.php';
	require_once QA_INCLUDE_DIR . 'app/cookies.php';
Scott committed
322

Scott committed
323 324 325
	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
326

Scott committed
327 328 329 330
	qa_report_event('u_confirmed', $userid, $handle, qa_cookie_get(), array(
		'email' => $email,
	));
}
Scott committed
331 332


Scott committed
333 334 335 336 337 338 339 340 341 342 343
/**
 * Set the user level of user $userid with $handle to $level (one of the QA_USER_LEVEL_* constraints in qa-app-users.php)
 * 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
344

Scott committed
345 346
	qa_db_user_set($userid, 'level', $level);
	qa_db_uapprovecount_update();
Scott committed
347

Scott committed
348 349
	if ($level >= QA_USER_LEVEL_APPROVED)
		qa_db_user_set_flag($userid, QA_USER_FLAGS_MUST_APPROVE, false);
Scott committed
350

Scott committed
351 352 353 354 355 356 357
	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
358 359


Scott committed
360 361 362 363 364 365 366 367 368 369
/**
 * 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
370

Scott committed
371 372
	qa_db_user_set_flag($userid, QA_USER_FLAGS_USER_BLOCKED, $blocked);
	qa_db_uapprovecount_update();
Scott committed
373

Scott committed
374 375 376 377 378
	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
379 380


Scott committed
381 382 383 384 385 386 387 388
/**
 * 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
389

Scott committed
390 391 392 393
	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
394

Scott committed
395
	qa_db_user_set($userid, 'emailcode', qa_db_user_rand_emailcode());
Scott committed
396

Scott committed
397 398 399 400 401 402 403
	$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
404
	}
Scott committed
405
}
Scott committed
406 407


408 409 410 411
/**
 * 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
412 413
 * @param $userid
 * @return mixed
414
 */
Scott committed
415 416 417
function qa_complete_reset_user($userid)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott committed
418

Scott committed
419 420 421 422 423
	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
424

Scott committed
425
	$password = qa_random_alphanum(max(QA_MIN_PASSWORD_LEN, QA_NEW_PASSWORD_LEN));
Scott committed
426

Scott committed
427
	$userinfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true));
Scott committed
428

Scott committed
429 430 431 432 433 434
	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
435

Scott committed
436 437
	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
438

Scott committed
439 440 441 442
	qa_report_event('u_reset', $userid, $userinfo['handle'], qa_cookie_get(), array(
		'email' => $userinfo['email'],
	));
}
Scott committed
443 444


445 446 447 448 449 450
/**
 * 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
451 452 453
function qa_finish_reset_user($userId, $newPassword)
{
	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
454

Scott committed
455 456
	// For qa_db_user_set_password(), qa_db_user_set()
	require_once QA_INCLUDE_DIR . 'db/users.php';
457

Scott committed
458 459
	// For qa_set_logged_in_user()
	require_once QA_INCLUDE_DIR . 'app/options.php';
460

Scott committed
461 462
	// For qa_cookie_get()
	require_once QA_INCLUDE_DIR . 'app/cookies.php';
463

Scott committed
464 465
	// For qa_db_select_with_pending(), qa_db_user_account_selectspec()
	require_once QA_INCLUDE_DIR . 'db/selects.php';
466

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

Scott committed
470
	qa_db_user_set_password($userId, $newPassword);
471

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

Scott committed
474
	$userInfo = qa_db_select_with_pending(qa_db_user_account_selectspec($userId, true));
475

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

Scott committed
478 479 480 481
	qa_report_event('u_reset', $userId, $userInfo['handle'], qa_cookie_get(), array(
		'email' => $userInfo['email'],
	));
}
482

Scott committed
483 484 485 486 487 488
/**
 * 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
489

Scott committed
490 491
	$qa_cached_logged_in_user = null;
}
Scott committed
492 493


Scott committed
494 495 496 497 498 499 500 501 502 503
/**
 * 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
504

Scott committed
505
	require_once QA_INCLUDE_DIR . 'util/image.php';
Scott committed
506

Scott committed
507
	$imagedata = qa_image_constrain_data($imagedata, $width, $height, qa_opt('avatar_store_size'));
Scott committed
508

Scott committed
509 510
	if (isset($imagedata)) {
		require_once QA_INCLUDE_DIR . 'app/blobs.php';
Scott committed
511

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

Scott committed
514 515 516 517 518 519
		if (isset($newblobid)) {
			qa_db_user_set($userid, 'avatarblobid', $newblobid);
			qa_db_user_set($userid, 'avatarwidth', $width);
			qa_db_user_set($userid, 'avatarheight', $height);
			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
520

Scott committed
521 522
			if (isset($oldblobid))
				qa_delete_blob($oldblobid);
Scott committed
523

Scott committed
524
			return true;
Scott committed
525 526 527
		}
	}

Scott committed
528 529
	return false;
}