diff --git a/public/admin.php b/public/admin.php index a8d83bc..20024fb 100644 --- a/public/admin.php +++ b/public/admin.php @@ -62,26 +62,30 @@ function donothing() function primary() { 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'); - $gs = (int) trim($_POST['gamesize'] ?? 250); - $c1n = trim($_POST['class1name'] ?? 'Mage'); - $c2n = trim($_POST['class2name'] ?? 'Warrior'); - $c3n = trim($_POST['class3name'] ?? 'Paladin'); + if ($form['valid']) { + $form = $form['data']; + if (($form['gamesize'] % 5) != 0) exit('Map size must be divisible by five.'); - 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;', [ - $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"); } else { - $errorlist = implode('
', $errors); + $errorlist = ul_from_validate_errors($form['errors']); admindisplay("Errors:
$errorlist

Please go back and try again.", "Main Settings"); } } diff --git a/public/forum.php b/public/forum.php index b9e2568..1c7f1aa 100644 --- a/public/forum.php +++ b/public/forum.php @@ -85,13 +85,22 @@ function reply() { global $userrow; - $p = $_POST['parent'] ?? 0; - $t = trim($_POST['title'] ?? ''); - $c = trim($_POST['content'] ?? ''); + $form = validate($_POST, [ + 'title' => ['length:2-30', 'alphanum-spaces'], + 'content' => [] + ]); - db()->query('INSERT INTO forum (author, title, content, parent) VALUES (?, ?, ?, ?);', [$userrow['username'], $t, $c, $p]); - db()->query('UPDATE forum SET newpostdate=CURRENT_TIMESTAMP, replies=replies + 1 WHERE id=?;', [$p]); - redirect("forum.php?do=thread:$p:0"); + if (!$form['valid']) { + exit(ul_from_validate_errors($form['errors'])); + } + + $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() @@ -99,10 +108,19 @@ function newthread() global $userrow; if (isset($_POST["submit"])) { - extract($_POST); - $t = trim($_POST['title'] ?? ''); - $c = trim($_POST['content'] ?? ''); - db()->query('INSERT INTO forum (author, title, content) VALUES (?, ?, ?);', [$userrow['username'], $t, $c]); + $form = validate($_POST, [ + 'title' => ['length:2-30', 'alphanum-spaces'], + 'content' => [] + ]); + + 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'); } diff --git a/public/install.php b/public/install.php index a7c91b6..3c11afb 100644 --- a/public/install.php +++ b/public/install.php @@ -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.

- - - - - + + + + +
Username:


Password:
Verify Password:


Email Address:
Verify Email:


Username:


Password:
Verify Password:


Email Address:
Verify Email:


Character Class:
@@ -676,48 +676,22 @@ function third() */ function fourth() { - $u = trim($_POST['username'] ??= ''); - $e = trim($_POST['email1'] ??= ''); - $ec = trim($_POST['email2'] ??= ''); - $p = $_POST['password1'] ??= ''; - $pc = $_POST['password2'] ??= ''; + $form = validate($_POST, [ + 'username' => ['length:3-18', 'alpha-spaces'], + 'email' => ['email'], + 'confirm_email' => ['confirm'], + 'password' => ['length:6-255'], + 'confirm_password' => ['confirm'] + ]); - $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 ""; - exit; - } + if (!$form['valid']) exit(ul_from_validate_errors($form['errors'])); + $form = $form['data']; if (db()->query( "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) { - echo "Failed to create user."; - exit; + exit("Failed to create user."); } file_put_contents('../.installed', date('Y-m-d H:i:s')); diff --git a/public/login.php b/public/login.php index 7d6af16..cae0927 100644 --- a/public/login.php +++ b/public/login.php @@ -16,15 +16,26 @@ function login() if (checkcookies() !== false) redirect('index.php'); 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 ($query === false) die("Invalid username or password. Please go back and try again."); + if (!$form['valid']) { + 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); - 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; - $rememberme = isset($_POST["rememberme"]) ? 1 : 0; + if ($row === false || !password_verify($_POST['password'] ?? '', $row['password'])) + 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]); set_cookie("dkgame", $cookie, $expiretime); @@ -41,6 +52,5 @@ function login() function logout() { set_cookie("dkgame", "", -3600); - header("Location: login.php?do=login"); - die(); + redirect('login.php?do=login'); } diff --git a/public/users.php b/public/users.php index 0ebbbf8..d94cb3a 100644 --- a/public/users.php +++ b/public/users.php @@ -24,61 +24,28 @@ function register() global $controlrow; if (isset($_POST["submit"])) { - $u = trim($_POST['username'] ?? ''); - $e = trim($_POST['email1'] ?? ''); - $e2 = trim($_POST['email2'] ?? ''); - $p = $_POST['password1'] ?? ''; - $p2 = $_POST['password2'] ?? ''; + $form = validate($_POST, [ + 'username' => ['length:3-18', 'alpha-spaces', 'unique:users,username'], + 'email' => ['email', 'unique:users,email'], + 'confirm_email' => ['confirm'], + 'password' => ['length:6-255'], + 'confirm_password' => ['confirm'], + 'charclass' => ['in:1,2,3'] + ]); - $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 = ""; + if (!$form['valid']) { + $err = ul_from_validate_errors($form['errors']); $page = "The following error(s) occurred when your account was being made:
$err
Please go back and try again."; } else { + $form = $form['data']; + $password = password_hash($form['password'], PASSWORD_ARGON2ID); $token = ($controlrow['verifyemail'] == true) ? token(8) : 'g2g'; 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 (sendregmail($e, $token)) { + if (sendregmail($form['email'], $token)) { $page = "Your account was created successfully.

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 Verification Page to enter your code and start playing."; } else { $page = "Your account was created successfully.

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); } - $topnav = "\"Log\"Register\"\"Help\""; display($page, "Register", false, false, false); } diff --git a/src/lib.php b/src/lib.php index 3c9dffc..45a0bc0 100644 --- a/src/lib.php +++ b/src/lib.php @@ -343,3 +343,186 @@ function token($length = 32): string { 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 = ''; +} diff --git a/templates/login.php b/templates/login.php index fb5207a..0cde59a 100644 --- a/templates/login.php +++ b/templates/login.php @@ -1,13 +1,35 @@ - - - - - - -
Username:
Password:
Remember me? Yes
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.

Want to play? You gotta register your own character.

You may also change your password, or request a new one if you've lost yours.
-
-THEVERYENDOFYOU; -?> + +$template = << + + + + + + + + + + + + + + + + + + + +
Username:
Password:
Remember me? + + Yes +
+ 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.

Want to play? You gotta + register your own character.

You may also + change your password, or + request a new one if you've lost yours. +
+ +HTML;