Add new form validator

This commit is contained in:
Sky Johnson 2024-12-13 10:24:52 -06:00
parent df99415ece
commit 042bca2364
7 changed files with 310 additions and 133 deletions

View File

@ -62,26 +62,30 @@ function donothing()
function primary() function primary()
{ {
if (isset($_POST["submit"])) { if (isset($_POST["submit"])) {
$errors = []; $form = validate($_POST, [
'gamename' => ['alphanum-spaces', 'length:1-20'],
'gamesize' => ['int', 'min:5'],
'class1name' => ['alpha-spaces', 'length:1-18'],
'class2name' => ['alpha-spaces', 'length:1-18'],
'class3name' => ['alpha-spaces', 'length:1-18'],
'gameopen' => ['bool'],
'verifyemail' => ['bool'],
'shownews' => ['bool'],
'showonline' => ['bool'],
'showbabble' => ['bool']
]);
$gn = trim($_POST['gamename'] ?? 'Dragon Knight'); if ($form['valid']) {
$gs = (int) trim($_POST['gamesize'] ?? 250); $form = $form['data'];
$c1n = trim($_POST['class1name'] ?? 'Mage'); if (($form['gamesize'] % 5) != 0) exit('Map size must be divisible by five.');
$c2n = trim($_POST['class2name'] ?? 'Warrior');
$c3n = trim($_POST['class3name'] ?? 'Paladin');
if (empty($gn)) $errors[] = "Game name is required.";
if (!is_int($gs) || !($gs > 0) || ($gs % 5) != 0) $errors[] = "Map size must be a number greater than 0 and divisible by five.";
if (empty($c1n) || empty($c2n) || empty($c3n)) $errors[] = "Class names are required.";
if (count($errors) === 0) {
db()->query('UPDATE control SET gamename=?, gamesize=?, class1name=?, class2name=?, class3name=?, gameopen=?, verifyemail=?, gameurl=?, adminemail=?, shownews=?, showonline=?, showbabble=? WHERE id=1;', [ db()->query('UPDATE control SET gamename=?, gamesize=?, class1name=?, class2name=?, class3name=?, gameopen=?, verifyemail=?, gameurl=?, adminemail=?, shownews=?, showonline=?, showbabble=? WHERE id=1;', [
$gn, $gs, $c1n, $c2n, $c3n, $_POST['gameopen'] ?? 1, $_POST['verifyemail'] ?? 1, $_POST['gameurl'] ?? '', $_POST['adminemail'] ?? '', $_POST['shownews'] ?? 1, $_POST['showonline'] ?? 1, $_POST['showbabble'] ?? 1 $form['gamename'], $form['gamesize'], $form['class1name'], $form['class1name'], $form['class1name'], $form['gameopen'], $form['verifyemail'], $form['gameurl'], $form['adminemail'], $form['shownews'], $form['showonline'], $form['showbabble']
]); ]);
admindisplay("Settings updated.", "Main Settings"); admindisplay("Settings updated.", "Main Settings");
} else { } else {
$errorlist = implode('<br>', $errors); $errorlist = ul_from_validate_errors($form['errors']);
admindisplay("<b>Errors:</b><br><div style=\"color:red;\">$errorlist</div><br>Please go back and try again.", "Main Settings"); admindisplay("<b>Errors:</b><br><div style=\"color:red;\">$errorlist</div><br>Please go back and try again.", "Main Settings");
} }
} }

View File

@ -85,13 +85,22 @@ function reply()
{ {
global $userrow; global $userrow;
$p = $_POST['parent'] ?? 0; $form = validate($_POST, [
$t = trim($_POST['title'] ?? ''); 'title' => ['length:2-30', 'alphanum-spaces'],
$c = trim($_POST['content'] ?? ''); 'content' => []
]);
db()->query('INSERT INTO forum (author, title, content, parent) VALUES (?, ?, ?, ?);', [$userrow['username'], $t, $c, $p]); if (!$form['valid']) {
db()->query('UPDATE forum SET newpostdate=CURRENT_TIMESTAMP, replies=replies + 1 WHERE id=?;', [$p]); exit(ul_from_validate_errors($form['errors']));
redirect("forum.php?do=thread:$p:0"); }
$form = $form['data'];
db()->query('INSERT INTO forum (author, title, content, parent) VALUES (?, ?, ?, ?);', [
$userrow['username'], $form['title'], $form['content'], $form['parent']
]);
db()->query('UPDATE forum SET newpostdate=CURRENT_TIMESTAMP, replies=replies + 1 WHERE id=?;', [$form['parent']]);
redirect("forum.php?do=thread:{$form['parent']}:0");
} }
function newthread() function newthread()
@ -99,10 +108,19 @@ function newthread()
global $userrow; global $userrow;
if (isset($_POST["submit"])) { if (isset($_POST["submit"])) {
extract($_POST); $form = validate($_POST, [
$t = trim($_POST['title'] ?? ''); 'title' => ['length:2-30', 'alphanum-spaces'],
$c = trim($_POST['content'] ?? ''); 'content' => []
db()->query('INSERT INTO forum (author, title, content) VALUES (?, ?, ?);', [$userrow['username'], $t, $c]); ]);
if (!$form['valid']) {
exit(ul_from_validate_errors($form['errors']));
}
$form = $form['data'];
db()->query('INSERT INTO forum (author, title, content) VALUES (?, ?, ?);', [
$userrow['username'], $form['title'], $form['content']
]);
redirect('forum.php'); redirect('forum.php');
} }

View File

@ -657,11 +657,11 @@ function third()
Now you must create an administrator account so you can use the control panel. Fill out the form below to create your account. You will be able to customize the class names through the control panel once your admin account is created.<br><br> Now you must create an administrator account so you can use the control panel. Fill out the form below to create your account. You will be able to customize the class names through the control panel once your admin account is created.<br><br>
<form action="install.php?page=4" method="post"> <form action="install.php?page=4" method="post">
<table width="50%"> <table width="50%">
<tr><td width="20%" style="vertical-align:top;">Username:</td><td><input type="text" name="username" size="30" maxlength="30" /><br><br><br></td></tr> <tr><td width="20%" style="vertical-align:top;">Username:</td><td><input type="text" name="username" /><br><br><br></td></tr>
<tr><td style="vertical-align:top;">Password:</td><td><input type="password" name="password1" size="30" maxlength="30" /></td></tr> <tr><td style="vertical-align:top;">Password:</td><td><input type="password" name="password" /></td></tr>
<tr><td style="vertical-align:top;">Verify Password:</td><td><input type="password" name="password2" size="30" maxlength="30" /><br><br><br></td></tr> <tr><td style="vertical-align:top;">Verify Password:</td><td><input type="password" name="confirm_password" /><br><br><br></td></tr>
<tr><td style="vertical-align:top;">Email Address:</td><td><input type="text" name="email1" size="30" maxlength="100" /></td></tr> <tr><td style="vertical-align:top;">Email Address:</td><td><input type="text" name="email" /></td></tr>
<tr><td style="vertical-align:top;">Verify Email:</td><td><input type="text" name="email2" size="30" maxlength="100" /><br><br><br></td></tr> <tr><td style="vertical-align:top;">Verify Email:</td><td><input type="text" name="confirm_email" /><br><br><br></td></tr>
<tr><td style="vertical-align:top;">Character Class:</td><td><select name="charclass"><option value="1">Mage</option><option value="2">Warrior</option><option value="3">Paladin</option></select></td></tr> <tr><td style="vertical-align:top;">Character Class:</td><td><select name="charclass"><option value="1">Mage</option><option value="2">Warrior</option><option value="3">Paladin</option></select></td></tr>
<tr><td colspan="2"><input type="submit" name="submit" value="Submit" /> <input type="reset" name="reset" value="Reset" /></td></tr> <tr><td colspan="2"><input type="submit" name="submit" value="Submit" /> <input type="reset" name="reset" value="Reset" /></td></tr>
</table> </table>
@ -676,48 +676,22 @@ function third()
*/ */
function fourth() function fourth()
{ {
$u = trim($_POST['username'] ??= ''); $form = validate($_POST, [
$e = trim($_POST['email1'] ??= ''); 'username' => ['length:3-18', 'alpha-spaces'],
$ec = trim($_POST['email2'] ??= ''); 'email' => ['email'],
$p = $_POST['password1'] ??= ''; 'confirm_email' => ['confirm'],
$pc = $_POST['password2'] ??= ''; 'password' => ['length:6-255'],
'confirm_password' => ['confirm']
]);
$errors = []; if (!$form['valid']) exit(ul_from_validate_errors($form['errors']));
if (empty($u) || strlen($u) < 3 || strlen($u) > 18 || !ctype_alnum(str_replace(' ', '', $u))) {
$errors[] = 'Username is required and must be between 3 and 18 characters long and contain only
alphanumeric characters and spaces.';
}
if (empty($e) || strlen($e) > 255 || !filter_var($e, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Email is required must be a valid email address.';
}
if ($e !== $ec) {
$errors[] = 'Verify Email must match.';
}
if (empty($p) || strlen($p) < 6) {
$errors[] = 'Password is required and must be at least 6 characters long.';
}
if ($pc !== $p) {
$errors[] = 'Verify Password must match.';
}
if (!empty($errors)) {
echo "<ul>";
foreach ($errors as $error) echo "<li>$error</li>";
echo "</ul>";
exit;
}
$form = $form['data'];
if (db()->query( if (db()->query(
"INSERT INTO users (username, password, email, verify, charclass, authlevel) VALUES (?, ?, ?, 1, ?, 1)", "INSERT INTO users (username, password, email, verify, charclass, authlevel) VALUES (?, ?, ?, 1, ?, 1)",
[$u, password_hash($p, PASSWORD_ARGON2ID), $e, $_POST['charclass']] [$form['username'], password_hash($form['password'], PASSWORD_ARGON2ID), $form['email'], $form['charclass']]
) === false) { ) === false) {
echo "Failed to create user."; exit("Failed to create user.");
exit;
} }
file_put_contents('../.installed', date('Y-m-d H:i:s')); file_put_contents('../.installed', date('Y-m-d H:i:s'));

View File

@ -16,15 +16,26 @@ function login()
if (checkcookies() !== false) redirect('index.php'); if (checkcookies() !== false) redirect('index.php');
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$u = trim($_POST['username'] ?? ''); $form = validate($_POST, [
'username' => ['length:3-18', 'alpha-spaces'],
'password' => ['length:6-255'],
'remember' => ['bool']
]);
$query = db()->query('SELECT id, username, password FROM users WHERE username = ? LIMIT 1;', [$u]); if (!$form['valid']) {
if ($query === false) die("Invalid username or password. Please go back and try again."); exit(ul_from_validate_errors($form['errors']));
}
$form = $form['data'];
$query = db()->query('SELECT id, username, password FROM users WHERE username = ? LIMIT 1;', [$form['username']]);
$row = $query->fetchArray(SQLITE3_ASSOC); $row = $query->fetchArray(SQLITE3_ASSOC);
if (!password_verify($_POST['password'] ?? '', $row['password'])) die("Invalid username or password. Please go back and try again.");
$expiretime = isset($_POST["rememberme"]) ? time() + 31536000 : 0; if ($row === false || !password_verify($_POST['password'] ?? '', $row['password']))
$rememberme = isset($_POST["rememberme"]) ? 1 : 0; die("Invalid username or password. Please go back and try again.");
$expiretime = $form['remember'] ? time() + 31536000 : 0;
$rememberme = $form['remember'] ? 1 : 0;
$cookie = implode(' ', [$row['id'], $row['username'], $row['password'], $rememberme]); $cookie = implode(' ', [$row['id'], $row['username'], $row['password'], $rememberme]);
set_cookie("dkgame", $cookie, $expiretime); set_cookie("dkgame", $cookie, $expiretime);
@ -41,6 +52,5 @@ function login()
function logout() function logout()
{ {
set_cookie("dkgame", "", -3600); set_cookie("dkgame", "", -3600);
header("Location: login.php?do=login"); redirect('login.php?do=login');
die();
} }

View File

@ -24,61 +24,28 @@ function register()
global $controlrow; global $controlrow;
if (isset($_POST["submit"])) { if (isset($_POST["submit"])) {
$u = trim($_POST['username'] ?? ''); $form = validate($_POST, [
$e = trim($_POST['email1'] ?? ''); 'username' => ['length:3-18', 'alpha-spaces', 'unique:users,username'],
$e2 = trim($_POST['email2'] ?? ''); 'email' => ['email', 'unique:users,email'],
$p = $_POST['password1'] ?? ''; 'confirm_email' => ['confirm'],
$p2 = $_POST['password2'] ?? ''; 'password' => ['length:6-255'],
'confirm_password' => ['confirm'],
'charclass' => ['in:1,2,3']
]);
$errors = []; if (!$form['valid']) {
$err = ul_from_validate_errors($form['errors']);
// Process username.
if (empty($u) || strlen($u) < 3 || strlen($u) > 18 || !ctype_alnum(str_replace(' ', '', $u))) {
$errors[] = 'Username is required and must be between 3 and 18 characters long and contain only
alphanumeric characters and spaces.';
}
if (db()->exists('users', 'username', $u)) {
$errors[] = 'Username already taken. Try another.';
}
// Process email address.
if (empty($e) || strlen($e) > 255 || !filter_var($e, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Email is required must be a valid email address.';
}
if ($e !== $e2) {
$errors[] = 'Verify Email must match.';
}
if (db()->exists('users', 'email', $e)) {
$errors[] = 'Email already taken. Forgot your password?';
}
// Process password.
if (empty($p) || strlen($p) < 6) {
$errors[] = 'Password is required and must be at least 6 characters long.';
}
if ($p2 !== $p) {
$errors[] = 'Verify Password must match.';
}
$password = password_hash($p, PASSWORD_ARGON2ID);
if (count($errors) !== 0) {
$err = "<ul>";
foreach ($errors as $error) $err .= "<li>$error</li>";
$err .= "</ul>";
$page = "The following error(s) occurred when your account was being made:<br><span style=\"color:red;\">$err</span><br>Please go back and try again."; $page = "The following error(s) occurred when your account was being made:<br><span style=\"color:red;\">$err</span><br>Please go back and try again.";
} else { } else {
$form = $form['data'];
$password = password_hash($form['password'], PASSWORD_ARGON2ID);
$token = ($controlrow['verifyemail'] == true) ? token(8) : 'g2g'; $token = ($controlrow['verifyemail'] == true) ? token(8) : 'g2g';
db()->query('INSERT INTO users (verify, username, password, email, charclass) VALUES (?, ?, ?, ?, ?)', [ db()->query('INSERT INTO users (verify, username, password, email, charclass) VALUES (?, ?, ?, ?, ?)', [
$token, $u, $password, $e, $_POST['charclass'] ?? 1 $token, $form['username'], $password, $form['email'], $form['charclass']
]); ]);
if ($controlrow['verifyemail'] == true) { if ($controlrow['verifyemail'] == true) {
if (sendregmail($e, $token)) { if (sendregmail($form['email'], $token)) {
$page = "Your account was created successfully.<br><br>You should receive an Account Verification email shortly. You will need the verification code contained in that email before you are allowed to log in. Once you have received the email, please visit the <a href=\"users.php?do=verify\">Verification Page</a> to enter your code and start playing."; $page = "Your account was created successfully.<br><br>You should receive an Account Verification email shortly. You will need the verification code contained in that email before you are allowed to log in. Once you have received the email, please visit the <a href=\"users.php?do=verify\">Verification Page</a> to enter your code and start playing.";
} else { } else {
$page = "Your account was created successfully.<br><br>However, there was a problem sending your verification email. Please check with the game administrator to help resolve this problem."; $page = "Your account was created successfully.<br><br>However, there was a problem sending your verification email. Please check with the game administrator to help resolve this problem.";
@ -97,7 +64,6 @@ function register()
$page = parsetemplate(gettemplate("register"), $controlrow); $page = parsetemplate(gettemplate("register"), $controlrow);
} }
$topnav = "<a href=\"login.php?do=login\"><img src=\"images/button_login.gif\" alt=\"Log In\" border=\"0\" /></a><a href=\"users.php?do=register\"><img src=\"images/button_register.gif\" alt=\"Register\" border=\"0\" /></a><a href=\"help.php\"><img src=\"images/button_help.gif\" alt=\"Help\" border=\"0\" /></a>";
display($page, "Register", false, false, false); display($page, "Register", false, false, false);
} }

View File

@ -343,3 +343,186 @@ function token($length = 32): string
{ {
return bin2hex(random_bytes($length)); return bin2hex(random_bytes($length));
} }
/**
* Validate any given array of data against rules. Returns [valid, data, error]. Data contains the trimmed
* values from the input array. Note: all fields with rules are assumed to be required, unless the optional
* rule is used.
*
* Example: ['required', 'no-trim', 'length:5-20', 'alphanum-spaces']
*/
function validate(array $input_data, array $rules): array
{
$data = [];
$errors = [];
foreach ($rules as $field => $field_rules) {
$value = $input_data[$field] ?? null;
$field_name = ucfirst(str_replace('_', ' ', $field));
$is_required = true;
$default_value = null;
if (in_array('optional', $field_rules)) {
$is_required = false;
}
foreach ($field_rules as $rule) {
if (strpos($rule, 'default:') === 0) {
$default_value = substr($rule, 8);
break;
}
}
if (empty($value) && $default_value !== null) {
$value = $default_value;
}
if (empty($value) && !$is_required) continue;
if ($is_required && empty($value)) {
$errors[$field][] = "{$field_name} is required.";
continue;
}
if (!in_array('no-trim', $field_rules)) {
$value = trim($value);
}
$data[$field] = $value;
foreach ($field_rules as $rule) {
// Parse rule and arguments
if (strpos($rule, ':') !== false) {
list($rule_name, $rule_args) = explode(':', $rule, 2);
} else {
$rule_name = $rule;
$rule_args = null;
}
if ($rule_name === 'optional') continue;
switch ($rule_name) {
case 'bool':
if (!isset($input_data[$field]) || empty($value)) {
$value = false;
} else {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($value === null) {
$errors[$field][] = "{$field_name} must be a valid boolean value.";
}
}
break;
case 'length':
list($min, $max) = explode('-', $rule_args);
$len = strlen((string)$value);
if ($len < $min || $len > $max) {
$errors[$field][] = "{$field_name} must be between {$min} and {$max} characters.";
}
break;
case 'alphanum':
if (!preg_match('/^[a-zA-Z0-9]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alpha':
if (!preg_match('/^[a-zA-Z]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alphanum-spaces':
if (!preg_match('/^[a-zA-Z0-9\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'alpha-spaces':
if (!preg_match('/^[a-zA-Z\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'email':
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field][] = "{$field_name} must be a valid email address.";
}
break;
case 'int':
if (!filter_var($value, FILTER_VALIDATE_INT)) {
$errors[$field][] = "{$field_name} must be an integer.";
}
break;
case 'min':
if ($value < $rule_args) {
$errors[$field][] = "{$field_name} must be at least {$rule_args}.";
}
break;
case 'max':
if ($value > $rule_args) {
$errors[$field][] = "{$field_name} must be no more than {$rule_args}.";
}
break;
case 'regex':
if (!preg_match($rule_args, $value)) {
$errors[$field][] = "{$field_name} does not match the required pattern.";
}
break;
case 'in':
$options = explode(',', $rule_args);
if (!in_array($value, $options)) {
$errors[$field][] = "{$field_name} must be one of: " . implode(', ', $options);
}
break;
case 'confirm':
$field_to_confirm = substr($field, 8);
$confirm_value = $data[$field_to_confirm] ?? '';
$confirm_field_name = ucfirst(str_replace('_', ' ', $field_to_confirm));
if ($value !== $confirm_value) {
$errors[$field][] = "{$field_name} must match {$confirm_field_name}.";
}
break;
case 'unique':
list($table, $column) = explode(',', $rule_args, 2);
if (db()->exists($table, $column, $value)) {
$errors[$field][] = "{$field_name} must be unique.";
}
break;
}
}
}
foreach ($input_data as $field => $value) {
if (!isset($data[$field])) $data[$field] = trim($value);
}
return [
'valid' => empty($errors),
'data' => $data,
'errors' => $errors
];
}
/**
* Generates a ul list from `validate()`'s errors.
*/
function ul_from_validate_errors(array $errors): string
{
$string = '<ul>';
foreach ($errors as $field => $errors) {
$string .= '<li>';
foreach ($errors as $error) $string .= $error;
$string .= '</li>';
}
return $string . '</ul>';
}

View File

@ -1,13 +1,35 @@
<?php <?php
$template = <<<THEVERYENDOFYOU
<form action="login.php?do=login" method="post"> $template = <<<HTML
<table width="75%"> <form action="login.php?do=login" method="post">
<tr><td width="30%">Username:</td><td><input type="text" size="30" name="username" /></td></tr> <table width="75%">
<tr><td>Password:</td><td><input type="password" size="30" name="password" /></td></tr> <tr>
<tr><td>Remember me?</td><td><input type="checkbox" name="rememberme" value="yes" /> Yes</td></tr> <td width="30%">Username:</td>
<tr><td colspan="2"><input type="submit" name="submit" value="Log In" /></td></tr> <td><input type="text" name="username"></td>
<tr><td colspan="2">Checking the "Remember Me" option will store your login information in a cookie so you don't have to enter it next time you get online.<br><br>Want to play? You gotta <a href="users.php?do=register">register your own character.</a><br><br>You may also <a href="users.php?do=changepassword">change your password</a>, or <a href="users.php?do=lostpassword">request a new one</a> if you've lost yours.</td></tr> </tr>
</table> <tr>
</form> <td>Password:</td>
THEVERYENDOFYOU; <td><input type="password" name="password"></td>
?> </tr>
<tr>
<td>Remember me?</td>
<td>
<input type="hidden" name="remember" value="0">
<input type="checkbox" name="remember" value="1"> Yes
</td>
</tr>
<tr>
<td colspan="2"><input type="submit" name="submit" value="Log In"></td>
</tr>
<tr>
<td colspan="2">
Checking the "Remember Me" option will store your login information in a cookie so you don't have
to enter it next time you get online.<br><br>Want to play? You gotta
<a href="users.php?do=register">register your own character.</a><br><br>You may also
<a href="users.php?do=changepassword">change your password</a>, or
<a href="users.php?do=lostpassword">request a new one</a> if you've lost yours.
</td>
</tr>
</table>
</form>
HTML;