acctfuncs.inc.php 42.5 KB
Newer Older
1
<?php
2
3
4
5
6
7
8
/**
 * Determine if an HTTP request variable is set
 *
 * @param string $name The request variable to test for
 *
 * @return string Return the value of the request variable, otherwise blank
 */
Dan McGee's avatar
Dan McGee committed
9
10
11
12
13
14
15
function in_request($name) {
	if (isset($_REQUEST[$name])) {
		return $_REQUEST[$name];
	}
	return "";
}

16
17
18
19
20
21
22
/**
 * Format the PGP key fingerprint
 *
 * @param string $fingerprint An unformatted PGP key fingerprint
 *
 * @return string PGP fingerprint with spaces every 4 characters
 */
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function html_format_pgp_fingerprint($fingerprint) {
	if (strlen($fingerprint) != 40 || !ctype_xdigit($fingerprint)) {
		return $fingerprint;
	}

	return htmlspecialchars(substr($fingerprint, 0, 4) . " " .
		substr($fingerprint, 4, 4) . " " .
		substr($fingerprint, 8, 4) . " " .
		substr($fingerprint, 12, 4) . " " .
		substr($fingerprint, 16, 4) . "  " .
		substr($fingerprint, 20, 4) . " " .
		substr($fingerprint, 24, 4) . " " .
		substr($fingerprint, 28, 4) . " " .
		substr($fingerprint, 32, 4) . " " .
		substr($fingerprint, 36, 4) . " ", ENT_QUOTES);
}

40
41
42
43
44
45
46
47
48
/**
 * Loads the account editing form, with any values that are already saved
 *
 * @global array $SUPPORTED_LANGS Languages that are supported by the AUR
 * @param string $A Form to use, either UpdateAccount or NewAccount
 * @param string $U The username to display
 * @param string $T The account type of the displayed user
 * @param string $S Whether the displayed user has a suspended account
 * @param string $E The e-mail address of the displayed user
49
 * @param string $H Whether the e-mail address of the displayed user is hidden
50
51
52
53
 * @param string $P The password value of the displayed user
 * @param string $C The confirmed password value of the displayed user
 * @param string $R The real name of the displayed user
 * @param string $L The language preference of the displayed user
Mark Weiman's avatar
Mark Weiman committed
54
 * @param string $TZ The timezone preference of the displayed user
55
 * @param string $HP The homepage of the displayed user
56
57
 * @param string $I The IRC nickname of the displayed user
 * @param string $K The PGP key fingerprint of the displayed user
58
 * @param string $PK The list of SSH public keys
59
 * @param string $J The inactivity status of the displayed user
60
 * @param string $CN Whether to notify of new comments
61
 * @param string $UN Whether to notify of package updates
62
 * @param string $ON Whether to notify of ownership changes
63
 * @param string $UID The user ID of the displayed user
64
 * @param string $N The username as present in the database
65
66
 * @param string $captcha_salt The salt used for the CAPTCHA.
 * @param string $captcha The CAPTCHA answer.
67
68
69
 *
 * @return void
 */
70
function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="",
71
		$L="",$TZ="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="",$captcha_salt="",$captcha="") {
72
73
	global $SUPPORTED_LANGS;

Mark Weiman's avatar
Mark Weiman committed
74
75
76
77
	if ($TZ == "") {
		$TZ = config_get("options", "default_timezone");
	}

78
79
	if (!in_array($captcha_salt, get_captcha_salts())) {
		$captcha_salt = get_captcha_salts()[0];
80
81
82
83
		$captcha = "";
	}
	$captcha_challenge = get_captcha_challenge($captcha_salt);

84
	include("account_edit_form.php");
85
	return;
86
}
87

88
89
90
91
92
93
94
95
96
97
/**
 * Process information given to new/edit account form
 *
 * @global array $SUPPORTED_LANGS Languages that are supported by the AUR
 * @param string $TYPE Either "edit" for editing or "new" for registering an account
 * @param string $A Form to use, either UpdateAccount or NewAccount
 * @param string $U The username for the account
 * @param string $T The account type for the user
 * @param string $S Whether or not the account is suspended
 * @param string $E The e-mail address for the user
98
 * @param string $H Whether or not the e-mail address should be hidden
99
100
101
102
 * @param string $P The password for the user
 * @param string $C The confirmed password for the user
 * @param string $R The real name of the user
 * @param string $L The language preference of the user
Mark Weiman's avatar
Mark Weiman committed
103
 * @param string $TZ The timezone preference of the user
104
 * @param string $HP The homepage of the displayed user
105
106
 * @param string $I The IRC nickname of the user
 * @param string $K The PGP fingerprint of the user
107
 * @param string $PK The list of public SSH keys
108
 * @param string $J The inactivity status of the user
109
 * @param string $CN Whether to notify of new comments
110
 * @param string $UN Whether to notify of package updates
111
 * @param string $ON Whether to notify of ownership changes
112
 * @param string $UID The user ID of the modified account
113
 * @param string $N The username as present in the database
114
 * @param string $passwd The password of the logged in user.
115
116
 * @param string $captcha_salt The salt used for the CAPTCHA.
 * @param string $captcha The CAPTCHA answer.
117
 *
118
 * @return array Boolean indicating success and message to be printed
119
 */
