more work

This commit is contained in:
Sky Johnson 2024-10-05 14:23:32 -05:00
parent 2263134e55
commit 36d8f3405f
29 changed files with 470 additions and 365 deletions

View File

@ -4,12 +4,12 @@
* A collection of functions to colorize the output of the terminal. * A collection of functions to colorize the output of the terminal.
*/ */
function c(string $c, string $s): string { return $c . $s . "\033[0m"; } function c($c, $s) { return $c . $s . "\033[0m"; }
function black(string $s): string { return c("\033[30m", $s); } function black($s) { return c("\033[30m", $s); }
function red(string $s): string { return c("\033[31m", $s); } function red($s) { return c("\033[31m", $s); }
function green(string $s): string { return c("\033[32m", $s); } function green($s) { return c("\033[32m", $s); }
function yellow(string $s): string { return c("\033[33m", $s); } function yellow($s) { return c("\033[33m", $s); }
function blue(string $s): string { return c("\033[34m", $s); } function blue($s) { return c("\033[34m", $s); }
function magenta(string $s): string { return c("\033[35m", $s); } function magenta($s) { return c("\033[35m", $s); }
function cyan(string $s): string { return c("\033[36m", $s); } function cyan($s) { return c("\033[36m", $s); }
function white(string $s): string { return c("\033[37m", $s); } function white($s) { return c("\033[37m", $s); }

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,7 @@ const BPS = 'blueprints.db';
/** /**
* Echo a string with a newline. * Echo a string with a newline.
*/ */
function eln(string $string): void function eln($string)
{ {
echo $string . PHP_EOL; echo $string . PHP_EOL;
} }
@ -63,7 +63,7 @@ if ($database === AUTH || $database === 'reset') {
password TEXT NOT NULL, password TEXT NOT NULL,
auth INT NOT NULL DEFAULT 0, auth INT NOT NULL DEFAULT 0,
char_id INTEGER NOT NULL DEFAULT 0, char_id INTEGER NOT NULL DEFAULT 0,
char_slots INTEGER NOT NULL DEFAULT 3, char_slots INTEGER NOT NULL DEFAULT 300,
created DATETIME DEFAULT CURRENT_TIMESTAMP, created DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME DEFAULT CURRENT_TIMESTAMP last_login DATETIME DEFAULT CURRENT_TIMESTAMP
)'); )');
@ -656,7 +656,7 @@ if ($database === LIVE || $database === 'reset') {
if ($database !== 'reset') exit(0); if ($database !== 'reset') exit(0);
} }
function created_or_error(bool $result, string $table): void function created_or_error($result, $table)
{ {
if ($result === false) { if ($result === false) {
eln(red('Failed to create table: ') . $table); eln(red('Failed to create table: ') . $table);

56
docs/captcha.md Normal file
View File

@ -0,0 +1,56 @@
Here is some example code for implementing a CAPTCHA, using the gd extension to create a server-rendered, randomized image.
```php
<?php
// Start the session to store the CAPTCHA answer
session_start();
// Define the image dimensions
$width = 200;
$height = 60;
// Create the image resource
$image = imagecreatetruecolor($width, $height);
// Define colors
$bg_color = imagecolorallocate($image, 255, 255, 255); // White background
$text_color = imagecolorallocate($image, 0, 0, 0); // Black text
$noise_color = imagecolorallocate($image, 100, 100, 100); // Gray for noise
// Fill the background
imagefill($image, 0, 0, $bg_color);
// Generate a random math equation (e.g., 3 + 5)
$num1 = rand(1, 9);
$num2 = rand(1, 9);
$operator = rand(0, 1) ? '+' : '-'; // Randomly choose addition or subtraction
$equation = "$num1 $operator $num2 = ?";
$answer = $operator == '+' ? ($num1 + $num2) : ($num1 - $num2);
// Store the answer in the session
$_SESSION['captcha'] = $answer;
// Add noise to the image (random dots)
for ($i = 0; $i < 1000; $i++) {
imagesetpixel($image, rand(0, $width), rand(0, $height), $noise_color);
}
// Set the path to the ZXX font
$font = __DIR__ . '/fonts/ZXX-Regular.ttf'; // Path to the ZXX font file
// Add the math equation using ZXX font
$font_size = 30;
$x = 20;
$y = 40;
imagettftext($image, $font_size, rand(-10, 10), $x, $y, $text_color, $font, $equation);
// Output the image as PNG
header('Content-Type: image/png');
imagepng($image);
// Free the image resource
imagedestroy($image);
?>
```
https://github.com/kawaiidesune/zxx

View File

@ -136,7 +136,19 @@ aside#left {
&:hover, &.active { &:hover, &.active {
color: white; color: white;
background-color: black; }
&:hover {
background-color: rgba(0, 0, 0, 0.3);
}
&.active {
background-color: #444c55;
color: #ffffff;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
} }
} }
} }
@ -182,6 +194,7 @@ span.badge {
color: #111111; color: #111111;
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 0.1rem 0.25rem; padding: 0.1rem 0.25rem;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) inset;
&.dark { &.dark {
background-color: #444c55; background-color: #444c55;
@ -343,3 +356,14 @@ h1:has(.badge), h2:has(.badge), h3:has(.badge), h4:has(.badge), h5:has(.badge),
border-color: #b3b3b3; border-color: #b3b3b3;
} }
} }
a {
color: #4C0515;
text-decoration: none;
transition: color 0.2s ease;
&:hover {
color: #6C0515;
text-decoration: underline;
}
}

View File

@ -3,99 +3,51 @@
outline: none; outline: none;
display: block; display: block;
width: 100%; width: 100%;
height: 34px; padding: 0.5rem;
padding: 6px 12px; color: white;
font-size: 14px; background-color: rgba(0, 0, 0, 0.2);
line-height: 1.42857143; border: 1px solid transparent;
color: #555555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); font-size: 1rem;
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
}
/* &::placeholder {
.radio-block { color: rgba(255, 255, 255, 0.7);
& > input[type="radio"] {
display: none;
} }
& > label { &:hover {
cursor: pointer; background-color: rgba(0, 0, 0, 0.3);
display: inline-block; }
border: none;
font-size: 1rem; &:focus {
background: #f7f8fa linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1)); background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset; border-color: rgba(0, 0, 0, 0.8);
color: #111111; }
padding: 0.5rem 1rem 0.5rem;
text-align: center; &.error {
border-radius: 3px; background-color: rgba(255, 43, 43, 0.2);
user-select: none;
text-decoration: none;
transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease;
-webkit-tap-highlight-color: transparent;
&:hover { &:hover {
background-color: #e0e0e0; background-color: rgba(255, 43, 43, 0.3);
background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
color: rgba(0, 0, 0, 0.8);
} }
& > .badge { &:focus {
background-color: #444c55; background-color: rgba(255, 43, 43, 0.3);
color: white; border-color: rgba(255, 43, 43, 0.8);
} }
} }
&.active > label {
background-color: #444c55;
color: #ffffff;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
& > .badge {
background-color: #f7f8fa;
color: #111111;
}
}
/* When the radio button is checked, change the background color of the label
& > input[type="radio"]:checked + label {
background-color: #5a9bd4;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(60, 100, 150, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
border: 1px solid;
border-color: #4a8ab0 #3a7a9c #2a6a88;
}
/* When the radio button is disabled, show a normal cursor
& > input[type="radio"]:disabled + label {
cursor: default;
}
} }
*/
.radio-block-2 { .form.group {
& > input[type="radio"] { margin-bottom: 1rem;
display: none;
}
& > label { & > label {
cursor: pointer;
display: block; display: block;
margin-bottom: 0.5rem;
}
& > .form.control:not(:last-child) {
margin-bottom: 0.5rem;
} }
} }
@ -113,28 +65,48 @@
width: 100%; width: 100%;
border-radius: 0.15rem; border-radius: 0.15rem;
cursor: pointer; cursor: pointer;
transition: color, background-color 0.2s ease; transition: color, background-color, border-color, background-image 0.2s ease;
padding: 0.5rem; padding: 0.5rem;
border: 1px solid transparent;
background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0));
&:hover { &:hover {
background-color: black; background-color: rgba(0, 0, 0, 0.3);
color: white; color: white;
} }
& > .badge { & > .badge {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
& > span.selected {
display: none;
margin-left: auto;
color: #a6e3a1;
}
} }
&.active > label { &.active > label {
background-color: black; background-color: #444c55;
color: white; color: #ffffff;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
& > span.selected {
display: inline-block;
}
} }
/* When the radio button is checked, change the background color of the label */ /* When the radio button is checked, change the background color of the label */
& > input[type="radio"]:checked + label { & > input[type="radio"]:checked + label {
background-color: #f4cc67; background-color: #f4cc67;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
color: #111111; color: #111111;
border: 1px solid;
border-color: #C59F43 #AA8326 #957321;
} }
/* When the radio button is disabled, show a normal cursor */ /* When the radio button is disabled, show a normal cursor */
@ -142,3 +114,8 @@
cursor: default; cursor: default;
} }
} }
/* If there is no character selected, hide the buttons */
.character-select:not(:has(input[type="radio"]:checked)) > .buttons {
display: none;
}

