acctfuncs.inc.php 35.8 KB
Newer Older
1
<?php
2

3
4
5
6
7
8
9
/**
 * 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
10
11
12
13
14
15
16
function in_request($name) {
	if (isset($_REQUEST[$name])) {
		return $_REQUEST[$name];
	}
	return "";
}

17
18
19
20
21
22
23
/**
 * Format the PGP key fingerprint
 *
 * @param string $fingerprint An unformatted PGP key fingerprint
 *
 * @return string PGP fingerprint with spaces every 4 characters
 */
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
}

41
42
43
44
45
46
47
48
49
/**
 * 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
50
 * @param string $H Whether the e-mail address of the displayed user is hidden
51
52
53
54
 * @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
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
67
 *
 * @return void
 */
68
function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="",
69
		$L="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="") {
70
71
	global $SUPPORTED_LANGS;

72
	include("account_edit_form.php");
73
	return;
74
}
75

76
77
78
79
80
81
82
83
84
85
/**
 * 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
86
 * @param string $H Whether or not the e-mail address should be hidden
87
88
89
90
 * @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
91
 * @param string $HP The homepage of the displayed user
92
93
 * @param string $I The IRC nickname of the user
 * @param string $K The PGP fingerprint of the user
94
 * @param string $PK The list of public SSH keys
95
 * @param string $J The inactivity status of the user
96
 * @param string $CN Whether to notify of new comments
97
 * @param string $UN Whether to notify of package updates
98
 * @param string $ON Whether to notify of ownership changes
99
 * @param string $UID The user ID of the modified account
100
 * @param string $N The username as present in the database
101
 *
102
 * @return array Boolean indicating success and message to be printed
103
 */
104
function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",
105
		$R="",$L="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="") {
106
	global $SUPPORTED_LANGS;
107

108
	$error = '';
109
	$message = '';
110
111
112
113
114
115
116
117

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

118
	$dbh = DB::connect();
119

120
	if(isset($_COOKIE['AURSID'])) {
121
		$editor_user = uid_from_sid($_COOKIE['AURSID']);
122
123
	}
	else {
124
		$editor_user = null;
125
	}
126
127

	if (empty($E) || empty($U)) {
128
129
		$error = __("Missing a required field.");
	}
130

131
132
	if ($TYPE != "new" && !$UID) {
		$error = __("Missing User ID");
133
	}
134

135
	if (!$error && !valid_username($U)) {
136
137
138
		$length_min = config_get_int('options', 'username_min_len');
		$length_max = config_get_int('options', 'username_max_len');

139
		$error = __("The username is invalid.") . "<ul>\n"
140
			. "<li>" . __("It must be between %s and %s characters long", $length_min, $length_max)
141
			. "</li>"
142
			. "<li>" . __("Start and end with a letter or number") . "</li>"
143
			. "<li>" . __("Can contain only one period, underscore or hyphen.")
144
			. "</li>\n</ul>";
145
	}
146

147
148
149
	if (!$error && $P && $C && ($P != $C)) {
		$error = __("Password fields do not match.");
	}
150
151
152
153
154
	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);
	}
155

156
157
158
	if (!$error && !valid_email($E)) {
		$error = __("The email address is invalid.");
	}
159
160
161
162
163

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

164
	if (!$error && !empty($PK)) {
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
		$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;
184
		}
185
186
187
188
189
190

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

193
194
195
196
197
	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
198
	}
199

200
201
202
203
	if (!$error && !array_key_exists($L, $SUPPORTED_LANGS)) {
		$error = __("Language is not currently supported.");
	}
	if (!$error) {
204
205
206
207
		/*
		 * Check whether the user name is available.
		 * TODO: Fix race condition.
		 */
208
		$q = "SELECT COUNT(*) AS CNT FROM Users ";
canyonknight's avatar
canyonknight committed
209
		$q.= "WHERE Username = " . $dbh->quote($U);
eric's avatar
eric committed
210
211
212
		if ($TYPE == "edit") {
			$q.= " AND ID != ".intval($UID);
		}
canyonknight's avatar
canyonknight committed
213
214
215
216
217
		$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
218
				"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
219
220
221
		}
	}
	if (!$error) {
222
223
224
225
		/*
		 * Check whether the e-mail address is available.
		 * TODO: Fix race condition.
		 */
226
		$q = "SELECT COUNT(*) AS CNT FROM Users ";
canyonknight's avatar
canyonknight committed
227
		$q.= "WHERE Email = " . $dbh->quote($E);
eric's avatar
eric committed
228
229
230
		if ($TYPE == "edit") {
			$q.= " AND ID != ".intval($UID);
		}
canyonknight's avatar
canyonknight committed
231
232
233
234
235
		$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
236
					"<strong>", htmlspecialchars($E,ENT_QUOTES), "</strong>");