120
121
function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",
		$R="",$L="",$TZ="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="",$passwd="",$captcha_salt="",$captcha="") {
122
	global $SUPPORTED_LANGS;
123

124
	$error = '';
125
	$message = '';
126
127
128
129
130
131
132
133

	if (is_ipbanned()) {
		$error = __('Account registration has been disabled ' .
					'for your IP address, probably due ' .
					'to sustained spam attacks. Sorry for the ' .
					'inconvenience.');
	}

134
	$dbh = DB::connect();
135

136
	if (isset($_COOKIE['AURSID'])) {
137
		$uid_session = uid_from_sid($_COOKIE['AURSID']);
138
139
140
		if (!$error && check_passwd($uid_session, $passwd) != 1) {
			$error = __("Invalid password.");
		}
141
	}
142
143

	if (empty($E) || empty($U)) {
144
145
		$error = __("Missing a required field.");
	}
146

147
148
	if ($TYPE != "new" && !$UID) {
		$error = __("Missing User ID");
149
	}
150

151
	if (!$error && !valid_username($U)) {
152
153
154
		$length_min = config_get_int('options', 'username_min_len');
		$length_max = config_get_int('options', 'username_max_len');

155
		$error = __("The username is invalid.") . "<ul>\n"
156
			. "<li>" . __("It must be between %s and %s characters long", $length_min, $length_max)
157
			. "</li>"
158
			. "<li>" . __("Start and end with a letter or number") . "</li>"
159
			. "<li>" . __("Can contain only one period, underscore or hyphen.")
160
			. "</li>\n</ul>";
161
	}
162

163
164
165
166
	if (!$error && $P && !$C) {
		$error = __("Please confirm your new password.");
	}
	if (!$error && $P && $P != $C) {
167
168
		$error = __("Password fields do not match.");
	}
169
170
171
172
173
	if (!$error && $P != '' && !good_passwd($P)) {
		$length_min = config_get_int('options', 'passwd_min_len');
		$error = __("Your password must be at least %s characters.",
			$length_min);
	}
174

175
176
177
	if (!$error && !valid_email($E)) {
		$error = __("The email address is invalid.");
	}
178

179
	if (!$error && !empty($HP) && !valid_homepage($HP)) {
180
181
182
		$error = __("The home page is invalid, please specify the full HTTP(s) URL.");
	}

183
184
185
186
	if (!$error && $K != '' && !valid_pgp_fingerprint($K)) {
		$error = __("The PGP key fingerprint is invalid.");
	}

187
	if (!$error && !empty($PK)) {
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
		$ssh_keys = array_filter(array_map('trim', explode("\n", $PK)));
		$ssh_fingerprints = array();

		foreach ($ssh_keys as &$ssh_key) {
			if (!valid_ssh_pubkey($ssh_key)) {
				$error = __("The SSH public key is invalid.");
				break;
			}

			$ssh_fingerprint = ssh_key_fingerprint($ssh_key);
			if (!$ssh_fingerprint) {
				$error = __("The SSH public key is invalid.");
				break;
			}

			$tokens = explode(" ", $ssh_key);
			$ssh_key = $tokens[0] . " " . $tokens[1];

			$ssh_fingerprints[] = $ssh_fingerprint;
207
		}
208
209
210
211
212
213

		/*
		 * Destroy last reference to prevent accidentally overwriting
		 * an array element.
		 */
		unset($ssh_key);
214
215
	}

216
217
218
219
220
	if (isset($_COOKIE['AURSID'])) {
		$atype = account_from_sid($_COOKIE['AURSID']);
		if (($atype == "User" && $T > 1) || ($atype == "Trusted User" && $T > 2)) {
			$error = __("Cannot increase account permissions.");
		}
eric's avatar
eric committed
221
	}
222

223
224
225
	if (!$error && !array_key_exists($L, $SUPPORTED_LANGS)) {
		$error = __("Language is not currently supported.");
	}
Mark Weiman's avatar
Mark Weiman committed
226
227
228
	if (!$error && !array_key_exists($TZ, generate_timezone_list())) {
		$error = __("Timezone is not currently supported.");
	}