View File

@ -3,7 +3,7 @@
/** /**
* Checks if the given username already exists. * Checks if the given username already exists.
*/ */
function auth_username_exists(string $username): bool function auth_username_exists($username)
{ {
return db_exists(db_auth(), 'users', 'username', $username); return db_exists(db_auth(), 'users', 'username', $username);
} }
@ -11,7 +11,7 @@ function auth_username_exists(string $username): bool
/** /**
* Checks if the given email already exists. * Checks if the given email already exists.
*/ */
function auth_email_exists(string $email): bool function auth_email_exists($email)
{ {
return db_exists(db_auth(), 'users', 'email', $email); return db_exists(db_auth(), 'users', 'email', $email);
} }
@ -20,7 +20,7 @@ function auth_email_exists(string $email): bool
* Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me * Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me
* cookie. If a remember me cookie exists, validate the session and set $_SESSION['user']. * cookie. If a remember me cookie exists, validate the session and set $_SESSION['user'].
*/ */
function auth_check(): bool function auth_check()
{ {
if (isset($_SESSION['user'])) return true; if (isset($_SESSION['user'])) return true;
@ -42,7 +42,7 @@ function auth_check(): bool
* Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and * Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and
* populate the $_SESSION['user'] array. * populate the $_SESSION['user'] array.
*/ */
function auth_only(): void function auth_only()
{ {
if (!auth_check()) redirect('/auth/login'); if (!auth_check()) redirect('/auth/login');
} }
@ -50,7 +50,7 @@ function auth_only(): void
/** /**
* If there is a user logged in, redirect to the home page. Used for when we have a guest-only page. * If there is a user logged in, redirect to the home page. Used for when we have a guest-only page.
*/ */
function guest_only(): void function guest_only()
{ {
if (auth_check()) redirect('/'); if (auth_check()) redirect('/');
} }
@ -59,7 +59,7 @@ function guest_only(): void
* Ensure the user has a character selected. If they have no character, redirect to the character creation page. Otherwise, * Ensure the user has a character selected. If they have no character, redirect to the character creation page. Otherwise,
* select the first character attached to the user. * select the first character attached to the user.
*/ */
function must_have_character(): void function must_have_character()
{ {
// If there is a character selected, make sure the session is up to date. // If there is a character selected, make sure the session is up to date.
if ($_SESSION['user']['char_id'] !== 0) { if ($_SESSION['user']['char_id'] !== 0) {

View File

@ -11,6 +11,7 @@ require_once SRC . '/database.php';
require_once SRC . '/auth.php'; require_once SRC . '/auth.php';
require_once SRC . '/router.php'; require_once SRC . '/router.php';
require_once SRC . '/components.php'; require_once SRC . '/components.php';
require_once SRC . '/render.php';
// Database models // Database models
require_once SRC . '/models/user.php'; require_once SRC . '/models/user.php';
@ -39,5 +40,8 @@ csrf();
// Have a global counter for queries // Have a global counter for queries
$GLOBALS['queries'] = 0; $GLOBALS['queries'] = 0;
// Set the default page layout
page_layout('basic');
// Run auth_check to see if we're logged in, since it populates the user data in SESSION // Run auth_check to see if we're logged in, since it populates the user data in SESSION
auth_check(); auth_check();

View File

@ -8,7 +8,7 @@ const nav_tabs = [
/** /**
* Render the logout button's form. * Render the logout button's form.
*/ */
function c_logout_button(): string function c_logout_button()
{ {
return render('components/logout_button'); return render('components/logout_button');
} }
@ -17,24 +17,31 @@ function c_logout_button(): string
* Render the character bar. Relies on there being a character in the session. Without one, this will return an empty * Render the character bar. Relies on there being a character in the session. Without one, this will return an empty
* string. * string.
*/ */
function c_char_bar(): string function c_char_bar()
{ {
if (char() === false) return ''; if (char() === false) return '';
return render('components/char_bar', ['char' => char()]); return render('components/char_bar', ['char' => char()]);
} }
/** /**
* Render the left sidebar navigation menu. Provide the active tab to highlight it. * Render the left sidebar navigation menu. Provide the active tab to highlight it. Will retrieve the current
* tab from the GLOBALS.
*/ */
function c_left_nav(int $activeTab): string function c_left_nav()
{ {
return render('components/left_nav', ['activeTab' => $activeTab]); $tab = match ($GLOBALS['active_nav_tab'] ?? '') {
'home' => 0,
'chars' => 1,
default => 0
};
return render('components/left_nav', ['ant' => $tab]);
} }
/** /**
* Render the debug query log. * Render the debug query log.
*/ */
function c_debug_query_log(): string function c_debug_query_log()
{ {
return render('components/debug_query_log'); return render('components/debug_query_log');
} }
@ -42,7 +49,7 @@ function c_debug_query_log(): string
/** /**
* Render the character select radio buttons. * Render the character select radio buttons.
*/ */
function c_char_select_box(int $id, array $char): string function c_char_select_box($id, array $char)
{ {
return render('components/char_select_box', ['id' => $id, 'char' => $char]); return render('components/char_select_box', ['id' => $id, 'char' => $char]);
} }
@ -50,15 +57,34 @@ function c_char_select_box(int $id, array $char): string
/** /**
* Render an alert with a given type and message. * Render an alert with a given type and message.
*/ */
function c_alert(string $t, string $m): string function c_alert($t, $m)
{ {
return "<div class=\"alert $t\">$m</div>"; return "<div class=\"alert $t\">$m</div>";
} }
/** /**
* Generate a form field. * Renders a danger alert with form errors, if there are any. Add an optional placement id.
*/ */
function c_form_field(string $type, string $name, string $id, string $placeholder, bool $required = false): string function c_form_errors($placement = '')
{ {
return render('components/form_field', ['type' => $type, 'name' => $name, 'id' => $id, 'placeholder' => $placeholder, 'required' => $required]); $errors = $GLOBALS[($placement !== '' ? "form-errors-$placement" : 'form-errors')] ?? false;
if ($errors === false) return '';
$html = '';
foreach ($errors as $field)
foreach ($field as $message) $html .= "<p>$message</p>";
return c_alert('danger', $html);
}
/**
* Generate a text form field. This component will automatically add the error class if there are errors for this field,
* depending on the form-errors GLOBAL. Pass an optional form ID to target a specific form GLOBAL.
*/
function c_form_field($type, $name, $placeholder, $required = false, $autocomplete = "off", $formId = '')
{
$errors = $GLOBALS[($formId !== '' ? "form-errors-$formId" : 'form-errors')] ?? false;
$html = "<input type=\"$type\" name=\"$name\" id=\"$name\" placeholder=\"$placeholder\"";
if ($required) $html .= ' required';
if (isset($_POST[$name]) && $type !== 'password') $html .= ' value="' . $_POST[$name] . '"';
$html .= $errors !== false && !empty($errors[$name]) ? ' class="form control error"' : ' class="form control"';
return $html . " autocomplete=\"$autocomplete\">";
} }

View File

@ -3,7 +3,7 @@
/** /**
* Displays the registration page. * Displays the registration page.
*/ */
function auth_controller_register_get(): void function auth_controller_register_get()
{ {
guest_only(); guest_only();
echo render('layouts/basic', ['view' => 'pages/auth/register']); echo render('layouts/basic', ['view' => 'pages/auth/register']);
@ -12,20 +12,16 @@ function auth_controller_register_get(): void
/** /**
* Handles the registration form submission. * Handles the registration form submission.
*/ */
function auth_controller_register_post(): void function auth_controller_register_post()
{ {
guest_only(); guest_only();
csrf_ensure(); csrf_ensure();
$errors = []; $errors = [];
$u = $_POST['username'] ?? ''; $u = trim($_POST['u'] ?? '');
$e = $_POST['email'] ?? ''; $e = trim($_POST['e'] ?? '');
$p = $_POST['password'] ?? ''; $p = $_POST['p'] ?? '';
// Trim the input.
$u = trim($u);
$e = trim($e);
/* /*
A username is required. A username is required.
@ -54,12 +50,6 @@ function auth_controller_register_post(): void
$errors['p'][] = 'Password is required and must be at least 6 characters long.'; $errors['p'][] = 'Password is required and must be at least 6 characters long.';
} }
// If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) {
flash('alert-registration', ['errors', $errors]);
redirect('/auth/register');
}
/* /*
A username must be unique. A username must be unique.
*/ */
@ -76,8 +66,9 @@ function auth_controller_register_post(): void
// If there are errors at this point, send them to the page with errors flashed. // If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) { if (!empty($errors)) {
flash('alert-registration', ['errors', $errors]); $GLOBALS['form-errors'] = $errors;
redirect('/auth/register'); echo page('auth/register');
exit;
} }
$user = user_create($u, $e, $p); $user = user_create($u, $e, $p);
@ -91,7 +82,7 @@ function auth_controller_register_post(): void
/** /**
* Displays the login page. * Displays the login page.
*/ */
function auth_controller_login_get(): void function auth_controller_login_get()
{ {
guest_only(); guest_only();
echo render('layouts/basic', ['view' => 'pages/auth/login']); echo render('layouts/basic', ['view' => 'pages/auth/login']);
@ -100,48 +91,35 @@ function auth_controller_login_get(): void
/** /**
* Handles the login form submission. * Handles the login form submission.
*/ */
function auth_controller_login_post(): void function auth_controller_login_post()
{ {
guest_only(); guest_only();
csrf_ensure(); csrf_ensure();
$errors = []; $errors = [];
$u = $_POST['username'] ?? ''; $u = trim($_POST['u'] ?? '');
$p = $_POST['password'] ?? ''; $p = $_POST['p'] ?? '';
// Trim the input. if (empty($u)) $errors['u'][] = 'Username is required.';
$u = trim($u); if (empty($p)) $errors['p'][] = 'Password is required.';
/*
A username is required.
*/
if (empty($u)) {
$errors['u'][] = 'Username is required.';
}
/*
A password is required.
*/
if (empty($p)) {
$errors['p'][] = 'Password is required.';
}
// If there are errors at this point, send them to the page with errors flashed. // If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) { if (!empty($errors)) {
flash('errors', $errors); $GLOBALS['form-errors'] = $errors;
redirect('/auth/login'); echo render('layouts/basic', ['view' => 'pages/auth/login']);
exit;
} }
$user = user_find($u); $user = user_find($u);
if ($user === false || !password_verify($p, $user['password'])) { if ($user === false || !password_verify($p, $user['password'])) {
$errors['u'][] = 'Invalid username or password.'; $errors['x'][] = 'Invalid username or password.';
flash('errors', $errors); $GLOBALS['form-errors'] = $errors;
redirect('/auth/login'); echo render('layouts/basic', ['view' => 'pages/auth/login']);
exit;
} }
$_SESSION['user'] = $user; $_SESSION['user'] = $user;
change_user_character($user['char_id']);
if ($_POST['remember'] ?? false) { if ($_POST['remember'] ?? false) {
$token = token(); $token = token();
@ -155,13 +133,19 @@ function auth_controller_login_post(): void
set_cookie('remember_me', $token, $expires); set_cookie('remember_me', $token, $expires);
} }
if (char_count($_SESSION['user']['id']) === 0) {
redirect('/character/create-first');
} elseif (!change_user_character($_SESSION['user']['char_id'])) {
router_error(999);
}
redirect('/'); redirect('/');
} }
/** /**
* Logs the user out. * Logs the user out.
*/ */
function auth_controller_logout_post(): void function auth_controller_logout_post()
{ {
csrf_ensure(); csrf_ensure();
session_delete($_SESSION['user']['id']); session_delete($_SESSION['user']['id']);

View File

@ -3,24 +3,22 @@
/** /**
* Display a list of characters for the currently logged in user. * Display a list of characters for the currently logged in user.
*/ */
function char_controller_list_get(): void function char_controller_list_get()
{ {
auth_only(); auth_only(); must_have_character();
must_have_character();
$chars = char_list(user('id')); $GLOBALS['active_nav_tab'] = 'chars';
echo page('chars/list', ['chars' => char_list(user('id'))]);
echo render('layouts/basic', ['view' => 'pages/chars/list', 'chars' => $chars, 'activeTab' => nav_tabs['chars']]);
} }
/** /**
* Handle an action from the character list page. * Handle an action from the character list page.
*/ */
function char_controller_list_post(): void function char_controller_list_post()
{ {
auth_only(); auth_only(); must_have_character(); csrf_ensure();
must_have_character();
csrf_ensure(); $GLOBALS['active_nav_tab'] = 'chars';
$char_id = (int) ($_POST['char_id'] ?? 0); $char_id = (int) ($_POST['char_id'] ?? 0);
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
@ -28,6 +26,12 @@ function char_controller_list_post(): void
// If the character ID is not a number, or the action is not a string, return a 400. // If the character ID is not a number, or the action is not a string, return a 400.
if (!is_numeric($char_id) || !is_string($action)) router_error(400); if (!is_numeric($char_id) || !is_string($action)) router_error(400);
// If the character ID is 0, return to the list.
if ($char_id === 0) {
flash('alert_character_list_1', ['', 'No character selected.']);
redirect('/characters');
}
// If the action is not one of the allowed actions, return a 400. // If the action is not one of the allowed actions, return a 400.
if (!in_array($action, ['select', 'delete'])) router_error(400); if (!in_array($action, ['select', 'delete'])) router_error(400);
@ -35,33 +39,25 @@ function char_controller_list_post(): void
if ($action === 'select') { if ($action === 'select') {
// If the character ID is the current character, do nothing. // If the character ID is the current character, do nothing.
if ($char_id === $_SESSION['user']['char_id'] || $char_id === 0) { if ($char_id === $_SESSION['user']['char_id'] || $char_id === 0) {
flash('info', 'You are already using that character.'); flash('alert_character_list_1', ['info', 'You are already using <b>' . char('name') . '</b>.']);
redirect('/characters'); redirect('/characters');
} }
// Make sure the character ID is valid. // Ensure the character ID is valid and belongs to the user.
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acccp)');
// Make sure the user owns the character.
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999); if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
change_user_character($char_id); change_user_character($char_id);
flash('success', 'Switched to character ' . char('name') . '!'); flash('alert_character_list_1', ['success', 'Switched to character <b>' . char('name') . '</b>!']);
} }
// If the action is to delete a character, move to the confirmation page. // If the action is to delete a character, move to the confirmation page.
if ($action === 'delete') { if ($action === 'delete') {
// Make sure the character ID is valid. // Ensure the character ID is valid and belongs to the user.
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (accdp)');
// Make sure the user owns the character.
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999); if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
$char = char_find($char_id); echo page('chars/delete', ['char' => char_find($char_id)]);
exit;
echo render('layouts/basic', ['view' => 'pages/chars/delete', 'char' => $char, 'activeTab' => nav_tabs['chars']]);
return;
} }
redirect('/characters'); redirect('/characters');
@ -70,29 +66,23 @@ function char_controller_list_post(): void
/** /**
* Delete a character for the currently logged in user. * Delete a character for the currently logged in user.
*/ */
function char_controller_delete_post(): void function char_controller_delete_post()
{ {
auth_only(); auth_only(); must_have_character(); csrf_ensure();
must_have_character();
csrf_ensure();
$char_id = (int) ($_POST['char_id'] ?? 0); $char_id = (int) ($_POST['char_id'] ?? 0);
$name = $_POST['name'] ?? '';
// If the character ID is not a number, return a 400. // If the character ID is not a number, return a 400.
if (!is_numeric($char_id)) router_error(400); if (!is_numeric($char_id)) router_error(400);
// Make sure the character ID is valid. // Ensure the character ID is valid and belongs to the user.
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acddp)');
// Make sure the user owns the character.
if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999); if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999);
$char = char_find($char_id); $char = char_find($char_id);
// Confirm the name matches the name of the character. // Confirm the name matches the name of the character. CASE SENSITIVE.
if ($char['name'] !== $_POST['name']) { if ($char['name'] !== trim($_POST['n'] ?? '')) {
flash('error', 'Failed to delete character. Name confirmation did not match.'); flash('alert_character_list_1', ['danger', 'Failed to delete <b>' . $char['name'] . '</b>. Name confirmation did not match.']);
redirect('/characters'); redirect('/characters');
} }
@ -105,37 +95,37 @@ function char_controller_delete_post(): void
if (count($chars) > 0) change_user_character($chars[0]['id']); if (count($chars) > 0) change_user_character($chars[0]['id']);
} }
flash('error', 'Character ' . $char['name'] . ' deleted.'); flash('alert_character_list_1', ['danger', 'Character <b>' . $char['name'] . '</b> deleted.']);
redirect('/characters'); redirect('/characters');
} }
/** /**
* Form to create your first character. * Form to create your first character.
*/ */
function char_controller_create_first_get(): void function char_controller_create_first_get()
{ {
auth_only(); auth_only();
$GLOBALS['active_nav_tab'] = 'chars';
// If the user already has a character, redirect them to the main page. // If the user already has a character, redirect them to the main page.
if (char_count(user('id')) > 0) redirect('/'); if (char_count(user('id')) > 0) redirect('/');
echo render('layouts/basic', ['view' => 'pages/chars/first', 'activeTab' => nav_tabs['chars']]); echo page('chars/first');
} }
/** /**
* Create a character for the currently logged in user. * Create a character for the currently logged in user.
*/ */
function char_controller_create_post(): void function char_controller_create_post()
{ {
auth_only(); auth_only(); csrf_ensure();
csrf_ensure();
$GLOBALS['active_nav_tab'] = 'chars';
$errors = []; $errors = [];
$name = $_POST['name'] ?? ''; $name = trim($_POST['n'] ?? '');
// Trim the input.
$name = trim($name);
/* /*
A name is required. A name is required.
@ -143,18 +133,27 @@ function char_controller_create_post(): void
A name must contain only alphanumeric characters and spaces. A name must contain only alphanumeric characters and spaces.
*/ */
if (empty($name) || strlen($name) < 3 || strlen($name) > 18 || !ctype_alnum(str_replace(' ', '', $name))) { if (empty($name) || strlen($name) < 3 || strlen($name) > 18 || !ctype_alnum(str_replace(' ', '', $name))) {
$errors['name'][] = 'Name is required and must be between 3 and 18 characters long and contain only alphanumeric characters and spaces.'; $errors['n'][] = 'Name is required and must be between 3 and 18 characters long and contain only alphanumeric characters and spaces.';
} }
/* /*
A character's name must be unique. A character's name must be unique.
*/ */
if (char_name_exists($name)) $errors['name'][] = 'Name is already taken.'; if (char_name_exists($name)) $errors['n'][] = 'Name is already taken.';
// If there are errors at this point, send them to the page with errors flashed. // If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) { if (!empty($errors)) {
flash('alert-cl2', ['errors', $errors]); $GLOBALS['form-errors-create-character'] = $errors;
redirect('/characters');
if (isset($_POST['first']) && $_POST['first'] === 'true') {
// If this is the first character, return to the first character creation page.
echo page('chars/first');
exit;
} else {
// If this is not the first character, return to the character list page.
echo page('chars/list', ['chars' => char_list(user('id'))]);
exit;
}
} }
// Create the character // Create the character
@ -168,7 +167,7 @@ function char_controller_create_post(): void
// Set the character as the user's selected character // Set the character as the user's selected character
change_user_character($char); change_user_character($char);
flash('alert_character_list_1', ['success', 'Character ' . $name . ' created!']); flash('alert_character_list_1', ['success', 'Character <b>' . $name . '</b> created!']);
redirect('/characters'); redirect('/characters');
} }

View File

@ -3,7 +3,7 @@
/** /**
* Open a connection to a database. * Open a connection to a database.
*/ */
function db_open(string $path): SQLite3 function db_open($path): SQLite3
{ {
$db = new SQLite3($path); $db = new SQLite3($path);
@ -55,11 +55,10 @@ function db_blueprints(): SQLite3
* Take a SQLite3 database connection, a query string, and an array of parameters. Prepare the query and * Take a SQLite3 database connection, a query string, and an array of parameters. Prepare the query and
* bind the parameters with proper type casting. Then execute the query and return the result. * bind the parameters with proper type casting. Then execute the query and return the result.
*/ */
function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result|false function db_query(SQLite3 $db, $query, array $params = [])
{ {
$stmt = $db->prepare($query); $stmt = $db->prepare($query);
if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value)); if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value));
$GLOBALS['queries']++;
db_log($query); db_log($query);
return $stmt->execute(); return $stmt->execute();
} }
@ -67,9 +66,8 @@ function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result
/** /**
* Take a SQLite3 database connection and a query string. Execute the query and return the result. * Take a SQLite3 database connection and a query string. Execute the query and return the result.
*/ */
function db_exec(SQLite3 $db, string $query): bool function db_exec(SQLite3 $db, $query)
{ {
$GLOBALS['queries']++;
db_log($query); db_log($query);
return $db->exec($query); return $db->exec($query);
} }
@ -78,18 +76,22 @@ function db_exec(SQLite3 $db, string $query): bool
* Take a SQLite3 database connection, a column name, and a value. Execute a COUNT query to see if the value * Take a SQLite3 database connection, a column name, and a value. Execute a COUNT query to see if the value
* exists in the column. Return true if the value exists, false otherwise. * exists in the column. Return true if the value exists, false otherwise.
*/ */
function db_exists(SQLite3 $db, string $table, string $column, mixed $value): bool function db_exists(SQLite3 $db, $table, $column, $value, $caseInsensitive = true)
{ {
$query = "SELECT 1 FROM $table WHERE $column = :v LIMIT 1"; if ($caseInsensitive) {
$query = "SELECT 1 FROM $table WHERE $column = :v COLLATE NOCASE LIMIT 1";
} else {
$query = "SELECT 1 FROM $table WHERE $column = :v LIMIT 1";
}
$result = db_query($db, $query, [':v' => $value]); $result = db_query($db, $query, [':v' => $value]);
db_log($query);
return $result->fetchArray(SQLITE3_NUM) !== false; return $result->fetchArray(SQLITE3_NUM) !== false;
} }
/** /**
* Return the appropriate SQLite type casting for the value. * Return the appropriate SQLite type casting for the value.
*/ */
function getSQLiteType(mixed $value): int function getSQLiteType($value): int
{ {
return match (true) { return match (true) {
is_int($value) => SQLITE3_INTEGER, is_int($value) => SQLITE3_INTEGER,
@ -102,7 +104,8 @@ function getSQLiteType(mixed $value): int
/** /**
* Log the given query string to the db debug log. * Log the given query string to the db debug log.
*/ */
function db_log(string $query): void function db_log($query)
{ {
$GLOBALS['queries']++;
if (env('debug', false)) $GLOBALS['query_log'][] = $query; if (env('debug', false)) $GLOBALS['query_log'][] = $query;
} }

View File

@ -3,7 +3,7 @@
/** /**
* Load the environment variables from the .env file. * Load the environment variables from the .env file.
*/ */
function env_load(string $filePath): void function env_load($filePath)
{ {
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)"); if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
@ -34,7 +34,7 @@ function env_load(string $filePath): void
/** /**
* Retrieve an environment variable. * Retrieve an environment variable.
*/ */
function env(string $key, mixed $default = null): mixed function env($key, $default = null)
{ {
return $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default); return $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
} }

View File

@ -1,28 +1,9 @@
<?php <?php
/**
* Return the path to a view file.
*/
function template(string $name): string
{
return __DIR__ . "/../templates/$name.php";
}
/**
* Render a view with the given data. Looks for `$view` through `template()`.
*/
function render(string $pathToBaseView, array $data = []): string|false
{
ob_start();
extract($data);
require template($pathToBaseView);
return ob_get_clean();
}
/** /**
* Generate a pretty dope token. * Generate a pretty dope token.
*/ */
function token(int $length = 32): string function token($length = 32)
{ {
return bin2hex(random_bytes($length)); return bin2hex(random_bytes($length));
} }
@ -30,7 +11,7 @@ function token(int $length = 32): string
/** /**
* Redirect to a new location. * Redirect to a new location.
*/ */
function redirect(string $location): void function redirect($location)
{ {
header("Location: $location"); header("Location: $location");
exit; exit;
@ -39,17 +20,25 @@ function redirect(string $location): void
/** /**
* Flash data to the session, or retrieve an existing flash value. Returns false if the key does not exist. * Flash data to the session, or retrieve an existing flash value. Returns false if the key does not exist.
*/ */
function flash(string $key, mixed $value = ''): mixed function flash($key, $value = '')
{ {
if ($value === '') return $_SESSION["flash_$key"] ?? false; if ($value === '') return $_SESSION["flash_$key"] ?? false;
$_SESSION["flash_$key"] = $value; $_SESSION["flash_$key"] = $value;
return $value; return $value;
} }
/**
* Clear a specific flash message.
*/
function unflash($key)
{
unset($_SESSION["flash_$key"]);
}
/** /**
* Clear all flash messages. * Clear all flash messages.
*/ */
function clear_flashes(): void function clear_flashes()
{ {
foreach ($_SESSION as $key => $_) { foreach ($_SESSION as $key => $_) {
if (str_starts_with($key, 'flash_')) unset($_SESSION[$key]); if (str_starts_with($key, 'flash_')) unset($_SESSION[$key]);
@ -59,7 +48,7 @@ function clear_flashes(): void
/** /**
* Create a CSRF token. * Create a CSRF token.
*/ */
function csrf(): string function csrf()
{ {
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = token(); if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = token();
return $_SESSION['csrf']; return $_SESSION['csrf'];
@ -68,7 +57,7 @@ function csrf(): string
/** /**
* Verify a CSRF token. * Verify a CSRF token.
*/ */
function csrf_verify(string $token): bool function csrf_verify($token)
{ {
if (hash_equals($_SESSION['csrf'] ?? '', $token)) { if (hash_equals($_SESSION['csrf'] ?? '', $token)) {
$_SESSION['csrf'] = token(); $_SESSION['csrf'] = token();
@ -81,7 +70,7 @@ function csrf_verify(string $token): bool
/** /**
* Create a hidden input field for CSRF tokens. * Create a hidden input field for CSRF tokens.
*/ */
function csrf_field(): string function csrf_field()
{ {
return '<input type="hidden" name="csrf" value="' . csrf() . '">'; return '<input type="hidden" name="csrf" value="' . csrf() . '">';
} }
@ -89,7 +78,7 @@ function csrf_field(): string
/** /**
* Kill the current request with a 418 error, if $_POST['csrf'] is invalid. * Kill the current request with a 418 error, if $_POST['csrf'] is invalid.
*/ */
function csrf_ensure(): void function csrf_ensure()
{ {
if (!csrf_verify($_POST['csrf'] ?? '')) router_error(418); if (!csrf_verify($_POST['csrf'] ?? '')) router_error(418);
} }
@ -97,7 +86,7 @@ function csrf_ensure(): void
/** /**
* Set a cookie with secure and HTTP-only flags. * Set a cookie with secure and HTTP-only flags.
*/ */
function set_cookie(string $name, string $value, int $expires): void function set_cookie($name, $value, $expires)
{ {
setcookie($name, $value, [ setcookie($name, $value, [
'expires' => $expires, 'expires' => $expires,
@ -112,7 +101,7 @@ function set_cookie(string $name, string $value, int $expires): void
/** /**
* Get the current user's array from SESSION if it exists. Specify a key to get a specific value. * Get the current user's array from SESSION if it exists. Specify a key to get a specific value.
*/ */
function user(string $field = ''): mixed function user($field = '')
{ {
if (empty($_SESSION['user'])) return false; if (empty($_SESSION['user'])) return false;
if ($field === '') return $_SESSION['user']; if ($field === '') return $_SESSION['user'];
@ -131,7 +120,7 @@ function user_selected_char(): int
* If the current user has a selected char and the data is in the session, retrieve either the full array of data * If the current user has a selected char and the data is in the session, retrieve either the full array of data
* or a specific field. If there is no character data, populate it. * or a specific field. If there is no character data, populate it.
*/ */
function char(string $field = ''): mixed function char($field = '')
{ {
// If there is no user, return false // If there is no user, return false
if (empty($_SESSION['user'])) return false; if (empty($_SESSION['user'])) return false;
@ -149,19 +138,28 @@ function char(string $field = ''): mixed
} }
/** /**
* Shorthand to update the user's selected character. * Shorthand to update the user's selected character. Returns true on success, false on failure. Database
* is updated if the character ID is different from the current session.
*/ */
function change_user_character(int $char_id): void function change_user_character($char_id)
{ {
$_SESSION['user']['char_id'] = $char_id; // If the character does not exist, return false
db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user('id')]); if (($char = char_find($char_id)) === false) return false;
$GLOBALS['char'] = char_find($char_id); $GLOBALS['char'] = $char;
// If the character ID is different, update the session and database
if ($_SESSION['user']['char_id'] !== $char_id) {
$_SESSION['user']['char_id'] = $char_id;
db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user('id')]);
}
return true;
} }
/** /**
* Get a percent between two ints, rounded to the nearest whole number or return 0. * Get a percent between two ints, rounded to the nearest whole number or return 0.
*/ */
function percent(int $num, int $denom, int $precision = 4): int function percent($num, $denom, $precision = 4): int
{ {
if ($denom === 0) return 0; if ($denom === 0) return 0;
$p = ($num / $denom) * 100; $p = ($num / $denom) * 100;
@ -173,7 +171,7 @@ function percent(int $num, int $denom, int $precision = 4): int
* the data is up to date with every request without having to query the database every use within, for example, a * the data is up to date with every request without having to query the database every use within, for example, a
* template. Will return false if the field does not exist, or the entire wallet array if no field is specified. * template. Will return false if the field does not exist, or the entire wallet array if no field is specified.
*/ */
function wallet(string $field = ''): array|int|false function wallet($field = ''): array|int
{ {
if (empty($GLOBALS['wallet'])) { if (empty($GLOBALS['wallet'])) {
$GLOBALS['wallet'] = db_query( $GLOBALS['wallet'] = db_query(
@ -190,7 +188,7 @@ function wallet(string $field = ''): array|int|false
/** /**
* Format an array of strings to a ul element. * Format an array of strings to a ul element.
*/ */
function array_to_ul(array $array): string function array_to_ul(array $array)
{ {
$html = ''; $html = '';
foreach ($array as $item) $html .= "<li>$item</li>"; foreach ($array as $item) $html .= "<li>$item</li>";

View File

@ -24,7 +24,7 @@ const currently = [
* of overrides to set additional fields. A character's name must be unique, but this function does not check for * of overrides to set additional fields. A character's name must be unique, but this function does not check for
* that. Returns the created character's ID. * that. Returns the created character's ID.
*/ */
function char_create(int $user_id, string $name, array $overrides = []): int function char_create($user_id, $name, array $overrides = []): int
{ {
// Prep the data and merge in any overrides // Prep the data and merge in any overrides
$data = ['user_id' => $user_id, 'name' => $name]; $data = ['user_id' => $user_id, 'name' => $name];
@ -49,7 +49,7 @@ function char_create(int $user_id, string $name, array $overrides = []): int
* Create a character's location record. A character's location is where they are in the game world. A character can only be * Create a character's location record. A character's location is where they are in the game world. A character can only be
* in one location at a time. Can define a starting location for the character. Default state is 'Exploring'. * in one location at a time. Can define a starting location for the character. Default state is 'Exploring'.
*/ */
function char_location_create(int $char_id, int $x = 0, int $y = 0, int $currently = 0): void function char_location_create($char_id, $x = 0, $y = 0, $currently = 0)
{ {
if (db_query(db_live(), "INSERT INTO char_locations (char_id, x, y, currently) VALUES (:p, :x, :y, :c)", [ if (db_query(db_live(), "INSERT INTO char_locations (char_id, x, y, currently) VALUES (:p, :x, :y, :c)", [
':p' => $char_id, ':p' => $char_id,
@ -65,7 +65,7 @@ function char_location_create(int $char_id, int $x = 0, int $y = 0, int $current
* Create the character's gear table. A character's gear is where they store their equipped items. * Create the character's gear table. A character's gear is where they store their equipped items.
* @TODO: implement initial gear * @TODO: implement initial gear
*/ */
function char_gear_create(int $char_id, array $initialGear = []): void function char_gear_create($char_id, array $initialGear = [])
{ {
if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) { if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) {
throw new Exception('Failed to create character gear. (cgc)'); throw new Exception('Failed to create character gear. (cgc)');
@ -73,19 +73,18 @@ function char_gear_create(int $char_id, array $initialGear = []): void
} }
/** /**
* Get a charcter by their ID. Returns the character's data as an associative array. * Get a charcter by their ID. Returns the character's data as an associative array, or false if not found.
*/ */
function char_find(int $char_id): array function char_find($char_id)
{ {
$char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC); $char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($char === false) throw new Exception('Character not found. (cf)'); return $char === false ? false : $char;
return $char;
} }
/** /**
* Count the number of characters associated with an account ID. * Count the number of characters associated with an account ID.
*/ */
function char_count(int $user_id): int function char_count($user_id): int
{ {
$count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM); $count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM);
if ($count === false) throw new Exception('Failed to count characters. (cc)'); if ($count === false) throw new Exception('Failed to count characters. (cc)');
@ -95,7 +94,7 @@ function char_count(int $user_id): int
/** /**
* Get a an array of id => [name, level] for all characters associated with an account ID. * Get a an array of id => [name, level] for all characters associated with an account ID.
*/ */
function char_list(int $user_id): array function char_list($user_id): array
{ {
$stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $user_id]); $stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $user_id]);
if ($stmt === false) throw new Exception('Failed to list characters. (cl)'); if ($stmt === false) throw new Exception('Failed to list characters. (cl)');
@ -111,7 +110,7 @@ function char_list(int $user_id): array
/** /**
* Get a character's location info by their character ID. Returns the location's data as an associative array. * Get a character's location info by their character ID. Returns the location's data as an associative array.
*/ */
function char_get_location(int $char_id): array function char_get_location($char_id): array
{ {
// Get the location // Get the location
$location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); $location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
@ -122,7 +121,7 @@ function char_get_location(int $char_id): array
/** /**
* See if a character name exists. * See if a character name exists.
*/ */
function char_name_exists(string $name): bool function char_name_exists($name)
{ {
return db_exists(db_live(), 'characters', 'name', $name); return db_exists(db_live(), 'characters', 'name', $name);
} }
@ -130,25 +129,31 @@ function char_name_exists(string $name): bool
/** /**
* Checks whether a character exists at a certain ID. * Checks whether a character exists at a certain ID.
*/ */
function char_exists(int $char_id): bool function char_exists($char_id)
{ {
return db_exists(db_live(), 'characters', 'id', $char_id); return db_exists(db_live(), 'characters', 'id', $char_id);
} }
/** /**
* See if the given character belongs to the given user. * See if the given character belongs to the given user. Returns false if the character does not belong to the user,
* or if the character does not exist. Returns true if the character belongs to the user. Generally this function
* shouldn't return false, as it should be called after the character's existence is confirmed. If it does return false,
* it is likely due to user interference.
*/ */
function char_belongs_to_user(int $char_id, int $user_id): bool function char_belongs_to_user($char_id, $user_id)
{ {
$char = db_query(db_live(), "SELECT user_id FROM characters WHERE id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); $char = db_query(
if ($char === false) throw new Exception('Character not found. (cbtu)'); db_live(),
return $char['user_id'] === $user_id; "SELECT 1 FROM characters WHERE id = :p AND user_id = :u",
[':p' => $char_id, ':u' => $user_id]
)->fetchArray(SQLITE3_ASSOC);
return $char !== false;
} }
/** /**
* Delete a character by their ID. This will delete all associated data tables as well. * Delete a character by their ID. This will delete all associated data tables as well.
*/ */
function char_delete(int $char_id): void function char_delete($char_id)
{ {
// Delete the character // Delete the character
if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $char_id]) === false) { if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $char_id]) === false) {

View File

@ -61,6 +61,6 @@ const item_qualities = [
/** /**
* Create an item * Create an item
*/ */
function create_item(string $name, array $type, array $opts) { function create_item($name, array $type, array $opts) {
} }

View File

@ -3,7 +3,7 @@
/** /**
* Create a session for a user with a token and expiration date. Returns the token on success, or false on failure. * Create a session for a user with a token and expiration date. Returns the token on success, or false on failure.
*/ */
function session_create(int $userId, int $expires): string|false function session_create($userId, $expires)
{ {
$token = token(); $token = token();
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [ $result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
@ -18,7 +18,7 @@ function session_create(int $userId, int $expires): string|false
/** /**
* Find a session by token. * Find a session by token.
*/ */
function session_find(string $token): array|false function session_find($token)
{ {
$result = db_query(db_auth(), "SELECT * FROM sessions WHERE token = :t", [':t' => $token]); $result = db_query(db_auth(), "SELECT * FROM sessions WHERE token = :t", [':t' => $token]);
$session = $result->fetchArray(SQLITE3_ASSOC); $session = $result->fetchArray(SQLITE3_ASSOC);
@ -30,7 +30,7 @@ function session_find(string $token): array|false
/** /**
* Delete sessions by user id. * Delete sessions by user id.
*/ */
function session_delete(int $userId): SQLite3Result|false function session_delete($userId)
{ {
return db_query(db_auth(), "DELETE FROM sessions WHERE user_id = :u", [':u' => $userId]); return db_query(db_auth(), "DELETE FROM sessions WHERE user_id = :u", [':u' => $userId]);
} }
@ -38,7 +38,7 @@ function session_delete(int $userId): SQLite3Result|false
/** /**
* Validate a session by token and expiration date. If expired, the session is deleted and false is returned. * Validate a session by token and expiration date. If expired, the session is deleted and false is returned.
*/ */
function session_validate(string $token): bool function session_validate($token)
{ {
$session = session_find($token); $session = session_find($token);
if (!$session) return false; if (!$session) return false;

View File

@ -3,7 +3,7 @@
/** /**
* Create a token for a user. Returns the token on success, or false on failure. * Create a token for a user. Returns the token on success, or false on failure.
*/ */
function token_create(int $userId): string|false function token_create($userId)
{ {
$token = token(); $token = token();
$result = db_query(db_auth(), "INSERT INTO tokens (token, user_id) VALUES (:t, :u)", [ $result = db_query(db_auth(), "INSERT INTO tokens (token, user_id) VALUES (:t, :u)", [
@ -17,7 +17,7 @@ function token_create(int $userId): string|false
/** /**
* Find a token by token. * Find a token by token.
*/ */
function token_find(string $token): array|false function token_find($token)
{ {
$result = db_query(db_auth(), "SELECT * FROM tokens WHERE token = :t", [':t' => $token]); $result = db_query(db_auth(), "SELECT * FROM tokens WHERE token = :t", [':t' => $token]);
$token = $result->fetchArray(SQLITE3_ASSOC); $token = $result->fetchArray(SQLITE3_ASSOC);
@ -29,7 +29,7 @@ function token_find(string $token): array|false
/** /**
* Delete a token by token. * Delete a token by token.
*/ */
function token_delete(string $token): SQLite3Result|false function token_delete($token)
{ {
return db_query(db_auth(), "DELETE FROM tokens WHERE token = :t", [':t' => $token]); return db_query(db_auth(), "DELETE FROM tokens WHERE token = :t", [':t' => $token]);
} }
@ -37,7 +37,7 @@ function token_delete(string $token): SQLite3Result|false
/** /**
* Validate a token by token and created date. Tokens are invalid if older than 7 days. * Validate a token by token and created date. Tokens are invalid if older than 7 days.
*/ */
function token_validate(string $token): bool function token_validate($token)
{ {
$token = token_find($token); $token = token_find($token);
if (!$token) return false; if (!$token) return false;

View File

@ -3,7 +3,7 @@
/** /**
* Find a user by username, email, or id. * Find a user by username, email, or id.
*/ */
function user_find(string|int $user): array|false function user_find($user)
{ {
$result = db_query(db_auth(), "SELECT * FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]); $result = db_query(db_auth(), "SELECT * FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
$user = $result->fetchArray(SQLITE3_ASSOC); $user = $result->fetchArray(SQLITE3_ASSOC);
@ -18,7 +18,7 @@ function user_find(string|int $user): array|false
* also up to the caller to validate password strength. This function will hash the password with the PASSWORD_ARGON2ID * also up to the caller to validate password strength. This function will hash the password with the PASSWORD_ARGON2ID
* algorithm. * algorithm.
*/ */
function user_create(string $username, string $email, string $password, int $auth = 0): SQLite3Result|false function user_create($username, $email, $password, $auth = 0)
{ {
return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [ return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [
':u' => $username, ':u' => $username,
@ -31,7 +31,7 @@ function user_create(string $username, string $email, string $password, int $aut
/** /**
* Delete a user by username, email, or id. * Delete a user by username, email, or id.
*/ */
function user_delete(string|int $user): SQLite3Result|false function user_delete($user)
{ {
return db_query(db_auth(), "DELETE FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]); return db_query(db_auth(), "DELETE FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]);
} }
@ -40,7 +40,7 @@ function user_delete(string|int $user): SQLite3Result|false
* Creates an account wallet. Can optionally specify the starting balances of the wallet. Returns the created wallet's * Creates an account wallet. Can optionally specify the starting balances of the wallet. Returns the created wallet's
* ID. If a currency is set to -1, the starting_silver or starting_star_gems fields from the env will be used. * ID. If a currency is set to -1, the starting_silver or starting_star_gems fields from the env will be used.
*/ */
function wallet_create(int $user_id, int $silver = -1, int $starGems = -1): void function wallet_create($user_id, $silver = -1, $starGems = -1)
{ {
if (db_query(db_live(), "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", [ if (db_query(db_live(), "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", [
':u' => $user_id, ':u' => $user_id,

37
src/render.php Normal file
View File

@ -0,0 +1,37 @@
<?php
/**
* Return the path to a view file.
*/
function template($name)
{
return __DIR__ . "/../templates/$name.php";
}
/**
* Render a view with the given data. Looks for `$view` through `template()`.
*/
function render($pathToBaseView, $data = [])
{
ob_start();
extract($data);
require template($pathToBaseView);
return ob_get_clean();
}
/**
* Set/retrieve the current page layout in/from GLOBALS.
*/
function page_layout($layout = '')
{
if ($layout !== '') $GLOBALS['page-layout'] = $layout;
return $GLOBALS['page-layout'] ?? 'basic';
}
/**
* Shorthand to render a page with the current layout.
*/
function page($view, array $data = [])
{
return render("layouts/" . page_layout(), ['view' => "pages/$view"] + $data);
}

View File

@ -7,7 +7,7 @@
* Example: * Example:
* `router_add($routes, 'GET', '/posts/:id', function($id) { echo "Viewing post $id"; });` * `router_add($routes, 'GET', '/posts/:id', function($id) { echo "Viewing post $id"; });`
*/ */
function router_add(array &$routes, string $method, string $route, callable $handler): void function router_add(array &$routes, $method, $route, callable $handler)
{ {
// Expand the route into segments and make dynamic segments into a common placeholder // Expand the route into segments and make dynamic segments into a common placeholder
$segments = array_map(function($segment) { $segments = array_map(function($segment) {
@ -33,7 +33,7 @@ function router_add(array &$routes, string $method, string $route, callable $han
* *
* @return array ['code', 'handler', 'params'] * @return array ['code', 'handler', 'params']
*/ */
function router_lookup(array $routes, string $method, string $uri): array function router_lookup(array $routes, $method, $uri): array
{ {
// node is a reference to our current location in the node tree // node is a reference to our current location in the node tree
$node = $routes; $node = $routes;
@ -76,7 +76,7 @@ function router_lookup(array $routes, string $method, string $uri): array
/** /**
* Register a GET route * Register a GET route
*/ */
function router_get(array &$routes, string $route, callable $handler): void function router_get(array &$routes, $route, callable $handler)
{ {
router_add($routes, 'GET', $route, $handler); router_add($routes, 'GET', $route, $handler);
} }
@ -84,7 +84,7 @@ function router_get(array &$routes, string $route, callable $handler): void
/** /**
* Register a POST route * Register a POST route
*/ */
function router_post(array &$routes, string $route, callable $handler): void function router_post(array &$routes, $route, callable $handler)
{ {
router_add($routes, 'POST', $route, $handler); router_add($routes, 'POST', $route, $handler);
} }
@ -92,7 +92,7 @@ function router_post(array &$routes, string $route, callable $handler): void
/** /**
* Register a PUT route * Register a PUT route
*/ */
function router_put(array &$routes, string $route, callable $handler): void function router_put(array &$routes, $route, callable $handler)
{ {
router_add($routes, 'PUT', $route, $handler); router_add($routes, 'PUT', $route, $handler);
} }
@ -100,7 +100,7 @@ function router_put(array &$routes, string $route, callable $handler): void
/** /**
* Register a DELETE route * Register a DELETE route
*/ */
function router_delete(array &$routes, string $route, callable $handler): void function router_delete(array &$routes, $route, callable $handler)
{ {
router_add($routes, 'DELETE', $route, $handler); router_add($routes, 'DELETE', $route, $handler);
} }
@ -108,7 +108,7 @@ function router_delete(array &$routes, string $route, callable $handler): void
/** /**
* Register a PATCH route * Register a PATCH route
*/ */
function router_patch(array &$routes, string $route, callable $handler): void function router_patch(array &$routes, $route, callable $handler)
{ {
router_add($routes, 'PATCH', $route, $handler); router_add($routes, 'PATCH', $route, $handler);
} }
@ -116,7 +116,7 @@ function router_patch(array &$routes, string $route, callable $handler): void
/** /**
* Handle a router error by setting the response code and echoing an error message * Handle a router error by setting the response code and echoing an error message
*/ */
function router_error(int $code): void function router_error($code)
{ {
http_response_code($code); http_response_code($code);
echo match ($code) { echo match ($code) {

View File

@ -3,5 +3,6 @@
<label for="char_<?= $id ?>"> <label for="char_<?= $id ?>">
<?= $char['name'] ?> <?= $char['name'] ?>
<span class="badge"><?= $char['level'] ?></span> <span class="badge"><?= $char['level'] ?></span>
<span class="selected">Currently Playing</span>
</label> </label>
</div> </div>

View File

@ -1,4 +1,4 @@
<div id="nav" class="box"> <div id="nav" class="box">
<a href="/" class="<?= $activeTab === 0 ? 'active' : '' ?>">Home</a> <a href="/" class="<?= $ant === 0 ? 'active' : '' ?>">Home</a>
<a href="/characters" class="<?= $activeTab === 1 ? 'active' : '' ?>">Characters</a> <a href="/characters" class="<?= $ant === 1 ? 'active' : '' ?>">Characters</a>
</div> </div>

View File

@ -1,29 +1,26 @@
<?php
$errors = flash('errors');
if ($errors !== false) {
foreach ($errors as $error) {
foreach ($error as $message) {
echo "<p>$message</p>";
}
}
}
?>
<div class="container-960"> <div class="container-960">
<h1 class="my-4">Login</h1> <h1 class="mb-2">Login</h1>
<p class="mb-4">
Welcome back, adventurer! Your journey continues here. Login to your account and pick up where you left off.
</p>
<?= c_form_errors() ?>
<form action="/auth/login" method="post"> <form action="/auth/login" method="post">
<?= csrf_field() ?> <?= csrf_field() ?>
<input class="form control mb-1" type="text" name="username" placeholder="Username"> <div class="form group">
<input class="form control mb-4" type="password" name="password" placeholder="Password"> <?= c_form_field('text', 'u', 'Username or Email') ?>
<?= c_form_field('password', 'p', 'Password') ?>
<div class="mb-4">
<input type="checkbox" name="remember" id="remember"> <label for="remember">remember me</label>
</div> </div>
<button class="ui button primary" type="submit">Login</button> <div class="mt-2 mb-4">
<a href="/auth/register" class="ui button secondary">Register</a> <input type="checkbox" name="remember" id="remember"> <label for="remember">Remember Me</label>
</div>
<button type="submit" class="ui button primary mb-4">Login</button>
<a href="/auth/register">New adventurer? Start here!</a>
</form> </form>
</div> </div>

View File

@ -1,25 +1,28 @@
<?php <div class="container-960" autocomplete="off">
$errors = flash('errors'); <h1 class="mb-2">Register</h1>
if ($errors !== false) {
foreach ($errors as $error) { <p class="mb-2">
foreach ($error as $message) { So you want to be an adventurer, eh? Fantastic choice! There is glory and treasure to be found at the end
echo "<p>$message</p>"; of this road; and danger too! If you're strong of will enough, you can become something more.
} </p>
}
} <p class="mb-4">
?> Start your journey here. Register an account and begin the never-ending quest for power and riches.
</p>
<?= c_form_errors() ?>
<div class="container-960">
<h1 class="my-4">Register</h1>
<form action="/auth/register" method="post"> <form action="/auth/register" method="post">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="text" name="username" placeholder="Username" class="form control mb-1"> <div class="form group">
<input type="text" name="email" placeholder="Email" class="form control mb-1"> <?= c_form_field('text', 'u', 'Username') ?>
<input type="password" name="password" placeholder="Password" class="form control mb-4"> <?= c_form_field('text', 'e', 'Email') ?>
<?= c_form_field('password', 'p', 'Password', autocomplete: "new-password") ?>
</div>
<button type="submit" class="ui button primary">Register</button> <button type="submit" class="ui button primary mr-4">Register</button>
<a href="/auth/login" class="ui button secondary">Login</a> <a href="/auth/login">Already an adventurer? Login!</a>
</form> </form>
</div> </div>

View File

@ -15,8 +15,8 @@
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="hidden" name="char_id" value="<?= $char['id'] ?>"> <input type="hidden" name="char_id" value="<?= $char['id'] ?>">
<label for="name">Type <b><?= $char['name'] ?></b> below.</label> <label for="n">Type <b><?= $char['name'] ?></b> below.</label>
<input id="name" class="form control mb-2" type="text" name="name" placeholder="Character Name"> <?= c_form_field('text', 'n', 'Character Name') ?>
<button class="ui button danger" type="submit">Delete</button> <button class="ui button danger" type="submit">Delete</button>
<a class="ui button" href="/characters">Back</a> <a class="ui button" href="/characters">Back</a>

View File

@ -1,14 +1,3 @@
<?php
$errors = flash('errors');
if ($errors !== false) {
foreach ($errors as $error) {
foreach ($error as $message) {
echo "<p>$message</p>";
}
}
}
?>
<div class="container-960"> <div class="container-960">
<h1 class="my-4">Create Your First Character</h1> <h1 class="my-4">Create Your First Character</h1>
@ -16,13 +5,18 @@
<p class="mb-4"> <p class="mb-4">
Before you can begin your adventure, you need to make your first character. Pick a name below. You Before you can begin your adventure, you need to make your first character. Pick a name below. You
can create multiple characters later, and there are no classes; feel free to experiment! can create multiple characters later, and there are no classes. Feel free to experiment!
</p> </p>
<?= c_form_errors('create-character') ?>
<form action="/character/create" method="post"> <form action="/character/create" method="post">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="hidden" name="first" value="true">
<input class="form control mb-2" type="text" name="name" placeholder="Character Name"> <div class="form group">
<?= c_form_field('text', 'n', 'Character Name', formId: 'create-character') ?>
</div>
<button class="ui button primary" type="submit">Create</button> <button class="ui button primary" type="submit">Create</button>
</form> </form>

View File

@ -1,20 +1,21 @@
<section> <section>
<?php
if (($f = flash('alert_character_list_1')) !== false) echo c_alert($f[0], $f[1]);
?>
<h1>Characters <span class="badge"><?= count($chars) . '/' . user('char_slots') ?></span></h1> <h1>Characters <span class="badge"><?= count($chars) . '/' . user('char_slots') ?></span></h1>
<?php <?php
if (count($chars) > 0): ?> if (($f = flash('alert_character_list_1')) !== false) echo c_alert($f[0], $f[1]);
if (count($chars) > 0):
?>
<form action="/characters" method="post"> <form action="/characters" method="post">
<input type="hidden" name="csrf" value="<?= csrf() ?>"> <input type="hidden" name="csrf" value="<?= csrf() ?>">
<div class="my-4 character-select"> <div class="my-2 character-select">
<?php foreach ($chars as $id => $char) echo c_char_select_box($id, $char); ?> <?php foreach ($chars as $id => $char) echo c_char_select_box($id, $char); ?>
</div>
<button type="submit" class="ui button primary" name="action" value="select">Select</button> <div class="mt-4 buttons">
<button type="submit" class="ui button danger" name="action" value="delete">Delete</button> <button type="submit" class="ui button primary" name="action" value="select">Select</button>
<button type="submit" class="ui button danger" name="action" value="delete">Delete</button>
</div>
</div>
</form> </form>
<?php else: ?> <?php else: ?>
<!-- Should never see this particular message. If you have, there's a bug. --> <!-- Should never see this particular message. If you have, there's a bug. -->
@ -22,13 +23,8 @@
<?php endif; ?> <?php endif; ?>
</section> </section>
<?php if (user('char_slots') > count($chars)): ?>
<section> <section>
<?php
if (($f = flash('alert_character_list_2')) !== false) echo c_alert($f[0], $f[1]);
if (($f = flash('errors_create_character')) !== false) echo c_alert('danger', array_to_ul($f));
?>
<?php if (user('char_slots') > count($chars)): ?>
<h2>Create a new character</h2> <h2>Create a new character</h2>
<?php $num_slots_left = user('char_slots') - count($chars); ?> <?php $num_slots_left = user('char_slots') - count($chars); ?>
<p>You have <b><?= $num_slots_left ?> <?= $num_slots_left === 1 ? 'slot' : 'slots' ?></b> left.</p> <p>You have <b><?= $num_slots_left ?> <?= $num_slots_left === 1 ? 'slot' : 'slots' ?></b> left.</p>
@ -36,10 +32,11 @@
<input type="hidden" name="csrf" value="<?= csrf() ?>"> <input type="hidden" name="csrf" value="<?= csrf() ?>">
<div class="my-4"> <div class="my-4">
<input class="form control" type="text" name="name" id="name" placeholder="Character name" required> <?= c_form_errors('create-character') ?>
<?= c_form_field('text', 'n', 'Character Name', formId: 'create-character') ?>
</div> </div>
<button type="submit" class="ui button secondary">Create</button> <button type="submit" class="ui button secondary">Create</button>
</form> </form>
<?php endif; ?>
</section> </section>
<?php endif; ?>