237
238
		}
	}
239
	if (!$error && count($ssh_keys) > 0) {
240
		/*
241
		 * Check whether any of the SSH public keys is already in use.
242
243
		 * TODO: Fix race condition.
		 */
244
245
246
247
		$q = "SELECT Fingerprint FROM SSHPubKeys ";
		$q.= "WHERE Fingerprint IN (";
		$q.= implode(',', array_map(array($dbh, 'quote'), $ssh_fingerprints));
		$q.= ")";
248
		if ($TYPE == "edit") {
249
			$q.= " AND UserID != " . intval($UID);
250
251
252
253
		}
		$result = $dbh->query($q);
		$row = $result->fetch(PDO::FETCH_NUM);

254
		if ($row) {
255
			$error = __("The SSH public key, %s%s%s, is already in use.",
256
					"<strong>", htmlspecialchars($row[0], ENT_QUOTES), "</strong>");
257
258
		}
	}
259

260
	if ($error) {
261
262
		$message = "<ul class='errorlist'><li>".$error."</li></ul>\n";
		return array(false, $message);
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
	}

	if ($TYPE == "new") {
		/* Create an unprivileged user. */
		$salt = generate_salt();
		if (empty($P)) {
			$send_resetkey = true;
			$email = $E;
		} else {
			$send_resetkey = false;
			$P = salted_hash($P, $salt);
		}
		$U = $dbh->quote($U);
		$E = $dbh->quote($E);
		$P = $dbh->quote($P);
		$salt = $dbh->quote($salt);
		$R = $dbh->quote($R);
		$L = $dbh->quote($L);
281
		$HP = $dbh->quote($HP);
282
283
284
285
		$I = $dbh->quote($I);
		$K = $dbh->quote(str_replace(" ", "", $K));
		$q = "INSERT INTO Users (AccountTypeID, Suspended, ";
		$q.= "InactivityTS, Username, Email, Passwd, Salt, ";
286
		$q.= "RealName, LangPreference, Homepage, IRCNick, PGPKey) ";
287
		$q.= "VALUES (1, 0, 0, $U, $E, $P, $salt, $R, $L, ";
288
		$q.= "$HP, $I, $K)";
289
290
		$result = $dbh->exec($q);
		if (!$result) {
291
			$message = __("Error trying to create account, %s%s%s.",
292
					"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
293
			return array(false, $message);
294
295
		}

296
297
298
		$uid = $dbh->lastInsertId();
		account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints);

299
		$message = __("The account, %s%s%s, has been successfully created.",
300
				"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
301
		$message .= "<p>\n";
302

303
304
		if ($send_resetkey) {
			send_resetkey($email, true);
305
306
			$message .= __("A password reset key has been sent to your e-mail address.");
			$message .= "</p>\n";
307
		} else {
308
309
			$message .= __("Click on the Login link above to use your account.");
			$message .= "</p>\n";
310
		}
311
	} else {
312
313
314
315
316
317
318
319
320
321
322
323
		/* 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;
		}
324

325
326
327
328
329
330
331
332
333
		$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";
334
		} else {
335
336
337
			$q.= ", Suspended = 0";
		}
		$q.= ", Email = " . $dbh->quote($E);
338
339
340
341
342
		if ($H) {
			$q.= ", HideEmail = 1";
		} else {
			$q.= ", HideEmail = 0";
		}
343
344
345
346
347
348
349
		if ($P) {
			$salt = generate_salt();
			$hash = salted_hash($P, $salt);
			$q .= ", Passwd = '$hash', Salt = '$salt'";
		}
		$q.= ", RealName = " . $dbh->quote($R);
		$q.= ", LangPreference = " . $dbh->quote($L);
350
		$q.= ", Homepage = " . $dbh->quote($HP);
351
352
353
		$q.= ", IRCNick = " . $dbh->quote($I);
		$q.= ", PGPKey = " . $dbh->quote(str_replace(" ", "", $K));
		$q.= ", InactivityTS = " . $inactivity_ts;
354
		$q.= ", CommentNotify = " . ($CN ? "1" : "0");
355
		$q.= ", UpdateNotify = " . ($UN ? "1" : "0");
356
		$q.= ", OwnershipNotify = " . ($ON ? "1" : "0");
357
358
		$q.= " WHERE ID = ".intval($UID);
		$result = $dbh->exec($q);
359

360
		$ssh_key_result = account_set_ssh_keys($UID, $ssh_keys, $ssh_fingerprints);
361

362
		if ($result === false || $ssh_key_result === false) {
363
			$message = __("No changes were made to the account, %s%s%s.",
364
365
					"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
		} else {
366
			$message = __("The account, %s%s%s, has been successfully modified.",
367
					"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
368
369
		}
	}
370
371

	return array(true, $message);
372
373
}

374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
/**
 * 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
389
function search_results_page($O=0,$SB="",$U="",$T="",
390
		$S="",$E="",$R="",$I="",$K="") {
391
392
393
394
395
396
397
398
399
400
401
402

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

403
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
404

405
406
407
408
409
410
411
412
413
414
415
416
	$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";
417
418
419
	} elseif ($T == "td") {
		$q.= "AND AccountTypes.ID = 4 ";
		$search_vars[] = "T";
420
421
422
423
424
425
	}
	if ($S) {
		$q.= "AND Users.Suspended = 1 ";
		$search_vars[] = "S";
	}
	if ($U) {
canyonknight's avatar
canyonknight committed
426
427
		$U = "%" . addcslashes($U, '%_') . "%";
		$q.= "AND Username LIKE " . $dbh->quote($U) . " ";
428
429
430
		$search_vars[] = "U";
	}
	if ($E) {
canyonknight's avatar
canyonknight committed
431
432
		$E = "%" . addcslashes($E, '%_') . "%";
		$q.= "AND Email LIKE " . $dbh->quote($E) . " ";
433
434
435
		$search_vars[] = "E";
	}
	if ($R) {
canyonknight's avatar
canyonknight committed
436
437
		$R = "%" . addcslashes($R, '%_') . "%";
		$q.= "AND RealName LIKE " . $dbh->quote($R) . " ";
438
439
440
		$search_vars[] = "R";
	}
	if ($I) {
canyonknight's avatar
canyonknight committed
441
442
		$I = "%" . addcslashes($I, '%_') . "%";
		$q.= "AND IRCNick LIKE " . $dbh->quote($I) . " ";
443
444
		$search_vars[] = "I";
	}
445
	if ($K) {
canyonknight's avatar
canyonknight committed
446
447
		$K = "%" . addcslashes(str_replace(" ", "", $K), '%_') . "%";
		$q.= "AND PGPKey LIKE " . $dbh->quote($K) . " ";
448
449
		$search_vars[] = "K";
	}
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
	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";
465
	$q.= "LIMIT " . $HITS_PER_PAGE . " OFFSET " . $OFFSET;
466

467
	$dbh = DB::connect();
468

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

canyonknight's avatar
canyonknight committed
471
	while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
472
473
474
		$userinfo[] = $row;
	}

475
	include("account_search_results.php");
476
477
478
	return;
}

479
480
481
482
/**
 * Attempt to login and generate a session
 *
 * @return array Session ID for user, error message if applicable
483
 */
484
function try_login() {
Loui Chang's avatar
Loui Chang committed
485
	$login_error = "";
486
487
488
	$new_sid = "";
	$userID = null;

489
490
491
492
493
494
495
496
497
498
499
	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);
	}
500

501
	$dbh = DB::connect();
502
	$userID = uid_from_loginname($_REQUEST['user']);
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526

	if (user_suspended($userID)) {
		$login_error = __('Account suspended');
		return array('SID' => '', 'error' => $login_error);
	} elseif (passwd_is_empty($userID)) {
		$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);
	} elseif (!valid_passwd($userID, $_REQUEST['passwd'])) {
		$login_error = __("Bad username or password.");
		return array('SID' => '', 'error' => $login_error);
	}

	$logged_in = 0;
	$num_tries = 0;

	/* Generate a session ID and store it. */
	while (!$logged_in && $num_tries < 5) {
527
528
		$session_limit = config_get_int('options', 'max_sessions_per_user');
		if ($session_limit) {
529
530
			/*
			 * Delete all user sessions except the
531
			 * last ($session_limit - 1).
532
533
534
535
536
			 */
			$q = "DELETE s.* FROM Sessions s ";
			$q.= "LEFT JOIN (SELECT SessionID FROM Sessions ";
			$q.= "WHERE UsersId = " . $userID . " ";
			$q.= "ORDER BY LastUpdateTS DESC ";
537
			$q.= "LIMIT " . ($session_limit - 1) . ") q ";
538
539
540
541
			$q.= "ON s.SessionID = q.SessionID ";
			$q.= "WHERE s.UsersId = " . $userID . " ";
			$q.= "AND q.SessionID IS NULL;";
			$dbh->query($q);
542
		}
543
544
545

		$new_sid = new_sid();
		$q = "INSERT INTO Sessions (UsersID, SessionID, LastUpdateTS)"
546
		  ." VALUES (" . $userID . ", '" . $new_sid . "', " . strval(time()) . ")";
547
548
549
550
551
552
		$result = $dbh->exec($q);

		/* Query will fail if $new_sid is not unique. */
		if ($result) {
			$logged_in = 1;
			break;
553
		}
554
555
556
557
558
559
560

		$num_tries++;
	}

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