229
	if (!$error) {
230
231
232
233
		/*
		 * Check whether the user name is available.
		 * TODO: Fix race condition.
		 */
234
		$q = "SELECT COUNT(*) AS CNT FROM Users ";
canyonknight's avatar
canyonknight committed
235
		$q.= "WHERE Username = " . $dbh->quote($U);
eric's avatar
eric committed
236
237
238
		if ($TYPE == "edit") {
			$q.= " AND ID != ".intval($UID);
		}
canyonknight's avatar
canyonknight committed
239
240
241
242
243
		$result = $dbh->query($q);
		$row = $result->fetch(PDO::FETCH_NUM);

		if ($row[0]) {
			$error = __("The username, %s%s%s, is already in use.",
Lukas Fleischer's avatar
Lukas Fleischer committed
244
				"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
245
246
247
		}
	}
	if (!$error) {
248
249
250
251
		/*
		 * Check whether the e-mail address is available.
		 * TODO: Fix race condition.
		 */
252
		$q = "SELECT COUNT(*) AS CNT FROM Users ";
canyonknight's avatar
canyonknight committed
253
		$q.= "WHERE Email = " . $dbh->quote($E);
eric's avatar
eric committed
254
255
256
		if ($TYPE == "edit") {
			$q.= " AND ID != ".intval($UID);
		}
canyonknight's avatar
canyonknight committed
257
258
259
260
261
		$result = $dbh->query($q);
		$row = $result->fetch(PDO::FETCH_NUM);

		if ($row[0]) {
			$error = __("The address, %s%s%s, is already in use.",
Lukas Fleischer's avatar
Lukas Fleischer committed
262
					"<strong>", htmlspecialchars($E,ENT_QUOTES), "</strong>");
263
264
		}
	}
265
	if (!$error && isset($ssh_keys) && count($ssh_keys) > 0) {
266
		/*
267
		 * Check whether any of the SSH public keys is already in use.
268
269
		 * TODO: Fix race condition.
		 */
270
271
272
273
		$q = "SELECT Fingerprint FROM SSHPubKeys ";
		$q.= "WHERE Fingerprint IN (";
		$q.= implode(',', array_map(array($dbh, 'quote'), $ssh_fingerprints));
		$q.= ")";
274
		if ($TYPE == "edit") {
275
			$q.= " AND UserID != " . intval($UID);
276
277
278
279
		}
		$result = $dbh->query($q);
		$row = $result->fetch(PDO::FETCH_NUM);

280
		if ($row) {
281
			$error = __("The SSH public key, %s%s%s, is already in use.",
282
					"<strong>", htmlspecialchars($row[0], ENT_QUOTES), "</strong>");
283
284
		}
	}
285

286
287
288
289
	if (!$error && $TYPE == "new" && empty($captcha)) {
		$error = __("The CAPTCHA is missing.");
	}

290
	if (!$error && $TYPE == "new" && !in_array($captcha_salt, get_captcha_salts())) {
291
292
293
294
295
296
297
		$error = __("This CAPTCHA has expired. Please try again.");
	}

	if (!$error && $TYPE == "new" && $captcha != get_captcha_answer($captcha_salt)) {
		$error = __("The entered CAPTCHA answer is invalid.");
	}

298
	if ($error) {
299
300
		$message = "<ul class='errorlist'><li>".$error."</li></ul>\n";
		return array(false, $message);
301
302
303
304
305
306
307
308
309
	}

	if ($TYPE == "new") {
		/* Create an unprivileged user. */
		if (empty($P)) {
			$send_resetkey = true;
			$email = $E;
		} else {
			$send_resetkey = false;
Lukas Fleischer's avatar
Lukas Fleischer committed
310
			$P = password_hash($P, PASSWORD_DEFAULT);
311
312
313
314
315
316
		}
		$U = $dbh->quote($U);
		$E = $dbh->quote($E);
		$P = $dbh->quote($P);
		$R = $dbh->quote($R);
		$L = $dbh->quote($L);
Mark Weiman's avatar
Mark Weiman committed
317
		$TZ = $dbh->quote($TZ);
318
		$HP = $dbh->quote($HP);
319
320
321
		$I = $dbh->quote($I);
		$K = $dbh->quote(str_replace(" ", "", $K));
		$q = "INSERT INTO Users (AccountTypeID, Suspended, ";
Lukas Fleischer's avatar
Lukas Fleischer committed
322
		$q.= "InactivityTS, Username, Email, Passwd , ";
Mark Weiman's avatar
Mark Weiman committed
323
		$q.= "RealName, LangPreference, Timezone, Homepage, IRCNick, PGPKey) ";
324
		$q.= "VALUES (1, 0, 0, $U, $E, $P, $R, $L, $TZ, ";
325
		$q.= "$HP, $I, $K)";
326
327
		$result = $dbh->exec($q);
		if (!$result) {
328
			$message = __("Error trying to create account, %s%s%s.",
329
					"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
330
			return array(false, $message);
331
332
		}

333
		$uid = $dbh->lastInsertId();
334
335
336
		if (isset($ssh_keys) && count($ssh_keys) > 0) {
			account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints);
		}
337

338
		$message = __("The account, %s%s%s, has been successfully created.",
339
				"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
340
		$message .= "<p>\n";
341

342
343
		if ($send_resetkey) {
			send_resetkey($email, true);
344
345
			$message .= __("A password reset key has been sent to your e-mail address.");
			$message .= "</p>\n";
346
		} else {
347
348
			$message .= __("Click on the Login link above to use your account.");
			$message .= "</p>\n";
349
		}
350
	} else {
351
352
353
354
355
356
357
358
359
360
361
362
		/* Modify an existing account. */
		$q = "SELECT InactivityTS FROM Users WHERE ";
		$q.= "ID = " . intval($UID);
		$result = $dbh->query($q);
		$row = $result->fetch(PDO::FETCH_NUM);
		if ($row[0] && $J) {
			$inactivity_ts = $row[0];
		} elseif ($J) {
			$inactivity_ts = time();
		} else {
			$inactivity_ts = 0;
		}
363

364
365
366
367
368
369
370
371
372
		$q = "UPDATE Users SET ";
		$q.= "Username = " . $dbh->quote($U);
		if ($T) {
			$q.= ", AccountTypeID = ".intval($T);
		}
		if ($S) {
			/* Ensure suspended users can't keep an active session */
			delete_user_sessions($UID);
			$q.= ", Suspended = 1";
373
		} else {
374
375
376
			$q.= ", Suspended = 0";
		}
		$q.= ", Email = " . $dbh->quote($E);
377
378
379
380
381
		if ($H) {
			$q.= ", HideEmail = 1";
		} else {
			$q.= ", HideEmail = 0";
		}
382
		if ($P) {
Lukas Fleischer's avatar
Lukas Fleischer committed
383
384
			$hash = password_hash($P, PASSWORD_DEFAULT);
			$q .= ", Passwd = " . $dbh->quote($hash);
385
386
387
		}
		$q.= ", RealName = " . $dbh->quote($R);
		$q.= ", LangPreference = " . $dbh->quote($L);
Mark Weiman's avatar
Mark Weiman committed
388
		$q.= ", Timezone = " . $dbh->quote($TZ);
389
		$q.= ", Homepage = " . $dbh->quote($HP);
390
391
392
		$q.= ", IRCNick = " . $dbh->quote($I);
		$q.= ", PGPKey = " . $dbh->quote(str_replace(" ", "", $K));
		$q.= ", InactivityTS = " . $inactivity_ts;
393
		$q.= ", CommentNotify = " . ($CN ? "1" : "0");
394
		$q.= ", UpdateNotify = " . ($UN ? "1" : "0");
395
		$q.= ", OwnershipNotify = " . ($ON ? "1" : "0");
396
397
		$q.= " WHERE ID = ".intval($UID);
		$result = $dbh->exec($q);
398

399
400
401
402
403
		if (isset($ssh_keys) && count($ssh_keys) > 0) {
			$ssh_key_result = account_set_ssh_keys($UID, $ssh_keys, $ssh_fingerprints);
		} else {
			$ssh_key_result = true;
		}
404

Mark Weiman's avatar
Mark Weiman committed
405
406
407
408
409
410
411
		if (isset($_COOKIE["AURTZ"]) && ($_COOKIE["AURTZ"] != $TZ)) {
			/* set new cookie for timezone */
			$timeout = intval(config_get("options", "persistent_cookie_timeout"));
			$cookie_time = time() + $timeout;
			setcookie("AURTZ", $TZ, $cookie_time, "/");
		}

412
413
414
415
416
417
418
		if (isset($_COOKIE["AURLANG"]) && ($_COOKIE["AURLANG"] != $L)) {
			/* set new cookie for language */
			$timeout = intval(config_get("options", "persistent_cookie_timeout"));
			$cookie_time = time() + $timeout;
			setcookie("AURLANG", $L, $cookie_time, "/");
		}

419
		if ($result === false || $ssh_key_result === false) {
420
			$message = __("No changes were made to the account, %s%s%s.",
421
422
					"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
		} else {
423
			$message = __("The account, %s%s%s, has been successfully modified.",
424
					"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
425
426
		}
	}
427
428

	return array(true, $message);
429
430
}

431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
/**
 * Display the search results page
 *
 * @param string $O The offset for the results page
 * @param string $SB The column to sort the results page by
 * @param string $U The username search criteria
 * @param string $T The account type search criteria
 * @param string $S Whether the account is suspended search criteria
 * @param string $E The e-mail address search criteria
 * @param string $R The real name search criteria
 * @param string $I The IRC nickname search criteria
 * @param string $K The PGP key fingerprint search criteria
 *
 * @return void
 */
Lukas Fleischer's avatar
Lukas Fleischer committed
446
function search_results_page($O=0,$SB="",$U="",$T="",
447
		$S="",$E="",$R="",$I="",$K="") {
448
449
450
451
452
453
454
455
456
457
458
459

	$HITS_PER_PAGE = 50;
	if ($O) {
		$OFFSET = intval($O);
	} else {
		$OFFSET = 0;
	}
	if ($OFFSET < 0) {
		$OFFSET = 0;
	}
	$search_vars = array();

460
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
461

462
463
464
465
466
467
468
469
470
471
472
473
	$q = "SELECT Users.*, AccountTypes.AccountType ";
	$q.= "FROM Users, AccountTypes ";
	$q.= "WHERE AccountTypes.ID = Users.AccountTypeID ";
	if ($T == "u") {
		$q.= "AND AccountTypes.ID = 1 ";
		$search_vars[] = "T";
	} elseif ($T == "t") {
		$q.= "AND AccountTypes.ID = 2 ";
		$search_vars[] = "T";
	} elseif ($T == "d") {
		$q.= "AND AccountTypes.ID = 3 ";
		$search_vars[] = "T";
474
475
476
	} elseif ($T == "td") {
		$q.= "AND AccountTypes.ID = 4 ";
		$search_vars[] = "T";
477
478
479
480
481
482
	}
	if ($S) {
		$q.= "AND Users.Suspended = 1 ";
		$search_vars[] = "S";
	}
	if ($U) {
canyonknight's avatar
canyonknight committed
483
484
		$U = "%" . addcslashes($U, '%_') . "%";
		$q.= "AND Username LIKE " . $dbh->quote($U) . " ";
485
486
487
		$search_vars[] = "U";
	}
	if ($E) {
canyonknight's avatar
canyonknight committed
488
489
		$E = "%" . addcslashes($E, '%_') . "%";
		$q.= "AND Email LIKE " . $dbh->quote($E) . " ";
490
491
492
		$search_vars[] = "E";
	}
	if ($R) {
canyonknight's avatar
canyonknight committed
493
494
		$R = "%" . addcslashes($R, '%_') . "%";
		$q.= "AND RealName LIKE " . $dbh->quote($R) . " ";
495
496
497
		$search_vars[] = "R";
	}
	if ($I) {
canyonknight's avatar
canyonknight committed
498
499
		$I = "%" . addcslashes($I, '%_') . "%";
		$q.= "AND IRCNick LIKE " . $dbh->quote($I) . " ";
500
501
		$search_vars[] = "I";
	}
502
	if ($K) {
canyonknight's avatar
canyonknight committed
503
504
		$K = "%" . addcslashes(str_replace(" ", "", $K), '%_') . "%";
		$q.= "AND PGPKey LIKE " . $dbh->quote($K) . " ";
505
506
		$search_vars[] = "K";
	}
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
	switch ($SB) {
		case 't':
			$q.= "ORDER BY AccountTypeID, Username ";
			break;
		case 'r':
			$q.= "ORDER BY RealName, AccountTypeID ";
			break;
		case 'i':
			$q.= "ORDER BY IRCNick, AccountTypeID ";
			break;
		default:
			$q.= "ORDER BY Username, AccountTypeID ";
			break;
	}
	$search_vars[] = "SB";
522
	$q.= "LIMIT " . $HITS_PER_PAGE . " OFFSET " . $OFFSET;
523

524
	$dbh = DB::connect();
525

canyonknight's avatar
canyonknight committed
526
	$result = $dbh->query($q);
527

528
529
530
531
532
	$userinfo = array();
	if ($result) {
		while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
			$userinfo[] = $row;
		}
533
534
	}

535
	include("account_search_results.php");
536
537
538
	return;
}

539
540
541
542
/**
 * Attempt to login and generate a session
 *
 * @return array Session ID for user, error message if applicable
543
 */
544
function try_login() {
Loui Chang's avatar
Loui Chang committed
545
	$login_error = "";
546
547
548
	$new_sid = "";
	$userID = null;

549
550
551
552
553
554
555
556
557
558
559
	if (!isset($_REQUEST['user']) && !isset($_REQUEST['passwd'])) {
		return array('SID' => '', 'error' => null);
	}

	if (is_ipbanned()) {
		$login_error = __('The login form is currently disabled ' .
						'for your IP address, probably due ' .
						'to sustained spam attacks. Sorry for the ' .
						'inconvenience.');
		return array('SID' => '', 'error' => $login_error);
	}
560

561
	$dbh = DB::connect();
562
	$userID = uid_from_loginname($_REQUEST['user']);
563
564
565
566

	if (user_suspended($userID)) {
		$login_error = __('Account suspended');
		return array('SID' => '', 'error' => $login_error);
Lukas Fleischer's avatar
Lukas Fleischer committed
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
	}

	switch (check_passwd($userID, $_REQUEST['passwd'])) {
		case -1:
			$login_error = __('Your password has been reset. ' .
				'If you just created a new account, please ' .
				'use the link from the confirmation email ' .
				'to set an initial password. Otherwise, ' .
				'please request a reset key on the %s' .
				'Password Reset%s page.', '<a href="' .
				htmlspecialchars(get_uri('/passreset')) . '">',
				'</a>');
			return array('SID' => '', 'error' => $login_error);
		case 0:
			$login_error = __("Bad username or password.");
			return array('SID' => '', 'error' => $login_error);
		case 1:
			break;
585
586
587
588
589
590
591
	}

	$logged_in = 0;
	$num_tries = 0;

	/* Generate a session ID and store it. */
	while (!$logged_in && $num_tries < 5) {
592
593
		$session_limit = config_get_int('options', 'max_sessions_per_user');
		if ($session_limit) {
594
595
			/*
			 * Delete all user sessions except the
596
			 * last ($session_limit - 1).
597
598
599
600
601
			 */
			$q = "DELETE s.* FROM Sessions s ";
			$q.= "LEFT JOIN (SELECT SessionID FROM Sessions ";
			$q.= "WHERE UsersId = " . $userID . " ";
			$q.= "ORDER BY LastUpdateTS DESC ";
602
			$q.= "LIMIT " . ($session_limit - 1) . ") q ";
603
604
605
606
			$q.= "ON s.SessionID = q.SessionID ";
			$q.= "WHERE s.UsersId = " . $userID . " ";
			$q.= "AND q.SessionID IS NULL;";
			$dbh->query($q);
607
		}
608
609
610

		$new_sid = new_sid();
		$q = "INSERT INTO Sessions (UsersID, SessionID, LastUpdateTS)"
611
		  ." VALUES (" . $userID . ", '" . $new_sid . "', " . strval(time()) . ")";
612
613
614
615
616
617
		$result = $dbh->exec($q);

		/* Query will fail if $new_sid is not unique. */
		if ($result) {
			$logged_in = 1;
			break;
618
		}
619
620
621
622
623
624
625

		$num_tries++;
	}

	if (!$logged_in) {
		$login_error = __('An error occurred trying to generate a user session.');
		return array('SID' => $new_sid, 'error' => $login_error);
626
	}
627

628
	$q = "UPDATE Users SET LastLogin = " . strval(time()) . ", ";
629
630
	$q.= "LastLoginIPAddress = " . $dbh->quote($_SERVER['REMOTE_ADDR']) . " ";
	$q.= "WHERE ID = $userID";
631
632
633
634
635
	$dbh->exec($q);

	/* Set the SID cookie. */
	if (isset($_POST['remember_me']) && $_POST['remember_me'] == "on") {
		/* Set cookies for 30 days. */
636
637
		$timeout = config_get_int('options', 'persistent_cookie_timeout');
		$cookie_time = time() + $timeout;
638
639
640
641
642
643
644
645
646
647

		/* Set session for 30 days. */
		$q = "UPDATE Sessions SET LastUpdateTS = $cookie_time ";
		$q.= "WHERE SessionID = '$new_sid'";
		$dbh->exec($q);
	} else {
		$cookie_time = 0;
	}

	setcookie("AURSID", $new_sid, $cookie_time, "/", null, !empty($_SERVER['HTTPS']), true);
648
649
650
651
652
653

	$referer = in_request('referer');
	if (strpos($referer, aur_location()) !== 0) {
		$referer = '/';
	}
	header("Location: " . get_uri($referer));
654
	$login_error = "";
655
656
}

657
658
659
660
661
662
663
664
/**
 * Determine if the user is using a banned IP address
 *
 * @return bool True if IP address is banned, otherwise false
 */
function is_ipbanned() {
	$dbh = DB::connect();

665
	$q = "SELECT * FROM Bans WHERE IPAddress = " . $dbh->quote($_SERVER['REMOTE_ADDR']);
666
667
	$result = $dbh->query($q);

668
	return ($result->fetchColumn() ? true : false);
669
670
}

671
672
673
/**
 * Validate a username against a collection of rules
 *
674
675
676
677
 * The username must be longer or equal to the configured minimum length. It
 * must be shorter or equal to the configured maximum length. It must start and
 * end with either a letter or a number. It can contain one period, hypen, or
 * underscore. Returns boolean of whether name is valid.
678
679
680
 *
 * @param string $user Username to validate
 *
681
 * @return bool True if username meets criteria, otherwise false
682
 */
683
function valid_username($user) {
684
685
686
687
	$length_min = config_get_int('options', 'username_min_len');
	$length_max = config_get_int('options', 'username_max_len');

	if (strlen($user) < $length_min || strlen($user) > $length_max) {
688
		return false;
689
	} else if (!preg_match("/^[a-z0-9]+[.\-_]?[a-z0-9]+$/Di", $user)) {
690
		return false;
691
	}
692

693
	return true;
694
695
}

696
697
698
699
700
701
702
/**
 * Determine if a user already has a proposal open about themselves
 *
 * @param string $user Username to checkout for open proposal
 *
 * @return bool True if there is an open proposal about the user, otherwise false
 */
703
function open_user_proposals($user) {
704
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
705
	$q = "SELECT * FROM TU_VoteInfo WHERE User = " . $dbh->quote($user) . " ";
706
	$q.= "AND End > " . strval(time());
canyonknight's avatar
canyonknight committed
707
	$result = $dbh->query($q);
708
709

	return ($result->fetchColumn() ? true : false);
canyonknight's avatar
canyonknight committed
710
711
}

712
713
714
715
716
717
718
719
720
721
/**
 * Add a new Trusted User proposal to the database
 *
 * @param string $agenda The agenda of the vote
 * @param string $user The use the vote is about
 * @param int $votelength The length of time for the vote to last
 * @param string $submitteruid The user ID of the individual who submitted the proposal
 *
 * @return void
 */
722
function add_tu_proposal($agenda, $user, $votelength, $quorum, $submitteruid) {
723
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
724

725
	$q = "SELECT COUNT(*) FROM Users WHERE (AccountTypeID = 2 OR AccountTypeID = 4)";
726
727
728
729
	$result = $dbh->query($q);
	$row = $result->fetch(PDO::FETCH_NUM);
	$active_tus = $row[0];

730
	$q = "INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End, Quorum, ";
731
	$q.= "SubmitterID, ActiveTUs) VALUES ";
canyonknight's avatar
canyonknight committed
732
	$q.= "(" . $dbh->quote($agenda) . ", " . $dbh->quote($user) . ", ";
733
	$q.= strval(time()) . ", " . strval(time()) . " + " . $dbh->quote($votelength);
734
735
	$q.= ", " . $dbh->quote($quorum) . ", " . $submitteruid . ", ";
	$q.= $active_tus . ")";
canyonknight's avatar
canyonknight committed
736
	$result = $dbh->exec($q);
canyonknight's avatar
canyonknight committed
737
738
}

739
740
741
742
743
744
745
746
/**
 * Add a reset key to the database for a specified user
 *
 * @param string $resetkey A password reset key to be stored in database
 * @param string $uid The user ID to store the reset key for
 *
 * @return void
 */
747
function create_resetkey($resetkey, $uid) {
748
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
749
750
751
	$q = "UPDATE Users ";
	$q.= "SET ResetKey = '" . $resetkey . "' ";
	$q.= "WHERE ID = " . $uid;
canyonknight's avatar
canyonknight committed
752
	$dbh->exec($q);
canyonknight's avatar
canyonknight committed
753
754
}

755
756
757
/**
 * Send a reset key to a specific e-mail address
 *
758
 * @param string $user User name or email address of the user
759
 * @param bool $welcome Whether to use the welcome message
760
761
762
 *
 * @return void
 */
763
764
function send_resetkey($user, $welcome=false) {
	$uid = uid_from_loginname($user);
765
766
767
768
769
770
771
772
773
	if ($uid == null) {
		return;
	}

	/* We (ab)use new_sid() to get a random 32 characters long string. */
	$resetkey = new_sid();
	create_resetkey($resetkey, $uid);

	/* Send e-mail with confirmation link. */
774
	notify(array($welcome ? 'welcome' : 'send-resetkey', $uid));
775
776
}

777
778
779
/**
 * Change a user's password in the database if reset key and e-mail are correct
 *
Lukas Fleischer's avatar
Lukas Fleischer committed
780
 * @param string $password The new password
781
 * @param string $resetkey Code e-mailed to a user to reset a password
782
 * @param string $user User name or email address of the user
783
784
785
 *
 * @return string|void Redirect page if successful, otherwise return error message
 */
786
function password_reset($password, $resetkey, $user) {
Lukas Fleischer's avatar
Lukas Fleischer committed
787
788
	$hash = password_hash($password, PASSWORD_DEFAULT);

789
	$dbh = DB::connect();
Lukas Fleischer's avatar
Lukas Fleischer committed
790
791
	$q = "UPDATE Users SET ";
	$q.= "Passwd = " . $dbh->quote($hash) . ", ";
canyonknight's avatar
canyonknight committed
792
793
	$q.= "ResetKey = '' ";
	$q.= "WHERE ResetKey != '' ";
canyonknight's avatar
canyonknight committed
794
	$q.= "AND ResetKey = " . $dbh->quote($resetkey) . " ";
795
796
	$q.= "AND (Email = " . $dbh->quote($user) . " OR ";
	$q.= "UserName = " . $dbh->quote($user) . ")";
canyonknight's avatar
canyonknight committed
797
	$result = $dbh->exec($q);
canyonknight's avatar
canyonknight committed
798

canyonknight's avatar
canyonknight committed
799
	if (!$result) {
canyonknight's avatar
canyonknight committed
800
801
802
		$error = __('Invalid e-mail and reset key combination.');
		return $error;
	} else {
803
		header('Location: ' . get_uri('/passreset/') . '?step=complete');
canyonknight's avatar
canyonknight committed
804
805
806
807
		exit();
	}
}

808
809
810
811
812
813
814
/**
 * Determine if the password is longer than the minimum length
 *
 * @param string $passwd The password to check
 *
 * @return bool True if longer than minimum length, otherwise false
 */
815
function good_passwd($passwd) {
816
817
	$length_min = config_get_int('options', 'passwd_min_len');
	return (strlen($passwd) >= $length_min);
818
819
}

820
821
822
/**
 * Determine if the password is correct and salt it if it hasn't been already
 *
Lukas Fleischer's avatar
Lukas Fleischer committed
823
 * @param int $user_id The user ID to check the password against
824
825
 * @param string $passwd The password the visitor sent
 *
Lukas Fleischer's avatar
Lukas Fleischer committed
826
 * @return int Positive if password is correct, negative if password is unset
827
 */
Lukas Fleischer's avatar
Lukas Fleischer committed
828
function check_passwd($user_id, $passwd) {
829
	$dbh = DB::connect();
830

Lukas Fleischer's avatar
Lukas Fleischer committed
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
	/* Get password hash and salt. */
	$q = "SELECT Passwd, Salt FROM Users WHERE ID = " . intval($user_id);
	$result = $dbh->query($q);
	if (!$result) {
		return 0;
	}
	$row = $result->fetch(PDO::FETCH_ASSOC);
	if (!$row) {
		return 0;
	}
	$hash = $row['Passwd'];
	$salt = $row['Salt'];
	if (!$hash) {
		return -1;
	}
846

Lukas Fleischer's avatar
Lukas Fleischer committed
847
848
849
850
851
	/* Verify the password hash. */
	if (!password_verify($passwd, $hash)) {
		/* Invalid password, fall back to MD5. */
		if (md5($salt . $passwd) != $hash) {
			return 0;
852
		}
853
854
	}

Lukas Fleischer's avatar
Lukas Fleischer committed
855
856
857
	/* Password correct, migrate the hash if necessary. */
	if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
		$hash = password_hash($passwd, PASSWORD_DEFAULT);
858

Lukas Fleischer's avatar
Lukas Fleischer committed
859
860
861
		$q = "UPDATE Users SET Passwd = " . $dbh->quote($hash) . " ";
		$q.= "WHERE ID = " . intval($user_id);
		$dbh->query($q);
862
	}
Lukas Fleischer's avatar
Lukas Fleischer committed
863
864

	return 1;
865
866
}

867
868
869
870
871
872
/**
 * Determine if the PGP key fingerprint is valid (must be 40 hexadecimal digits)
 *
 * @param string $fingerprint PGP fingerprint to check if valid
 *
 * @return bool True if the fingerprint is 40 hexadecimal digits, otherwise false
873
 */
874
function valid_pgp_fingerprint($fingerprint) {
875
876
	$fingerprint = str_replace(" ", "", $fingerprint);
	return (strlen($fingerprint) == 40 && ctype_xdigit($fingerprint));
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
}

/**
 * Determine if the SSH public key is valid
 *
 * @param string $pubkey SSH public key to check
 *
 * @return bool True if the SSH public key is valid, otherwise false
 */
function valid_ssh_pubkey($pubkey) {
	$valid_prefixes = array(
		"ssh-rsa", "ssh-dss", "ecdsa-sha2-nistp256",
		"ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "ssh-ed25519"
	);

	$has_valid_prefix = false;
	foreach ($valid_prefixes as $prefix) {
		if (strpos($pubkey, $prefix . " ") === 0) {
			$has_valid_prefix = true;
			break;
		}
	}
	if (!$has_valid_prefix) {
		return false;
	}

	$tokens = explode(" ", $pubkey);
	if (empty($tokens[1])) {
		return false;
	}

	return (base64_encode(base64_decode($tokens[1], true)) == $tokens[1]);
909
910
}

911
912
913
914
915
916
/**
 * Determine if the user account has been suspended
 *
 * @param string $id The ID of user to check if suspended
 *
 * @return bool True if the user is suspended, otherwise false
917
 */
918
function user_suspended($id) {
919
	$dbh = DB::connect();
elij's avatar
elij committed
920
921
922
	if (!$id) {
		return false;
	}
923
	$q = "SELECT Suspended FROM Users WHERE ID = " . $id;
canyonknight's avatar
canyonknight committed
924
	$result = $dbh->query($q);
925
	if ($result) {
canyonknight's avatar
canyonknight committed
926
		$row = $result->fetch(PDO::FETCH_NUM);
927
		if ($row[0]) {
928
929
			return true;
		}
930
931
932
933
	}
	return false;
}

934
935
936
937
938
939
/**
 * Delete a specified user account from the database
 *
 * @param int $id The user ID of the account to be deleted
 *
 * @return void
940
 */
941
function user_delete($id) {
942
	$dbh = DB::connect();
943
944
945
946
947
948
949
950
951
	$id = intval($id);

	/*
	 * These are normally already taken care of by propagation constraints
	 * but it is better to be explicit here.
	 */
	$fields_delete = array(
		array("Sessions", "UsersID"),
		array("PackageVotes", "UsersID"),
952
		array("PackageNotifications", "UsersID")
953
954
955
956
957
	);

	$fields_set_null = array(
		array("PackageBases", "SubmitterUID"),
		array("PackageBases", "MaintainerUID"),
958
		array("PackageBases", "PackagerUID"),
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
		array("PackageComments", "UsersID"),
		array("PackageComments", "DelUsersID"),
		array("PackageRequests", "UsersID"),
		array("TU_VoteInfo", "SubmitterID"),
		array("TU_Votes", "UserID")
	);

	foreach($fields_delete as list($table, $field)) {
		$q = "DELETE FROM " . $table . " ";
		$q.= "WHERE " . $field . " = " . $id;
		$dbh->query($q);
	}

	foreach($fields_set_null as list($table, $field)) {
		$q = "UPDATE " . $table . " SET " . $field . " = NULL ";
		$q.= "WHERE " . $field . " = " . $id;
		$dbh->query($q);
	}

978
	$q = "DELETE FROM Users WHERE ID = " . $id;
canyonknight's avatar
canyonknight committed
979
	$dbh->query($q);
980
981
982
	return;
}

983
984
985
986
987
988
989
/**
 * Remove the session from the database on logout
 *
 * @param string $sid User's session ID
 *
 * @return void
 */
990
function delete_session_id($sid) {
991
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
992

canyonknight's avatar
canyonknight committed
993
994
	$q = "DELETE FROM Sessions WHERE SessionID = " . $dbh->quote($sid);
	$dbh->query($q);
canyonknight's avatar
canyonknight committed
995
996
}

997
998
999
1000
/**
 * Remove all sessions belonging to a particular user
 *
 * @param int $uid ID of user to remove all sessions for
For faster browsing, not all history is shown. View entire blame