refactor database access
|
@ -126,3 +126,16 @@
|
||||||
.character-select:not(:has(input[type="radio"]:checked)) > .buttons {
|
.character-select:not(:has(input[type="radio"]:checked)) > .buttons {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form.logout-form {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
& > button {
|
||||||
|
display: inline-block;
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
@import 'src/buttons.css';
|
@import 'utilities.css';
|
||||||
|
@import 'buttons.css';
|
||||||
:root {
|
@import 'forms.css';
|
||||||
font-size: 16px;
|
|
||||||
--main-font: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #bcc6cf;
|
background-color: #bcc6cf;
|
||||||
|
@ -18,16 +9,13 @@ body {
|
||||||
background-position: center top;
|
background-position: center top;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
font-family: var(--main-font);
|
font-family: var(--main-font);
|
||||||
}
|
|
||||||
|
|
||||||
main#game-container {
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#game-ui, div#game-windows {
|
div#ui, div#windows {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -37,7 +25,18 @@ div#game-ui, div#game-windows {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#game-windows {
|
div#ui {
|
||||||
|
section#menu {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div#windows {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -99,7 +98,7 @@ div#game-windows {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas#game-canvas {
|
canvas#canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 6.6 KiB |
|
@ -4,6 +4,7 @@
|
||||||
Setup
|
Setup
|
||||||
*/
|
*/
|
||||||
define('SRC', __DIR__ . '/../src');
|
define('SRC', __DIR__ . '/../src');
|
||||||
|
define('DATABASE_PATH', __DIR__ . '/../database');
|
||||||
require_once SRC . '/bootstrap.php';
|
require_once SRC . '/bootstrap.php';
|
||||||
|
|
||||||
$r = new Router;
|
$r = new Router;
|
||||||
|
@ -12,8 +13,8 @@ $r = new Router;
|
||||||
Home
|
Home
|
||||||
*/
|
*/
|
||||||
$r->get('/', function() {
|
$r->get('/', function() {
|
||||||
if (!user()) redirect('/login');
|
if (user()) redirect('/world');
|
||||||
redirect('/world');
|
echo render('layouts/basic', ['view' => 'pages/hello']);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -78,7 +79,7 @@ $r->post('/register', function() {
|
||||||
// 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)) {
|
||||||
$GLOBALS['form-errors'] = $errors;
|
$GLOBALS['form-errors'] = $errors;
|
||||||
echo page('auth/register');
|
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,20 +125,14 @@ $r->post('/login', function() {
|
||||||
$_SESSION['user'] = serialize($user);
|
$_SESSION['user'] = serialize($user);
|
||||||
|
|
||||||
if ($_POST['remember'] ?? false) {
|
if ($_POST['remember'] ?? false) {
|
||||||
$token = token();
|
$session = Session::create($user->id, strtotime('+30 days'));
|
||||||
$expires = strtotime('+30 days');
|
if ($session === false) error_response(400);
|
||||||
$result = db_query(
|
set_cookie('remember_me', $session->token, $session->expires);
|
||||||
db_auth(),
|
|
||||||
"INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)",
|
|
||||||
[':t' => $token, ':u' => user()->id, ':e' => $expires]
|
|
||||||
);
|
|
||||||
if (!$result) error_response(400);
|
|
||||||
set_cookie('remember_me', $token, $expires);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user()->char_count() === 0) {
|
if ($user->char_count() === 0) {
|
||||||
redirect('/character/create-first');
|
redirect('/character/create-first');
|
||||||
} elseif (!change_user_character(user()->char_id)) {
|
} elseif (!change_user_character($user->char_id)) {
|
||||||
echo "failed to change user character (aclp)";
|
echo "failed to change user character (aclp)";
|
||||||
error_response(999);
|
error_response(999);
|
||||||
}
|
}
|
||||||
|
@ -147,14 +142,14 @@ $r->post('/login', function() {
|
||||||
|
|
||||||
$r->post('/logout', function() {
|
$r->post('/logout', function() {
|
||||||
csrf_ensure();
|
csrf_ensure();
|
||||||
session_delete(user()->id);
|
Session::delete(user()->id);
|
||||||
unset($_SESSION['user']);
|
unset($_SESSION['user']);
|
||||||
set_cookie('remember_me', '', 1);
|
set_cookie('remember_me', '', 1);
|
||||||
redirect('/');
|
redirect('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
$r->get('/debug/logout', function() {
|
$r->get('/debug/logout', function() {
|
||||||
session_delete(user()->id);
|
Session::delete(user()->id);
|
||||||
unset($_SESSION['user']);
|
unset($_SESSION['user']);
|
||||||
set_cookie('remember_me', '', 1);
|
set_cookie('remember_me', '', 1);
|
||||||
redirect('/');
|
redirect('/');
|
||||||
|
@ -167,7 +162,7 @@ $r->get('/characters', function() {
|
||||||
auth_only_and_must_have_character();
|
auth_only_and_must_have_character();
|
||||||
|
|
||||||
$GLOBALS['active_nav_tab'] = 'chars';
|
$GLOBALS['active_nav_tab'] = 'chars';
|
||||||
echo page('chars/list', ['chars' => user()->char_list()]);
|
//echo page('chars/list', ['chars' => user()->char_list()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
$r->post('/characters', function() {
|
$r->post('/characters', function() {
|
||||||
|
@ -210,7 +205,7 @@ $r->post('/characters', function() {
|
||||||
if ($action === 'delete') {
|
if ($action === 'delete') {
|
||||||
if (!Character::belongs_to($char_id, user()->id)) error_response(999);
|
if (!Character::belongs_to($char_id, user()->id)) error_response(999);
|
||||||
|
|
||||||
echo page('chars/delete', ['char' => Character::find($char_id)]);
|
//echo page('chars/delete', ['char' => Character::find($char_id)]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +220,7 @@ $r->get('/character/create-first', function() {
|
||||||
// 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 (user()->char_count() > 0) redirect('/');
|
if (user()->char_count() > 0) redirect('/');
|
||||||
|
|
||||||
echo page('chars/first');
|
//echo page('chars/first');
|
||||||
});
|
});
|
||||||
|
|
||||||
$r->post('/character/create', function() {
|
$r->post('/character/create', function() {
|
||||||
|
@ -257,11 +252,11 @@ $r->post('/character/create', function() {
|
||||||
|
|
||||||
if (isset($_POST['first']) && $_POST['first'] === 'true') {
|
if (isset($_POST['first']) && $_POST['first'] === 'true') {
|
||||||
// If this is the first character, return to the first character creation page.
|
// If this is the first character, return to the first character creation page.
|
||||||
echo page('chars/first');
|
//echo page('chars/first');
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
// If this is not the first character, return to the character list page.
|
// If this is not the first character, return to the character list page.
|
||||||
echo page('chars/list', ['chars' => user()->char_list()]);
|
//echo page('chars/list', ['chars' => user()->char_list()]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,7 +352,7 @@ $r->post('/move', function() {
|
||||||
error_response(999);
|
error_response(999);
|
||||||
}
|
}
|
||||||
|
|
||||||
$r = db_query(db_live(), 'UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [
|
$r = live_db()->query('UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [
|
||||||
':x' => $x,
|
':x' => $x,
|
||||||
':y' => $y,
|
':y' => $y,
|
||||||
':c' => user()->char_id
|
':c' => user()->char_id
|
||||||
|
@ -371,7 +366,7 @@ $r->post('/move', function() {
|
||||||
/*
|
/*
|
||||||
UI
|
UI
|
||||||
*/
|
*/
|
||||||
$r->post('/ui/stats', function() {
|
$r->get('/ui/stats', function() {
|
||||||
ui_guard();
|
ui_guard();
|
||||||
echo c_profile_stats(char());
|
echo c_profile_stats(char());
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,24 +5,19 @@ session_start();
|
||||||
// SRC is defined as the path to the src/ directory from public/
|
// SRC is defined as the path to the src/ directory from public/
|
||||||
|
|
||||||
define('CLASS_MAP', [
|
define('CLASS_MAP', [
|
||||||
|
'Database' => '/database.php',
|
||||||
|
'Router' => '/router.php',
|
||||||
'User' => '/models/user.php',
|
'User' => '/models/user.php',
|
||||||
'Character' => '/models/character.php',
|
'Character' => '/models/character.php',
|
||||||
'Wallet' => '/models/wallet.php'
|
'Wallet' => '/models/wallet.php',
|
||||||
|
'Session' => '/models/session.php',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
require_once SRC . '/helpers.php';
|
require_once SRC . '/helpers.php';
|
||||||
|
|
||||||
stopwatch_start('bootstrap'); // Start the bootstrap stopwatch
|
|
||||||
|
|
||||||
require_once SRC . '/util/env.php';
|
|
||||||
require_once SRC . '/util/database.php';
|
|
||||||
require_once SRC . '/util/auth.php';
|
require_once SRC . '/util/auth.php';
|
||||||
require_once SRC . '/util/router.php';
|
|
||||||
require_once SRC . '/util/components.php';
|
require_once SRC . '/util/components.php';
|
||||||
require_once SRC . '/util/render.php';
|
|
||||||
require_once SRC . '/util/enums.php';
|
require_once SRC . '/util/enums.php';
|
||||||
|
|
||||||
require_once SRC . '/models/session.php';
|
|
||||||
require_once SRC . '/models/token.php';
|
require_once SRC . '/models/token.php';
|
||||||
|
|
||||||
spl_autoload_register(function (string $class) {
|
spl_autoload_register(function (string $class) {
|
||||||
|
@ -43,14 +38,11 @@ if (env('debug') === 'true') {
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create an array in GLOBALS to hold database connections.
|
||||||
|
$GLOBALS['databases'] = [];
|
||||||
|
|
||||||
// Generate a new CSRF token. (if one doesn't exist, that is)
|
// Generate a new CSRF token. (if one doesn't exist, that is)
|
||||||
csrf();
|
csrf();
|
||||||
|
|
||||||
// Have global counters for queries
|
|
||||||
$GLOBALS['queries'] = 0;
|
|
||||||
$GLOBALS['query_time'] = 0;
|
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
||||||
stopwatch_stop('bootstrap'); // Stop the bootstrap stopwatch
|
|
||||||
|
|
81
src/database.php
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic wrapper around a SQLite3 object to add our own semantics to data operations.
|
||||||
|
*/
|
||||||
|
class Database
|
||||||
|
{
|
||||||
|
private SQLite3 $db;
|
||||||
|
public int $count = 0;
|
||||||
|
public array $log = [];
|
||||||
|
public float $query_time = 0;
|
||||||
|
|
||||||
|
public function __construct(string $db_path)
|
||||||
|
{
|
||||||
|
$db = new SQLite3($db_path);
|
||||||
|
|
||||||
|
// Increase cache size to 32MB
|
||||||
|
$db->exec('PRAGMA cache_size = 32000');
|
||||||
|
// Enable WAL mode
|
||||||
|
$db->exec('PRAGMA journal_mode = WAL');
|
||||||
|
// Move temp store to memory
|
||||||
|
$db->exec('PRAGMA temp_store = MEMORY');
|
||||||
|
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function query(string $query, array $params = []): SQLite3Result|false
|
||||||
|
{
|
||||||
|
$p = strpos($query, '?') !== false; // generic placeholders?
|
||||||
|
$stmt = $this->db->prepare($query);
|
||||||
|
if (!empty($params)) {
|
||||||
|
foreach ($params as $k => $v) $stmt->bindValue($p ? $k + 1 : $k, $v, $this->getSQLiteType($v));
|
||||||
|
}
|
||||||
|
$start = microtime(true);
|
||||||
|
$r = $stmt->execute();
|
||||||
|
$this->log($query, microtime(true) - $start);
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exec(string $query): bool
|
||||||
|
{
|
||||||
|
$start = microtime(true);
|
||||||
|
$r = $this->db->exec($query);
|
||||||
|
$this->log($query, microtime(true) - $start);
|
||||||
|
return $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exists(string $table, string $column, mixed $value, bool $case_insensitive = true): bool
|
||||||
|
{
|
||||||
|
if ($case_insensitive) {
|
||||||
|
$query = "SELECT 1 FROM $table WHERE $column = :v COLLATE NOCASE LIMIT 1";
|
||||||
|
} else {
|
||||||
|
$query = "SELECT 1 FROM $table WHERE $column = :v LIMIT 1";
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->query($query, [':v' => $value]);
|
||||||
|
return $result->fetchArray(SQLITE3_NUM) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function log(string $query, float $time_taken): void
|
||||||
|
{
|
||||||
|
$this->count++;
|
||||||
|
$this->query_time += $time_taken;
|
||||||
|
if (env('debug', false)) $this->log[] = [$query, $time_taken];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSQLiteType(mixed $value): int
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
is_int($value) => SQLITE3_INTEGER,
|
||||||
|
is_float($value) => SQLITE3_FLOAT,
|
||||||
|
is_null($value) => SQLITE3_NULL,
|
||||||
|
default => SQLITE3_TEXT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastInsertRowID(): int
|
||||||
|
{
|
||||||
|
return $this->db->lastInsertRowID();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,86 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the environment variables from the .env file.
|
||||||
|
*/
|
||||||
|
function env_load($filePath)
|
||||||
|
{
|
||||||
|
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
|
||||||
|
|
||||||
|
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
|
||||||
|
// Skip lines that are empty after trimming or are comments
|
||||||
|
if ($line === '' || str_starts_with($line, '#')) continue;
|
||||||
|
|
||||||
|
// Skip lines without an '=' character
|
||||||
|
if (strpos($line, '=') === false) continue;
|
||||||
|
|
||||||
|
[$name, $value] = explode('=', $line, 2);
|
||||||
|
|
||||||
|
$name = trim($name);
|
||||||
|
$value = trim($value, " \t\n\r\0\x0B\"'"); // Trim whitespace and quotes
|
||||||
|
|
||||||
|
if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
|
||||||
|
putenv("$name=$value");
|
||||||
|
$_ENV[$name] = $value;
|
||||||
|
$_SERVER[$name] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an environment variable.
|
||||||
|
*/
|
||||||
|
function env($key, $default = null)
|
||||||
|
{
|
||||||
|
$v = $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
|
||||||
|
return match(true) {
|
||||||
|
$v === 'true' => true,
|
||||||
|
$v === 'false' => false,
|
||||||
|
is_numeric($v) => (int) $v,
|
||||||
|
is_float($v) => (float) $v,
|
||||||
|
default => $v
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path to a view file.
|
||||||
|
*/
|
||||||
|
function template(string $name): string
|
||||||
|
{
|
||||||
|
return SRC . "/../templates/$name.php";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a view with the given data. Looks for `$view` through `template()`.
|
||||||
|
*/
|
||||||
|
function render(string $path_to_base_view, array $data = []): string|false
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
extract($data);
|
||||||
|
require template($path_to_base_view);
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the auth database connection from GLOBALS['databases'], or create it if it doesn't exist.
|
||||||
|
*/
|
||||||
|
function auth_db(): Database
|
||||||
|
{
|
||||||
|
return $GLOBALS['databases']['auth'] ??= new Database(DATABASE_PATH . '/auth.db');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the live database connection from GLOBALS['databases'], or create it if it doesn't exist.
|
||||||
|
*/
|
||||||
|
function live_db(): Database
|
||||||
|
{
|
||||||
|
return $GLOBALS['databases']['live'] ??= new Database(DATABASE_PATH . '/live.db');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a pretty dope token.
|
* Generate a pretty dope token.
|
||||||
*/
|
*/
|
||||||
|
@ -75,7 +156,8 @@ function csrf_field()
|
||||||
*/
|
*/
|
||||||
function csrf_ensure()
|
function csrf_ensure()
|
||||||
{
|
{
|
||||||
if (!csrf_verify($_POST['csrf'] ?? '')) error_response(418);
|
$csrf = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF'] ?? '';
|
||||||
|
if (!csrf_verify($csrf)) error_response(418);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,7 +220,7 @@ function change_user_character(int $char_id): bool
|
||||||
// If the character ID is different, update the session and database
|
// If the character ID is different, update the session and database
|
||||||
if (user()->char_id !== $char_id) {
|
if (user()->char_id !== $char_id) {
|
||||||
modify_user_session('char_id', $char_id);
|
modify_user_session('char_id', $char_id);
|
||||||
db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user()->id]);
|
auth_db()->query("UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user()->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -175,8 +257,7 @@ function wallet(): Wallet|false
|
||||||
function location($field = '')
|
function location($field = '')
|
||||||
{
|
{
|
||||||
if (empty($GLOBALS['location'])) {
|
if (empty($GLOBALS['location'])) {
|
||||||
$GLOBALS['location'] = db_query(
|
$GLOBALS['location'] = live_db()->query(
|
||||||
db_live(),
|
|
||||||
"SELECT * FROM char_locations WHERE char_id = :c",
|
"SELECT * FROM char_locations WHERE char_id = :c",
|
||||||
[':c' => user()->char_id]
|
[':c' => user()->char_id]
|
||||||
)->fetchArray(SQLITE3_ASSOC);
|
)->fetchArray(SQLITE3_ASSOC);
|
||||||
|
@ -289,7 +370,7 @@ function error_response(int $code): void
|
||||||
*/
|
*/
|
||||||
function title(int $title_id): array|false
|
function title(int $title_id): array|false
|
||||||
{
|
{
|
||||||
return db_query(db_live(), 'SELECT * FROM titles WHERE id = ?', [$title_id])->fetchArray();
|
return live_db()->query('SELECT * FROM titles WHERE id = ?', [$title_id])->fetchArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -364,3 +445,11 @@ function ui_guard()
|
||||||
ajax_only();
|
ajax_only();
|
||||||
csrf_ensure();
|
csrf_ensure();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand to call fetchArray() on a SQLite3Result. Defaults to SQLITE3_ASSOC but can pass any constant to $mode.
|
||||||
|
*/
|
||||||
|
function db_fetch_array(SQLite3Result $result, int $mode = SQLITE3_ASSOC): array|false
|
||||||
|
{
|
||||||
|
return $result->fetchArray($mode);
|
||||||
|
}
|
||||||
|
|
|
@ -70,8 +70,7 @@ class Character
|
||||||
*/
|
*/
|
||||||
public static function find(int|string $id): Character|false
|
public static function find(int|string $id): Character|false
|
||||||
{
|
{
|
||||||
$q = db_query(
|
$q = live_db()->query(
|
||||||
db_live(),
|
|
||||||
"SELECT * FROM characters WHERE id = :id OR name = :id COLLATE NOCASE",
|
"SELECT * FROM characters WHERE id = :id OR name = :id COLLATE NOCASE",
|
||||||
[':id' => $id]
|
[':id' => $id]
|
||||||
);
|
);
|
||||||
|
@ -98,13 +97,13 @@ class Character
|
||||||
$v = implode(', ', array_map(fn($x) => ":$x", $k));
|
$v = implode(', ', array_map(fn($x) => ":$x", $k));
|
||||||
|
|
||||||
// Create the character!
|
// Create the character!
|
||||||
if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
|
if (live_db()->query("INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
|
||||||
// @TODO: Log this error
|
// @TODO: Log this error
|
||||||
throw new Exception('Failed to create character. (cc)');
|
throw new Exception('Failed to create character. (cc)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the character ID
|
// Get the character ID
|
||||||
return Character::find(db_live()->lastInsertRowID());
|
return Character::find(live_db()->lastInsertRowID());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,8 +111,7 @@ class Character
|
||||||
*/
|
*/
|
||||||
public function create_location(int $x = 0, int $y = 0, int $currently = 0): bool
|
public function create_location(int $x = 0, int $y = 0, int $currently = 0): bool
|
||||||
{
|
{
|
||||||
$l = db_query(
|
$l = live_db()->query(
|
||||||
db_live(),
|
|
||||||
"INSERT INTO char_locations (char_id, x, y, currently) VALUES (:i, :x, :y, :c)",
|
"INSERT INTO char_locations (char_id, x, y, currently) VALUES (:i, :x, :y, :c)",
|
||||||
[':i' => $this->id, ':x' => $x, ':y' => $y, ':c' => $currently]
|
[':i' => $this->id, ':x' => $x, ':y' => $y, ':c' => $currently]
|
||||||
);
|
);
|
||||||
|
@ -126,7 +124,7 @@ class Character
|
||||||
public function create_gear(array $initialGear = []): bool
|
public function create_gear(array $initialGear = []): bool
|
||||||
{
|
{
|
||||||
// @TODO implement initial gear
|
// @TODO implement initial gear
|
||||||
$g = db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:i)", [':i' => $this->id]);
|
$g = live_db()->query("INSERT INTO char_gear (char_id) VALUES (:i)", [':i' => $this->id]);
|
||||||
return $g !== false;
|
return $g !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +133,7 @@ class Character
|
||||||
*/
|
*/
|
||||||
public static function name_exists(string $name): bool
|
public static function name_exists(string $name): bool
|
||||||
{
|
{
|
||||||
return db_exists(db_live(), 'characters', 'name', $name);
|
return live_db()->exists('characters', 'name', $name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,7 +141,7 @@ class Character
|
||||||
*/
|
*/
|
||||||
public static function exists(int $id): bool
|
public static function exists(int $id): bool
|
||||||
{
|
{
|
||||||
return db_exists(db_live(), 'characters', 'id', $id);
|
return live_db()->exists('characters', 'id', $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -151,8 +149,7 @@ class Character
|
||||||
*/
|
*/
|
||||||
public static function belongs_to(int $id, int $user_id): bool
|
public static function belongs_to(int $id, int $user_id): bool
|
||||||
{
|
{
|
||||||
$q = db_query(
|
$q = live_db()->query(
|
||||||
db_live(),
|
|
||||||
"SELECT 1 FROM characters WHERE id = :i AND user_id = :u LIMIT 1",
|
"SELECT 1 FROM characters WHERE id = :i AND user_id = :u LIMIT 1",
|
||||||
[':i' => $id, ':u' => $user_id]
|
[':i' => $id, ':u' => $user_id]
|
||||||
);
|
);
|
||||||
|
@ -175,8 +172,7 @@ class Character
|
||||||
*/
|
*/
|
||||||
public function award_title(int $title_id): bool
|
public function award_title(int $title_id): bool
|
||||||
{
|
{
|
||||||
$r = db_query(
|
$r = live_db()->query(
|
||||||
db_live(),
|
|
||||||
'INSERT INTO owned_titles (`title_id`, `char_id`) VALUES (:t, :i)',
|
'INSERT INTO owned_titles (`title_id`, `char_id`) VALUES (:t, :i)',
|
||||||
[':t' => $title_id, ':i' => $this->id]
|
[':t' => $title_id, ':i' => $this->id]
|
||||||
);
|
);
|
||||||
|
@ -189,84 +185,9 @@ class Character
|
||||||
public static function delete(int $id)
|
public static function delete(int $id)
|
||||||
{
|
{
|
||||||
// Delete the character
|
// Delete the character
|
||||||
if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $id]) === false) {
|
if (live_db()->query("DELETE FROM characters WHERE id = :p", [':p' => $id]) === false) {
|
||||||
throw new Exception('Failed to delete character. (C::d)');
|
throw new Exception('Failed to delete character. (C::d)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get item IDs from the character's inventory
|
|
||||||
$items = db_query(db_live(), "SELECT item_id FROM char_inventory WHERE char_id = :p", [':p' => $id]);
|
|
||||||
// delete the character's inventory and items
|
|
||||||
while ($row = $items->fetchArray(SQLITE3_ASSOC)) {
|
|
||||||
if (db_query(db_live(), "DELETE FROM char_inventory WHERE char_id = :c", [':c' => $id]) === false) {
|
|
||||||
throw new Exception('Failed to delete character inventory. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db_query(db_live(), "DELETE FROM items WHERE id = :p", [':p' => $row['id']]) === false) {
|
|
||||||
throw new Exception('Failed to delete character item slots. (C::d)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the character's location
|
|
||||||
if (db_query(db_live(), "DELETE FROM char_locations WHERE char_id = :p", [':p' => $id]) === false) {
|
|
||||||
throw new Exception('Failed to delete character location. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the character's gear
|
|
||||||
if (db_query(db_live(), "DELETE FROM char_gear WHERE char_id = :p", [':p' => $id]) === false) {
|
|
||||||
throw new Exception('Failed to delete character gear. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the character's bank
|
|
||||||
if (db_query(db_live(), "DELETE FROM char_bank WHERE char_id = :p", [':p' => $id]) === false) {
|
|
||||||
throw new Exception('Failed to delete character bank. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete character's banked items
|
|
||||||
if (db_query(db_live(), "DELETE FROM char_banked_items WHERE char_id = :p", [':p' => $id]) === false) {
|
|
||||||
throw new Exception('Failed to delete character bank items. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the user's guild membership
|
|
||||||
if (db_query(db_live(), "DELETE FROM guild_members WHERE char_id = :p", [':p' => $id]) === false) {
|
|
||||||
throw new Exception('Failed to delete character guild membership. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the character was a guild leader, hand leadership to the next highest ranking member
|
|
||||||
$guild = db_query(db_live(), "SELECT id FROM guilds WHERE leader_id = :p", [':p' => $id])->fetchArray(SQLITE3_ASSOC);
|
|
||||||
if ($guild !== false) {
|
|
||||||
$members = db_query(db_live(), "SELECT char_id FROM guild_members WHERE guild_id = :p ORDER BY rank DESC", [':p' => $guild['id']]);
|
|
||||||
$newLeader = $members->fetchArray(SQLITE3_ASSOC);
|
|
||||||
if ($newLeader !== false) {
|
|
||||||
db_query(db_live(), "UPDATE guilds SET leader_id = :p WHERE id = :g", [':p' => $newLeader['char_id'], ':g' => $guild['id']]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a list of all pve fight IDs.
|
|
||||||
$pve = db_query(db_fights(), "SELECT id FROM pve WHERE char_id = :p", [':p' => $id]);
|
|
||||||
// Get a list of all pvp fight IDs.
|
|
||||||
$pvp = db_query(db_fights(), "SELECT id FROM pvp WHERE char1_id = :p OR char2_id = :p", [':p' => $id]);
|
|
||||||
|
|
||||||
// Delete all pve fights
|
|
||||||
while ($row = $pve->fetchArray(SQLITE3_ASSOC)) {
|
|
||||||
if (db_query(db_fights(), "DELETE FROM pve WHERE id = :p", [':p' => $row['id']]) === false) {
|
|
||||||
throw new Exception('Failed to delete pve fight. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db_query(db_fights(), "DELETE FROM pve_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
|
|
||||||
throw new Exception('Failed to delete pve fight logs. (C::d)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all pvp fights
|
|
||||||
while ($row = $pvp->fetchArray(SQLITE3_ASSOC)) {
|
|
||||||
if (db_query(db_fights(), "DELETE FROM pvp WHERE id = :p", [':p' => $row['id']]) === false) {
|
|
||||||
throw new Exception('Failed to delete pvp fight. (C::d)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db_query(db_fights(), "DELETE FROM pvp_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) {
|
|
||||||
throw new Exception('Failed to delete pvp fight logs. (C::d)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -278,8 +199,7 @@ class Character
|
||||||
|
|
||||||
$t = title($this->title_id);
|
$t = title($this->title_id);
|
||||||
|
|
||||||
$q = db_query(
|
$q = live_db()->query(
|
||||||
db_live(),
|
|
||||||
'SELECT awarded FROM owned_titles WHERE char_id = ? AND title_id = ? LIMIT 1',
|
'SELECT awarded FROM owned_titles WHERE char_id = ? AND title_id = ? LIMIT 1',
|
||||||
[$this->id, $this->title_id]
|
[$this->id, $this->title_id]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,50 +1,46 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
class Session
|
||||||
* Create a session for a user with a token and expiration date. Returns the token on success, or false on failure.
|
|
||||||
*/
|
|
||||||
function session_create($userId, $expires)
|
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $user_id,
|
||||||
|
public string $token,
|
||||||
|
public int $expires
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function create(int $user_id, int $expires): Session|false
|
||||||
|
{
|
||||||
$token = token();
|
$token = token();
|
||||||
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
|
$result = auth_db()->query("INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
|
||||||
':t' => $token,
|
':t' => $token,
|
||||||
':u' => $userId,
|
':u' => $user_id,
|
||||||
':e' => $expires
|
':e' => $expires
|
||||||
]);
|
]);
|
||||||
if (!$result) return false;
|
if ($result === false) return false;
|
||||||
return $token;
|
return new Session($user_id, $token, $expires);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function find(string $token): Session|false
|
||||||
* Find a session by token.
|
{
|
||||||
*/
|
$result = auth_db()->query("SELECT * FROM sessions WHERE token = :t", [':t' => $token]);
|
||||||
function session_find($token)
|
$session = db_fetch_array($result);
|
||||||
{
|
if ($session === false) return false;
|
||||||
$result = db_query(db_auth(), "SELECT * FROM sessions WHERE token = :t", [':t' => $token]);
|
|
||||||
$session = $result->fetchArray(SQLITE3_ASSOC);
|
|
||||||
if (!$session) return false;
|
|
||||||
$result->finalize();
|
$result->finalize();
|
||||||
return $session;
|
return new Session($session['user_id'], $session['token'], $session['expires']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function delete(int $user_id): SQLite3Result|false
|
||||||
* Delete sessions by user id.
|
{
|
||||||
*/
|
return auth_db()->query("DELETE FROM sessions WHERE user_id = :u", [':u' => $user_id]);
|
||||||
function session_delete($userId)
|
}
|
||||||
{
|
|
||||||
return db_query(db_auth(), "DELETE FROM sessions WHERE user_id = :u", [':u' => $userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
public function validate(): bool
|
||||||
* Validate a session by token and expiration date. If expired, the session is deleted and false is returned.
|
{
|
||||||
*/
|
if (empty($this->user_id) || empty($this->token)) return false;
|
||||||
function session_validate($token)
|
if ($this->expires < time()) {
|
||||||
{
|
self::delete($this->user_id);
|
||||||
$session = session_find($token);
|
|
||||||
if (!$session) return false;
|
|
||||||
if ($session['expires'] < time()) {
|
|
||||||
session_delete($session['user_id']);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
function token_create($userId)
|
function token_create($userId)
|
||||||
{
|
{
|
||||||
$token = token();
|
$token = token();
|
||||||
$result = db_query(db_auth(), "INSERT INTO tokens (token, user_id) VALUES (:t, :u)", [
|
$result = auth_db()->query("INSERT INTO tokens (token, user_id) VALUES (:t, :u)", [
|
||||||
':t' => $token,
|
':t' => $token,
|
||||||
':u' => $userId
|
':u' => $userId
|
||||||
]);
|
]);
|
||||||
|
@ -19,7 +19,7 @@ function token_create($userId)
|
||||||
*/
|
*/
|
||||||
function token_find($token)
|
function token_find($token)
|
||||||
{
|
{
|
||||||
$result = db_query(db_auth(), "SELECT * FROM tokens WHERE token = :t", [':t' => $token]);
|
$result = auth_db()->query("SELECT * FROM tokens WHERE token = :t", [':t' => $token]);
|
||||||
$token = $result->fetchArray(SQLITE3_ASSOC);
|
$token = $result->fetchArray(SQLITE3_ASSOC);
|
||||||
if (!$token) return false;
|
if (!$token) return false;
|
||||||
$result->finalize();
|
$result->finalize();
|
||||||
|
@ -31,7 +31,7 @@ function token_find($token)
|
||||||
*/
|
*/
|
||||||
function token_delete($token)
|
function token_delete($token)
|
||||||
{
|
{
|
||||||
return db_query(db_auth(), "DELETE FROM tokens WHERE token = :t", [':t' => $token]);
|
return auth_db()->query("DELETE FROM tokens WHERE token = :t", [':t' => $token]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -68,8 +68,7 @@ class User
|
||||||
*/
|
*/
|
||||||
public static function find(string|int $identifier): User|false
|
public static function find(string|int $identifier): User|false
|
||||||
{
|
{
|
||||||
$r = db_query(
|
$r = auth_db()->query(
|
||||||
db_auth(),
|
|
||||||
"SELECT * FROM users WHERE username = :i COLLATE NOCASE OR email = :i COLLATE NOCASE OR id = :i LIMIT 1",
|
"SELECT * FROM users WHERE username = :i COLLATE NOCASE OR email = :i COLLATE NOCASE OR id = :i LIMIT 1",
|
||||||
[':i' => $identifier]
|
[':i' => $identifier]
|
||||||
);
|
);
|
||||||
|
@ -86,7 +85,7 @@ class User
|
||||||
*/
|
*/
|
||||||
public static function create(string $username, string $email, string $password, int $auth = 0): SQLite3Result|false
|
public static function create(string $username, string $email, string $password, int $auth = 0): SQLite3Result|false
|
||||||
{
|
{
|
||||||
return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [
|
return auth_db()->query("INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [
|
||||||
':u' => $username,
|
':u' => $username,
|
||||||
':e' => $email,
|
':e' => $email,
|
||||||
':p' => password_hash($password, PASSWORD_ARGON2ID),
|
':p' => password_hash($password, PASSWORD_ARGON2ID),
|
||||||
|
@ -107,7 +106,7 @@ class User
|
||||||
*/
|
*/
|
||||||
public static function username_exists(string $username): bool
|
public static function username_exists(string $username): bool
|
||||||
{
|
{
|
||||||
return db_exists(db_auth(), 'users', 'username', $username);
|
return auth_db()->exists('users', 'username', $username);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,7 +114,7 @@ class User
|
||||||
*/
|
*/
|
||||||
public static function email_exists(string $email): bool
|
public static function email_exists(string $email): bool
|
||||||
{
|
{
|
||||||
return db_exists(db_auth(), 'users', 'email', $email);
|
return auth_db()->exists('users', 'email', $email);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,8 +122,7 @@ class User
|
||||||
*/
|
*/
|
||||||
public static function delete(string|int $identifier): SQLite3Result|false
|
public static function delete(string|int $identifier): SQLite3Result|false
|
||||||
{
|
{
|
||||||
return db_query(
|
return auth_db()->query(
|
||||||
db_auth(),
|
|
||||||
"DELETE FROM users WHERE username = :i OR email = :i OR id = :i",
|
"DELETE FROM users WHERE username = :i OR email = :i OR id = :i",
|
||||||
[':i' => $identifier]
|
[':i' => $identifier]
|
||||||
);
|
);
|
||||||
|
@ -135,8 +133,7 @@ class User
|
||||||
*/
|
*/
|
||||||
public function char_count(): int
|
public function char_count(): int
|
||||||
{
|
{
|
||||||
$c = db_query(
|
$c = live_db()->query(
|
||||||
db_live(),
|
|
||||||
"SELECT COUNT(*) FROM characters WHERE user_id = :u",
|
"SELECT COUNT(*) FROM characters WHERE user_id = :u",
|
||||||
[':u' => $this->id]
|
[':u' => $this->id]
|
||||||
)->fetchArray(SQLITE3_NUM);
|
)->fetchArray(SQLITE3_NUM);
|
||||||
|
@ -150,7 +147,7 @@ class User
|
||||||
*/
|
*/
|
||||||
public function char_list(): array|false
|
public function char_list(): array|false
|
||||||
{
|
{
|
||||||
$q = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = ?", [$this->id]);
|
$q = live_db()->query("SELECT id, name, level FROM characters WHERE user_id = ?", [$this->id]);
|
||||||
if ($q === false) throw new Exception('Failed to list characters. (U->cl)');
|
if ($q === false) throw new Exception('Failed to list characters. (U->cl)');
|
||||||
|
|
||||||
$c = [];
|
$c = [];
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Wallet
|
||||||
|
|
||||||
public static function find(int $user_id): Wallet|false
|
public static function find(int $user_id): Wallet|false
|
||||||
{
|
{
|
||||||
$r = db_query(db_live(), 'SELECT * FROM wallets WHERE user_id = ?', [$user_id]);
|
$r = live_db()->query('SELECT * FROM wallets WHERE user_id = ?', [$user_id]);
|
||||||
if ($r === false) throw new Exception('Failed to query wallet. (W::f)'); // badly formed query
|
if ($r === false) throw new Exception('Failed to query wallet. (W::f)'); // badly formed query
|
||||||
$w = $r->fetchArray(SQLITE3_ASSOC);
|
$w = $r->fetchArray(SQLITE3_ASSOC);
|
||||||
if ($w === false) return false; // no wallet found
|
if ($w === false) return false; // no wallet found
|
||||||
|
@ -19,8 +19,7 @@ class Wallet
|
||||||
|
|
||||||
public static function create(int $user_id, int $silver = -1, int $starGems = -1): SQLite3Result|false
|
public static function create(int $user_id, int $silver = -1, int $starGems = -1): SQLite3Result|false
|
||||||
{
|
{
|
||||||
return db_query(
|
return live_db()->query(
|
||||||
db_live(),
|
|
||||||
"INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)",
|
"INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)",
|
||||||
[
|
[
|
||||||
':u' => $user_id,
|
':u' => $user_id,
|
||||||
|
@ -37,7 +36,7 @@ class Wallet
|
||||||
{
|
{
|
||||||
$cs = $c->string(true);
|
$cs = $c->string(true);
|
||||||
$new = $this->{$cs} + $amt;
|
$new = $this->{$cs} + $amt;
|
||||||
return db_query(db_live(), "UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new, $this->user_id]);
|
return live_db()->query("UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new, $this->user_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +46,6 @@ class Wallet
|
||||||
{
|
{
|
||||||
$cs = $c->string(true);
|
$cs = $c->string(true);
|
||||||
$new = $this->{$cs} - $amt;
|
$new = $this->{$cs} - $amt;
|
||||||
return db_query(db_live(), "UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new < 0 ? 0 : $new, $this->user_id]);
|
return live_db()->query("UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new < 0 ? 0 : $new, $this->user_id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,14 @@
|
||||||
*/
|
*/
|
||||||
class Router
|
class Router
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* List of valid HTTP verbs.
|
||||||
|
*/
|
||||||
|
private const VALID_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tree of currently registered routes.
|
||||||
|
*/
|
||||||
private array $routes = [];
|
private array $routes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,6 +25,9 @@ class Router
|
||||||
*/
|
*/
|
||||||
public function add(string $method, string $route, callable $handler): Router
|
public function add(string $method, string $route, callable $handler): Router
|
||||||
{
|
{
|
||||||
|
$this->validateMethod($method);
|
||||||
|
$this->validateRoute($route);
|
||||||
|
|
||||||
// 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) {
|
||||||
return str_starts_with($segment, ':') ? ':x' : $segment;
|
return str_starts_with($segment, ':') ? ':x' : $segment;
|
||||||
|
@ -98,4 +109,58 @@ class Router
|
||||||
{
|
{
|
||||||
return $this->add('POST', $route, $handler);
|
return $this->add('POST', $route, $handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand to register a PUT route.
|
||||||
|
*/
|
||||||
|
public function put(string $route, callable $handler): Router
|
||||||
|
{
|
||||||
|
return $this->add('PUT', $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand to register a DELETE route.
|
||||||
|
*/
|
||||||
|
public function delete(string $route, callable $handler): Router
|
||||||
|
{
|
||||||
|
return $this->add('DELETE', $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand to register a PATCH route.
|
||||||
|
*/
|
||||||
|
public function patch(string $route, callable $handler): Router
|
||||||
|
{
|
||||||
|
return $this->add('PATCH', $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given method against valid HTTP verbs.
|
||||||
|
*/
|
||||||
|
private function validateMethod(string $method): void
|
||||||
|
{
|
||||||
|
if (!in_array($method, self::VALID_METHODS)) {
|
||||||
|
throw new InvalidArgumentException("Invalid HTTP method: $method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a new route follows expected formatting.
|
||||||
|
*/
|
||||||
|
private function validateRoute(string $route): void
|
||||||
|
{
|
||||||
|
if ($route === '') {
|
||||||
|
throw new InvalidArgumentException("Route cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure route starts with a slash
|
||||||
|
if (!str_starts_with($route, '/')) {
|
||||||
|
throw new InvalidArgumentException("Route must start with a '/'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Check for consecutive dynamic segments or invalid characters
|
||||||
|
if (preg_match('/(:x.*){2,}/', $route)) {
|
||||||
|
throw new InvalidArgumentException("Invalid route pattern: consecutive dynamic segments");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -9,9 +9,9 @@ function auth_check()
|
||||||
if (isset($_SESSION['user'])) return true;
|
if (isset($_SESSION['user'])) return true;
|
||||||
|
|
||||||
if (isset($_COOKIE['remember_me'])) {
|
if (isset($_COOKIE['remember_me'])) {
|
||||||
$session = session_validate($_COOKIE['remember_me']);
|
$session = Session::find($_COOKIE['remember_me']);
|
||||||
if ($session === true) {
|
if ($session->validate()) {
|
||||||
$user = User::find($session['user_id']);
|
$user = User::find($session->user_id);
|
||||||
$_SESSION['user'] = serialize($user);
|
$_SESSION['user'] = serialize($user);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -54,8 +54,7 @@ function must_have_character()
|
||||||
|
|
||||||
// if no character selected, select the first one
|
// if no character selected, select the first one
|
||||||
if (user()->char_id === 0) {
|
if (user()->char_id === 0) {
|
||||||
$char = db_query(
|
$char = live_db()->query(
|
||||||
db_live(),
|
|
||||||
'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1',
|
'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1',
|
||||||
[':u' => user()->id]
|
[':u' => user()->id]
|
||||||
)->fetchArray(SQLITE3_ASSOC);
|
)->fetchArray(SQLITE3_ASSOC);
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
define('DBP', SRC . '/../database');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a connection to a database.
|
|
||||||
*/
|
|
||||||
function db_open($path)
|
|
||||||
{
|
|
||||||
$db = new SQLite3($path);
|
|
||||||
|
|
||||||
// Increase cache size to 32MB
|
|
||||||
$db->exec('PRAGMA cache_size = 32000');
|
|
||||||
// Enable WAL mode
|
|
||||||
$db->exec('PRAGMA journal_mode = WAL');
|
|
||||||
// Move temp store to memory
|
|
||||||
$db->exec('PRAGMA temp_store = MEMORY');
|
|
||||||
|
|
||||||
return $db;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a connection to the auth database.
|
|
||||||
*/
|
|
||||||
function db_auth()
|
|
||||||
{
|
|
||||||
return $GLOBALS['db_auth'] ??= db_open(DBP . '/auth.db');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a connection to the live database.
|
|
||||||
*/
|
|
||||||
function db_live()
|
|
||||||
{
|
|
||||||
return $GLOBALS['db_live'] ??= db_open(DBP . '/live.db');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a connection to the fights database.
|
|
||||||
*/
|
|
||||||
function db_fights()
|
|
||||||
{
|
|
||||||
return $GLOBALS['db_fights'] ??= db_open(DBP . '/fights.db');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a connection to the blueprints database.
|
|
||||||
*/
|
|
||||||
function db_blueprints()
|
|
||||||
{
|
|
||||||
return $GLOBALS['db_blueprints'] ??= db_open(DBP . '/blueprints.db');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result|false
|
|
||||||
{
|
|
||||||
$p = strpos($query, '?') !== false; // are generic placeholders?
|
|
||||||
$stmt = $db->prepare($query);
|
|
||||||
if (!empty($params)) {
|
|
||||||
foreach ($params as $k => $v) $stmt->bindValue($p ? $k + 1 : $k, $v, getSQLiteType($v));
|
|
||||||
}
|
|
||||||
$start = microtime(true);
|
|
||||||
$r = $stmt->execute();
|
|
||||||
db_log($query, microtime(true) - $start);
|
|
||||||
return $r;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a SQLite3 database connection and a query string. Execute the query and return the result.
|
|
||||||
*/
|
|
||||||
function db_exec($db, $query)
|
|
||||||
{
|
|
||||||
$start = microtime(true);
|
|
||||||
$r = $db->exec($query);
|
|
||||||
db_log($query, microtime(true) - $start);
|
|
||||||
return $r;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a SQLite3 database connection, a column name, and a value. Execute a SELECT query to see if the value
|
|
||||||
* exists in the column. Return true if the value exists, false otherwise.
|
|
||||||
*/
|
|
||||||
function db_exists(SQLite3 $db, string $table, string $column, mixed $value, bool $caseInsensitive = true): bool
|
|
||||||
{
|
|
||||||
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]);
|
|
||||||
return $result->fetchArray(SQLITE3_NUM) !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the appropriate SQLite type casting for the value.
|
|
||||||
*/
|
|
||||||
function getSQLiteType($value): int
|
|
||||||
{
|
|
||||||
return match (true) {
|
|
||||||
is_int($value) => SQLITE3_INTEGER,
|
|
||||||
is_float($value) => SQLITE3_FLOAT,
|
|
||||||
is_null($value) => SQLITE3_NULL,
|
|
||||||
default => SQLITE3_TEXT
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log the given query string to the db debug log.
|
|
||||||
*/
|
|
||||||
function db_log($query, $timeTaken = 0)
|
|
||||||
{
|
|
||||||
$GLOBALS['queries']++;
|
|
||||||
$GLOBALS['query_time'] += $timeTaken;
|
|
||||||
if (env('debug', false)) $GLOBALS['query_log'][] = [$query, $timeTaken];
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the environment variables from the .env file.
|
|
||||||
*/
|
|
||||||
function env_load($filePath)
|
|
||||||
{
|
|
||||||
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
|
|
||||||
|
|
||||||
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$line = trim($line);
|
|
||||||
|
|
||||||
// Skip lines that are empty after trimming or are comments
|
|
||||||
if ($line === '' || str_starts_with($line, '#')) continue;
|
|
||||||
|
|
||||||
// Skip lines without an '=' character
|
|
||||||
if (strpos($line, '=') === false) continue;
|
|
||||||
|
|
||||||
[$name, $value] = explode('=', $line, 2);
|
|
||||||
|
|
||||||
$name = trim($name);
|
|
||||||
$value = trim($value, " \t\n\r\0\x0B\"'"); // Trim whitespace and quotes
|
|
||||||
|
|
||||||
if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
|
|
||||||
putenv("$name=$value");
|
|
||||||
$_ENV[$name] = $value;
|
|
||||||
$_SERVER[$name] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an environment variable.
|
|
||||||
*/
|
|
||||||
function env($key, $default = null)
|
|
||||||
{
|
|
||||||
$v = $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
|
|
||||||
return match(true) {
|
|
||||||
$v === 'true' => true,
|
|
||||||
$v === 'false' => false,
|
|
||||||
is_numeric($v) => (int) $v,
|
|
||||||
is_float($v) => (float) $v,
|
|
||||||
default => $v
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the path to a view file.
|
|
||||||
*/
|
|
||||||
function template($name)
|
|
||||||
{
|
|
||||||
return SRC . "/../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();
|
|
||||||
}
|
|
|
@ -9,11 +9,20 @@
|
||||||
<script src="/assets/scripts/WindowManager.js"></script>
|
<script src="/assets/scripts/WindowManager.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="game-container">
|
<canvas id="canvas"></canvas>
|
||||||
<div id="game-ui">
|
|
||||||
|
<div id="ui">
|
||||||
|
<section id="menu">
|
||||||
|
<a id="btn-chars"><img src="/assets/img/ui/icons/user.png"></a>
|
||||||
|
<a id="btn-stats"><img src="/assets/img/ui/icons/bargraph.png"></a>
|
||||||
|
<form class="logout-form" action="/logout" method="post">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit"><img src="/assets/img/ui/icons/stop.png"></button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="character-hud">
|
<section id="character-hud">
|
||||||
<span id="character-name"><?= char()->name ?></span>
|
<span id="character-name">(<?= char()->level ?>) <?= char()->name ?>, <?= char()->title()['name'] ?></span>
|
||||||
<span id="character-title">L<?= char()->level ?> <?= char()->title()['name'] ?></span>
|
|
||||||
|
|
||||||
<div class="hud-meter">
|
<div class="hud-meter">
|
||||||
<div class="hp" style="width: <?= percent(char()->hp, char()->m_hp) ?>%"></div>
|
<div class="hp" style="width: <?= percent(char()->hp, char()->m_hp) ?>%"></div>
|
||||||
|
@ -25,25 +34,13 @@
|
||||||
<!--<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Mana<br><?= char()->mp ?> / <?= char()->m_mp ?>"></div>-->
|
<!--<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Mana<br><?= char()->mp ?> / <?= char()->m_mp ?>"></div>-->
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="menu">
|
|
||||||
<a id="btn-chars"><img src="/assets/img/ui/icons/user.png"></a>
|
|
||||||
<a id="btn-stats"><img src="/assets/img/ui/icons/bargraph.png"></a>
|
|
||||||
<form action="/logout" method="post">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<input type="submit" value="Logout" class="ui button secondary">
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="game-windows"></div>
|
<div id="windows"></div>
|
||||||
|
|
||||||
<canvas id="game-canvas"></canvas>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const csrf = '<?= csrf() ?>'
|
const csrf = '<?= csrf() ?>'
|
||||||
let WM = new WindowManager(document.getElementById('game-windows'))
|
const WM = new WindowManager(document.getElementById('windows'))
|
||||||
|
|
||||||
const uiBtns = {
|
const uiBtns = {
|
||||||
'stats': document.getElementById('btn-stats'),
|
'stats': document.getElementById('btn-stats'),
|
||||||
|
@ -52,20 +49,16 @@
|
||||||
|
|
||||||
uiBtns.stats.addEventListener('click', function () {
|
uiBtns.stats.addEventListener('click', function () {
|
||||||
fetch('/ui/stats', {
|
fetch('/ui/stats', {
|
||||||
method: 'POST',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
},
|
'X-CSRF': csrf
|
||||||
body: `csrf=${csrf}`
|
|
||||||
}).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.text()
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to move character');
|
|
||||||
}
|
}
|
||||||
}).then(text => {
|
}).then(response => {
|
||||||
WM.updateWindow('stats', text, 'Stats')
|
return response.text()
|
||||||
|
}).then(html => {
|
||||||
|
WM.updateWindow('stats', html, 'Stats')
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
|
@ -74,7 +67,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const game = {
|
const game = {
|
||||||
canvas: document.getElementById('game-canvas'),
|
canvas: document.getElementById('canvas'),
|
||||||
tiles: {
|
tiles: {
|
||||||
size: 32,
|
size: 32,
|
||||||
img: new Image(),
|
img: new Image(),
|
||||||
|
|
4
templates/pages/hello.php
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<h1>Welcome!</h1>
|
||||||
|
<a class="ui button primary" href="/login">Login</a>
|
||||||
|
<a class="ui button secondary" href="/register">Register</a>
|
||||||
|
|