563
	$q = "UPDATE Users SET LastLogin = " . strval(time()) . ", ";
564
565
	$q.= "LastLoginIPAddress = " . $dbh->quote($_SERVER['REMOTE_ADDR']) . " ";
	$q.= "WHERE ID = $userID";
566
567
568
569
570
	$dbh->exec($q);

	/* Set the SID cookie. */
	if (isset($_POST['remember_me']) && $_POST['remember_me'] == "on") {
		/* Set cookies for 30 days. */
571
572
		$timeout = config_get_int('options', 'persistent_cookie_timeout');
		$cookie_time = time() + $timeout;
573
574
575
576
577
578
579
580
581
582

		/* 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);
583
584
585
586
587
588

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

592
593
594
595
596
597
598
599
600
601
602
/**
 * 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();

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

603
	return ($result->fetchColumn() ? true : false);
604
605
}

606
607
608
/**
 * Validate a username against a collection of rules
 *
609
610
611
612
 * 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.
613
614
615
 *
 * @param string $user Username to validate
 *
616
 * @return bool True if username meets criteria, otherwise false
617
 */
618
function valid_username($user) {
619
620
621
622
	$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) {
623
		return false;
624
	} else if (!preg_match("/^[a-z0-9]+[.\-_]?[a-z0-9]+$/Di", $user)) {
625
		return false;
626
	}
627

628
	return true;
629
630
}

631
632
633
634
635
636
637
/**
 * 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
 */
638
function open_user_proposals($user) {
639
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
640
	$q = "SELECT * FROM TU_VoteInfo WHERE User = " . $dbh->quote($user) . " ";
641
	$q.= "AND End > " . strval(time());
canyonknight's avatar
canyonknight committed
642
	$result = $dbh->query($q);
643
644

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

647
648
649
650
651
652
653
654
655
656
/**
 * 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
 */
657
function add_tu_proposal($agenda, $user, $votelength, $quorum, $submitteruid) {
658
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
659

660
	$q = "SELECT COUNT(*) FROM Users WHERE (AccountTypeID = 2 OR AccountTypeID = 4)";
661
662
663
664
	$result = $dbh->query($q);
	$row = $result->fetch(PDO::FETCH_NUM);
	$active_tus = $row[0];

665
	$q = "INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End, Quorum, ";
666
	$q.= "SubmitterID, ActiveTUs) VALUES ";
canyonknight's avatar
canyonknight committed
667
	$q.= "(" . $dbh->quote($agenda) . ", " . $dbh->quote($user) . ", ";
668
	$q.= strval(time()) . ", " . strval(time()) . " + " . $dbh->quote($votelength);
669
670
	$q.= ", " . $dbh->quote($quorum) . ", " . $submitteruid . ", ";
	$q.= $active_tus . ")";
canyonknight's avatar
canyonknight committed
671
	$result = $dbh->exec($q);
canyonknight's avatar
canyonknight committed
672
673
}

674
675
676
677
678
679
680
681
/**
 * 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
 */
682
function create_resetkey($resetkey, $uid) {
683
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
684
685
686
	$q = "UPDATE Users ";
	$q.= "SET ResetKey = '" . $resetkey . "' ";
	$q.= "WHERE ID = " . $uid;
canyonknight's avatar
canyonknight committed
687
	$dbh->exec($q);
canyonknight's avatar
canyonknight committed
688
689
}

690
691
692
693
/**
 * Send a reset key to a specific e-mail address
 *
 * @param string $email E-mail address of the user resetting their password
694
 * @param bool $welcome Whether to use the welcome message
695
696
697
 *
 * @return void
 */
698
function send_resetkey($email, $welcome=false) {
699
	$uid = uid_from_email($email);
700
701
702
703
704
705
706
707
708
	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. */
709
	notify(array($welcome ? 'welcome' : 'send-resetkey', $uid));
710
711
}

712
713
714
715
716
717
718
719
720
721
/**
 * Change a user's password in the database if reset key and e-mail are correct
 *
 * @param string $hash New MD5 hash of a user's password
 * @param string $salt New salt for the user's password
 * @param string $resetkey Code e-mailed to a user to reset a password
 * @param string $email E-mail address of the user resetting their password
 *
 * @return string|void Redirect page if successful, otherwise return error message
 */
722
function password_reset($hash, $salt, $resetkey, $email) {
723
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
724
725
726
727
728
	$q = "UPDATE Users ";
	$q.= "SET Passwd = '$hash', ";
	$q.= "Salt = '$salt', ";
	$q.= "ResetKey = '' ";
	$q.= "WHERE ResetKey != '' ";
canyonknight's avatar
canyonknight committed
729
730
731
	$q.= "AND ResetKey = " . $dbh->quote($resetkey) . " ";
	$q.= "AND Email = " . $dbh->quote($email);
	$result = $dbh->exec($q);
canyonknight's avatar
canyonknight committed
732

canyonknight's avatar
canyonknight committed
733
	if (!$result) {
canyonknight's avatar
canyonknight committed
734
735
736
		$error = __('Invalid e-mail and reset key combination.');
		return $error;
	} else {
737
		header('Location: ' . get_uri('/passreset/') . '?step=complete');
canyonknight's avatar
canyonknight committed
738
739
740
741
		exit();
	}
}

742
743
744
745
746
747
748
/**
 * 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
 */
749
function good_passwd($passwd) {
750
751
	$length_min = config_get_int('options', 'passwd_min_len');
	return (strlen($passwd) >= $length_min);
752
753
}

754
755
756
757
758
759
760
/**
 * Determine if the password is correct and salt it if it hasn't been already
 *
 * @param string $userID The user ID to check the password against
 * @param string $passwd The password the visitor sent
 *
 * @return bool True if password was correct and properly salted, otherwise false
761
 */
762
function valid_passwd($userID, $passwd) {
763
	$dbh = DB::connect();
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
	if ($passwd == "") {
		return false;
	}

	/* Get salt for this user. */
	$salt = get_salt($userID);
	if ($salt) {
		$q = "SELECT ID FROM Users ";
		$q.= "WHERE ID = " . $userID . " ";
		$q.= "AND Passwd = " . $dbh->quote(salted_hash($passwd, $salt));
		$result = $dbh->query($q);
		if (!$result) {
			return false;
		}

		$row = $result->fetch(PDO::FETCH_NUM);
		return ($row[0] > 0);
	} else {
		/* Check password without using salt. */
		$q = "SELECT ID FROM Users ";
		$q.= "WHERE ID = " . $userID . " ";
		$q.= "AND Passwd = " . $dbh->quote(md5($passwd));
		$result = $dbh->query($q);
		if (!$result) {
			return false;
		}

		$row = $result->fetch(PDO::FETCH_NUM);
		if (!$row[0]) {
			return false;
794
		}
795
796
797
798
799
800
801
802
803

		/* Password correct, but salt it first! */
		if (!save_salt($userID, $passwd)) {
			trigger_error("Unable to salt user's password;" .
				" ID " . $userID, E_USER_WARNING);
			return false;
		}

		return true;
804
805
806
	}
}

807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
/**
 * Determine if a user's password is empty
 *
 * @param string $uid The user ID to check for an empty password
 *
 * @return bool True if the user's password is empty, otherwise false
 */
function passwd_is_empty($uid) {
	$dbh = DB::connect();

	$q = "SELECT * FROM Users WHERE ID = " . $dbh->quote($uid) . " ";
	$q .= "AND Passwd = " . $dbh->quote('');
	$result = $dbh->query($q);

	if ($result->fetchColumn()) {
		return true;
	} else {
		return false;
	}
}

828
829
830
831
832
833
/**
 * 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
834
 */
835
function valid_pgp_fingerprint($fingerprint) {
836
837
	$fingerprint = str_replace(" ", "", $fingerprint);
	return (strlen($fingerprint) == 40 && ctype_xdigit($fingerprint));
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
}

/**
 * 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]);
870
871
}

872
873
874
875
876
877
/**
 * 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
878
 */
879
function user_suspended($id) {
880
	$dbh = DB::connect();
elij's avatar
elij committed
881
882
883
	if (!$id) {
		return false;
	}
884
	$q = "SELECT Suspended FROM Users WHERE ID = " . $id;
canyonknight's avatar
canyonknight committed
885
	$result = $dbh->query($q);
886
	if ($result) {
canyonknight's avatar
canyonknight committed
887
		$row = $result->fetch(PDO::FETCH_NUM);
888
		if ($row[0]) {
889
890
			return true;
		}
891
892
893
894
	}
	return false;
}

895
896
897
898
899
900
/**
 * Delete a specified user account from the database
 *
 * @param int $id The user ID of the account to be deleted
 *
 * @return void
901
 */
902
function user_delete($id) {
903
	$dbh = DB::connect();
904
905
906
907
908
909
910
911
912
	$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"),
913
		array("PackageNotifications", "UsersID")
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
	);

	$fields_set_null = array(
		array("PackageBases", "SubmitterUID"),
		array("PackageBases", "MaintainerUID"),
		array("PackageBases", "SubmitterUID"),
		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);
	}

939
	$q = "DELETE FROM Users WHERE ID = " . $id;
canyonknight's avatar
canyonknight committed
940
	$dbh->query($q);
941
942
943
	return;
}

944
945
946
947
948
949
950
/**
 * Remove the session from the database on logout
 *
 * @param string $sid User's session ID
 *
 * @return void
 */
951
function delete_session_id($sid) {
952
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
953

canyonknight's avatar
canyonknight committed
954
955
	$q = "DELETE FROM Sessions WHERE SessionID = " . $dbh->quote($sid);
	$dbh->query($q);
canyonknight's avatar
canyonknight committed
956
957
}

958
959
960
961
962
963
964
/**
 * Remove all sessions belonging to a particular user
 *
 * @param int $uid ID of user to remove all sessions for
 *
 * @return void
 */
965
function delete_user_sessions($uid) {
966
	$dbh = DB::connect();
967
968
969
970
971

	$q = "DELETE FROM Sessions WHERE UsersID = " . intval($uid);
	$dbh->exec($q);
}

972
973
974
975
976
/**
 * Remove sessions from the database that have exceed the timeout
 *
 * @return void
 */
977
function clear_expired_sessions() {
978
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
979

980
	$timeout = config_get_int('options', 'login_timeout');
981
	$q = "DELETE FROM Sessions WHERE LastUpdateTS < (" . strval(time()) . " - " . $timeout . ")";
canyonknight's avatar
canyonknight committed
982
	$dbh->query($q);
983
984
985

	return;
}
986

987
988
989
990
991
992
993
994
/**
 * Get account details for a specific user
 *
 * @param string $uid The User ID of account to get information for
 * @param string $username The username of the account to get for
 *
 * @return array Account details for the specified user
 */
995
function account_details($uid, $username) {
996
	$dbh = DB::connect();
canyonknight's avatar
canyonknight committed
997
998
999
1000
	$q = "SELECT Users.*, AccountTypes.AccountType ";
	$q.= "FROM Users, AccountTypes ";
	$q.= "WHERE AccountTypes.ID = Users.AccountTypeID ";
	if (!empty($uid)) {
For faster browsing, not all history is shown. View entire blame