Significant refactors, add middleware to router
This commit is contained in:
parent
d82e0fdaf3
commit
50b78f8131
|
@ -17,7 +17,7 @@ CREATE TABLE sessions (
|
|||
`token` TEXT NOT NULL UNIQUE,
|
||||
`expires` INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_sessions_user_id ON sessions (`user_id`);
|
||||
CREATE INDEX idx_sessions_token ON sessions (`token`);
|
||||
|
||||
DROP TABLE IF EXISTS tokens;
|
||||
CREATE TABLE tokens (
|
||||
|
|
|
@ -72,6 +72,6 @@ case $1 in
|
|||
drop_db
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {create|populate|reset}"
|
||||
echo "Usage: $0 {create|populate|reset|drop}"
|
||||
;;
|
||||
esac
|
||||
|
|
126
public/index.php
126
public/index.php
|
@ -20,85 +20,14 @@ $r->get('/', function() {
|
|||
/*
|
||||
Auth
|
||||
*/
|
||||
$r->get('/register', function() {
|
||||
guest_only();
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||
});
|
||||
|
||||
$r->post('/register', function() {
|
||||
guest_only();
|
||||
csrf_ensure();
|
||||
|
||||
$errors = [];
|
||||
|
||||
$u = trim($_POST['u'] ?? '');
|
||||
$e = trim($_POST['e'] ?? '');
|
||||
$p = $_POST['p'] ?? '';
|
||||
|
||||
/*
|
||||
A username is required.
|
||||
A username must be at least 3 characters long and at most 18 characters long.
|
||||
A username must contain only alphanumeric characters and spaces.
|
||||
*/
|
||||
if (empty($u) || strlen($u) < 3 || strlen($u) > 18 || !ctype_alnum(str_replace(' ', '', $u))) {
|
||||
$errors['u'][] = 'Username is required and must be between 3 and 18 characters long and contain only
|
||||
alphanumeric characters and spaces.';
|
||||
}
|
||||
|
||||
/*
|
||||
An email is required.
|
||||
An email must be at most 255 characters long.
|
||||
An email must be a valid email address.
|
||||
*/
|
||||
if (empty($e) || strlen($e) > 255 || !filter_var($e, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['e'][] = 'Email is required must be a valid email address.';
|
||||
}
|
||||
|
||||
/*
|
||||
A password is required.
|
||||
A password must be at least 6 characters long.
|
||||
*/
|
||||
if (empty($p) || strlen($p) < 6) {
|
||||
$errors['p'][] = 'Password is required and must be at least 6 characters long.';
|
||||
}
|
||||
|
||||
/*
|
||||
A username must be unique.
|
||||
*/
|
||||
if (User::username_exists($u)) {
|
||||
$errors['u'][] = 'Username is already taken.';
|
||||
}
|
||||
|
||||
/*
|
||||
An email must be unique.
|
||||
*/
|
||||
if (User::email_exists($e)) {
|
||||
$errors['e'][] = 'Email is already taken.';
|
||||
}
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
$GLOBALS['form-errors'] = $errors;
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (User::create($u, $e, $p) === false) error_response(400);
|
||||
|
||||
$_SESSION['user'] = serialize(User::find($u));
|
||||
Wallet::create(user()->id);
|
||||
redirect('/character/create-first');
|
||||
});
|
||||
$r->get('/register', 'Actions\Auth::register_get')->middleware('guest_only');
|
||||
$r->post('/register', 'Actions\Auth::register_post')->middleware('guest_only');
|
||||
|
||||
$r->get('/login', function() {
|
||||
guest_only();
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/login']);
|
||||
});
|
||||
})->middleware('guest_only');
|
||||
|
||||
$r->post('/login', function() {
|
||||
guest_only();
|
||||
csrf_ensure();
|
||||
|
||||
$errors = [];
|
||||
|
||||
$u = trim($_POST['u'] ?? '');
|
||||
|
@ -138,10 +67,9 @@ $r->post('/login', function() {
|
|||
}
|
||||
|
||||
redirect('/');
|
||||
});
|
||||
})->middleware('guest_only');
|
||||
|
||||
$r->post('/logout', function() {
|
||||
csrf_ensure();
|
||||
Session::delete(user()->id);
|
||||
unset($_SESSION['user']);
|
||||
set_cookie('remember_me', '', 1);
|
||||
|
@ -159,18 +87,10 @@ $r->get('/debug/logout', function() {
|
|||
Characters
|
||||
*/
|
||||
$r->get('/characters', function() {
|
||||
auth_only_and_must_have_character();
|
||||
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
//echo page('chars/list', ['chars' => user()->char_list()]);
|
||||
});
|
||||
})->middleware('must_have_character');
|
||||
|
||||
$r->post('/characters', function() {
|
||||
auth_only_and_must_have_character();
|
||||
csrf_ensure();
|
||||
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
|
||||
$char_id = (int) ($_POST['char_id'] ?? 0);
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
|
@ -210,24 +130,16 @@ $r->post('/characters', function() {
|
|||
}
|
||||
|
||||
redirect('/characters');
|
||||
});
|
||||
})->middleware('must_have_character');
|
||||
|
||||
$r->get('/character/create-first', function() {
|
||||
auth_only();
|
||||
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
|
||||
// If the user already has a character, redirect them to the main page.
|
||||
if (user()->char_count() > 0) redirect('/');
|
||||
|
||||
//echo page('chars/first');
|
||||
});
|
||||
})->middleware('auth_only');
|
||||
|
||||
$r->post('/character/create', function() {
|
||||
auth_only(); csrf_ensure();
|
||||
|
||||
$GLOBALS['active_nav_tab'] = 'chars';
|
||||
|
||||
$errors = [];
|
||||
|
||||
$name = trim($_POST['n'] ?? '');
|
||||
|
@ -275,12 +187,9 @@ $r->post('/character/create', function() {
|
|||
|
||||
flash('alert_character_list_1', ['success', 'Character <b>' . $name . '</b> created!']);
|
||||
redirect('/characters');
|
||||
});
|
||||
})->middleware('auth_only');
|
||||
|
||||
$r->post('/character/delete', function() {
|
||||
auth_only_and_must_have_character();
|
||||
csrf_ensure();
|
||||
|
||||
$char_id = (int) ($_POST['char_id'] ?? 0);
|
||||
|
||||
// If the character ID is not a number, return a 400.
|
||||
|
@ -308,15 +217,14 @@ $r->post('/character/delete', function() {
|
|||
|
||||
flash('alert_character_list_1', ['danger', 'Character <b>' . $char['name'] . '</b> deleted.']);
|
||||
redirect('/characters');
|
||||
});
|
||||
})->middleware('must_have_character');
|
||||
|
||||
/*
|
||||
World
|
||||
*/
|
||||
$r->get('/world', function() {
|
||||
auth_only_and_must_have_character();
|
||||
echo render('layouts/game');
|
||||
});
|
||||
})->middleware('must_have_character');
|
||||
|
||||
$r->post('/move', function() {
|
||||
/*
|
||||
|
@ -328,8 +236,6 @@ $r->post('/move', function() {
|
|||
data to move them, we can just get and update their lcoation using the user's currently selected character ID.
|
||||
*/
|
||||
|
||||
ajax_only(); auth_only(); csrf_ensure();
|
||||
|
||||
define('directions', [
|
||||
[0, -1], // Up
|
||||
[0, 1], // Down
|
||||
|
@ -361,28 +267,24 @@ $r->post('/move', function() {
|
|||
if ($r === false) throw new Exception('Failed to move character. (wcmp)');
|
||||
|
||||
json_response(['x' => $x, 'y' => $y]);
|
||||
});
|
||||
})->middleware('ajax_only')->middleware('must_have_character');
|
||||
|
||||
/*
|
||||
UI
|
||||
*/
|
||||
$r->get('/ui/stats', function() {
|
||||
ui_guard();
|
||||
echo c_profile_stats(char());
|
||||
});
|
||||
})->middleware('ajax_only')->middleware('must_have_character');
|
||||
|
||||
/*
|
||||
Router
|
||||
*/
|
||||
// [code, handler, params]
|
||||
stopwatch_start('router');
|
||||
// [code, handler, params, middleware]
|
||||
$l = $r->lookup($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
|
||||
stopwatch_stop('router');
|
||||
|
||||
stopwatch_start('handler');
|
||||
if ($l['code'] !== 200) error_response($l['code']);
|
||||
if (!empty($l['middleware'])) foreach ($l['middleware'] as $middleware) $middleware();
|
||||
$l['handler'](...$l['params'] ?? []);
|
||||
stopwatch_stop('handler');
|
||||
|
||||
/*
|
||||
Cleanup
|
||||
|
|
76
src/actions/auth.php
Normal file
76
src/actions/auth.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace Actions;
|
||||
|
||||
use \User;
|
||||
|
||||
class Auth
|
||||
{
|
||||
public static function register_get(): void
|
||||
{
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||
}
|
||||
|
||||
public static function register_post(): void
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
$u = trim($_POST['u'] ?? '');
|
||||
$e = trim($_POST['e'] ?? '');
|
||||
$p = $_POST['p'] ?? '';
|
||||
|
||||
/*
|
||||
A username is required.
|
||||
A username must be at least 3 characters long and at most 18 characters long.
|
||||
A username must contain only alphanumeric characters and spaces.
|
||||
*/
|
||||
if (empty($u) || strlen($u) < 3 || strlen($u) > 18 || !ctype_alnum(str_replace(' ', '', $u))) {
|
||||
$errors['u'][] = 'Username is required and must be between 3 and 18 characters long and contain only
|
||||
alphanumeric characters and spaces.';
|
||||
}
|
||||
|
||||
/*
|
||||
An email is required.
|
||||
An email must be at most 255 characters long.
|
||||
An email must be a valid email address.
|
||||
*/
|
||||
if (empty($e) || strlen($e) > 255 || !filter_var($e, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['e'][] = 'Email is required must be a valid email address.';
|
||||
}
|
||||
|
||||
/*
|
||||
A password is required.
|
||||
A password must be at least 6 characters long.
|
||||
*/
|
||||
if (empty($p) || strlen($p) < 6) {
|
||||
$errors['p'][] = 'Password is required and must be at least 6 characters long.';
|
||||
}
|
||||
|
||||
/*
|
||||
A username must be unique.
|
||||
*/
|
||||
if (User::username_exists($u)) {
|
||||
$errors['u'][] = 'Username is already taken.';
|
||||
}
|
||||
|
||||
/*
|
||||
An email must be unique.
|
||||
*/
|
||||
if (User::email_exists($e)) {
|
||||
$errors['e'][] = 'Email is already taken.';
|
||||
}
|
||||
|
||||
// If there are errors at this point, send them to the page with errors flashed.
|
||||
if (!empty($errors)) {
|
||||
$GLOBALS['form-errors'] = $errors;
|
||||
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (\User::create($u, $e, $p) === false) error_response(400);
|
||||
|
||||
$_SESSION['user'] = serialize(\User::find($u));
|
||||
\Wallet::create(user()->id);
|
||||
redirect('/character/create-first');
|
||||
}
|
||||
}
|
72
src/auth.php
72
src/auth.php
|
@ -1,72 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* 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'].
|
||||
*/
|
||||
function auth_check()
|
||||
{
|
||||
if (isset($_SESSION['user'])) return true;
|
||||
|
||||
if (isset($_COOKIE['remember_me'])) {
|
||||
$session = Session::find($_COOKIE['remember_me']);
|
||||
if ($session->validate()) {
|
||||
$user = User::find($session->user_id);
|
||||
$_SESSION['user'] = serialize($user);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function auth_only()
|
||||
{
|
||||
if (!auth_check()) redirect('/auth/login');
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a user logged in, redirect to the home page. Used for when we have a guest-only page.
|
||||
*/
|
||||
function guest_only()
|
||||
{
|
||||
if (auth_check()) redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function must_have_character()
|
||||
{
|
||||
// If there is a character selected, make sure the session is up to date.
|
||||
if (user()->char_id !== 0) {
|
||||
char();
|
||||
return;
|
||||
}
|
||||
|
||||
// if no characters, redirect to create first
|
||||
if (user()->char_count() === 0) redirect('/character/create-first');
|
||||
|
||||
// if no character selected, select the first one
|
||||
if (user()->char_id === 0) {
|
||||
$char = live_db()->query(
|
||||
'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1',
|
||||
[':u' => user()->id]
|
||||
)->fetchArray(SQLITE3_ASSOC);
|
||||
change_user_character($char['id']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user must be authenticated and have a character.
|
||||
*/
|
||||
function auth_only_and_must_have_character()
|
||||
{
|
||||
auth_only();
|
||||
must_have_character();
|
||||
}
|
|
@ -1,9 +1,28 @@
|
|||
<?php
|
||||
|
||||
session_start();
|
||||
|
||||
// SRC is defined as the path to the src/ directory from public/
|
||||
|
||||
/*
|
||||
============================================================
|
||||
Libraries
|
||||
============================================================
|
||||
*/
|
||||
require_once SRC . '/helpers.php';
|
||||
require_once SRC . '/components.php';
|
||||
require_once SRC . '/enums.php';
|
||||
|
||||
/*
|
||||
============================================================
|
||||
PHP Sessions
|
||||
============================================================
|
||||
*/
|
||||
session_start();
|
||||
|
||||
/*
|
||||
============================================================
|
||||
Autoloading
|
||||
============================================================
|
||||
*/
|
||||
define('CLASS_MAP', [
|
||||
'Database' => '/database.php',
|
||||
'Router' => '/router.php',
|
||||
|
@ -11,15 +30,9 @@ define('CLASS_MAP', [
|
|||
'Character' => '/models/character.php',
|
||||
'Wallet' => '/models/wallet.php',
|
||||
'Session' => '/models/session.php',
|
||||
'Actions\Auth' => '/actions/auth.php',
|
||||
]);
|
||||
|
||||
require_once SRC . '/helpers.php';
|
||||
require_once SRC . 'auth.php';
|
||||
require_once SRC . 'components.php';
|
||||
require_once SRC . 'enums.php';
|
||||
|
||||
require_once SRC . '/models/token.php';
|
||||
|
||||
spl_autoload_register(function (string $class) {
|
||||
if (array_key_exists($class, CLASS_MAP)) require_once SRC . CLASS_MAP[$class];
|
||||
});
|
||||
|
@ -28,21 +41,39 @@ spl_autoload_register(function (string $class) {
|
|||
define('START_TIME', microtime(true));
|
||||
|
||||
/*
|
||||
Load env, set error reporting, etc.
|
||||
============================================================
|
||||
.ENV
|
||||
============================================================
|
||||
*/
|
||||
env_load(SRC . '/../.env');
|
||||
|
||||
if (env('debug') === 'true') {
|
||||
if (env('debug', false)) {
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
}
|
||||
|
||||
// Create an array in GLOBALS to hold database connections.
|
||||
$GLOBALS['databases'] = [];
|
||||
/*
|
||||
============================================================
|
||||
CSRF
|
||||
============================================================
|
||||
*/
|
||||
csrf(); // generate a CSRF token, or retrieve the current token
|
||||
|
||||
// Generate a new CSRF token. (if one doesn't exist, that is)
|
||||
csrf();
|
||||
// error any request that fails CSRF on these methods
|
||||
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])) {
|
||||
$csrf = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF'] ?? '';
|
||||
if (!hash_equals($_SESSION['csrf'] ?? '', $csrf)) error_response(418);
|
||||
}
|
||||
|
||||
// Run auth_check to see if we're logged in, since it populates the user data in SESSION
|
||||
auth_check();
|
||||
/*
|
||||
============================================================
|
||||
Global State
|
||||
============================================================
|
||||
*/
|
||||
$GLOBALS['databases'] = []; // database interfaces
|
||||
|
||||
// all relevant state to handling requests
|
||||
$GLOBALS['state'] = [
|
||||
'logged_in' => isset($_SESSION['user']) || validate_session()
|
||||
];
|
||||
|
|
|
@ -1,46 +1,45 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Generic wrapper around a SQLite3 object to add our own semantics to data operations.
|
||||
* An extension to the SQLite3 class to add our own logging and binding semantics!
|
||||
*/
|
||||
class Database
|
||||
class Database extends SQLite3
|
||||
{
|
||||
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);
|
||||
parent::__construct($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;
|
||||
parent::exec('PRAGMA cache_size = 32000');
|
||||
parent::exec('PRAGMA journal_mode = WAL');
|
||||
parent::exec('PRAGMA temp_store = MEMORY');
|
||||
}
|
||||
|
||||
public function query(string $query, array $params = []): SQLite3Result|false
|
||||
{
|
||||
$p = strpos($query, '?') !== false; // generic placeholders?
|
||||
$stmt = $this->db->prepare($query);
|
||||
$p = strpos($query, '?') !== false;
|
||||
$stmt = $this->prepare($query);
|
||||
|
||||
if (!empty($params)) {
|
||||
foreach ($params as $k => $v) $stmt->bindValue($p ? $k + 1 : $k, $v, $this->getSQLiteType($v));
|
||||
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);
|
||||
$r = parent::exec($query);
|
||||
$this->log($query, microtime(true) - $start);
|
||||
return $r;
|
||||
}
|
||||
|
@ -61,7 +60,10 @@ class Database
|
|||
{
|
||||
$this->count++;
|
||||
$this->query_time += $time_taken;
|
||||
if (env('debug', false)) $this->log[] = [$query, $time_taken];
|
||||
|
||||
if (env('debug', false)) {
|
||||
$this->log[] = [$query, $time_taken];
|
||||
}
|
||||
}
|
||||
|
||||
private function getSQLiteType(mixed $value): int
|
||||
|
@ -73,9 +75,4 @@ class Database
|
|||
default => SQLITE3_TEXT
|
||||
};
|
||||
}
|
||||
|
||||
public function lastInsertRowID(): int
|
||||
{
|
||||
return $this->db->lastInsertRowID();
|
||||
}
|
||||
}
|
||||
|
|
134
src/helpers.php
134
src/helpers.php
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Load the environment variables from the .env file.
|
||||
*/
|
||||
function env_load($filePath)
|
||||
function env_load(string $filePath): void
|
||||
{
|
||||
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
|
||||
|
||||
|
@ -34,7 +34,7 @@ function env_load($filePath)
|
|||
/**
|
||||
* Retrieve an environment variable.
|
||||
*/
|
||||
function env($key, $default = null)
|
||||
function env(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$v = $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
|
||||
return match(true) {
|
||||
|
@ -135,14 +135,6 @@ function csrf()
|
|||
return $_SESSION['csrf'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a CSRF token.
|
||||
*/
|
||||
function csrf_verify($token)
|
||||
{
|
||||
return hash_equals($_SESSION['csrf'] ?? '', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hidden input field for CSRF tokens.
|
||||
*/
|
||||
|
@ -157,7 +149,7 @@ function csrf_field()
|
|||
function csrf_ensure()
|
||||
{
|
||||
$csrf = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF'] ?? '';
|
||||
if (!csrf_verify($csrf)) error_response(418);
|
||||
if (!hash_equals($_SESSION['csrf'] ?? '', $csrf)) error_response(418);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -181,7 +173,7 @@ function set_cookie($name, $value, $expires)
|
|||
function user(): User|false
|
||||
{
|
||||
if (empty($_SESSION['user'])) return false;
|
||||
return unserialize($_SESSION['user']);
|
||||
return $GLOBALS['state']['user'] ??= unserialize($_SESSION['user']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -277,35 +269,6 @@ function array_to_ul($array)
|
|||
return "<ul>$html</ul>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a keyed stopwatch to measure the time between two points in the code.
|
||||
*/
|
||||
function stopwatch_start($key)
|
||||
{
|
||||
if (!env('debug', false)) return;
|
||||
$GLOBALS['stopwatch'][$key] = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a keyed stopwatch. Stores the time in the global $stopwatch array under the key.
|
||||
*/
|
||||
function stopwatch_stop($key)
|
||||
{
|
||||
if (!env('debug', false)) return;
|
||||
if (empty($GLOBALS['stopwatch'][$key])) return 0;
|
||||
$GLOBALS['stopwatch'][$key] = microtime(true) - $GLOBALS['stopwatch'][$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stopwatch value and format it to within 10 digits.
|
||||
*/
|
||||
function stopwatch_get(string $key): string
|
||||
{
|
||||
if (!env('debug', false)) return '';
|
||||
if (empty($GLOBALS['stopwatch'][$key])) return '';
|
||||
return number_format($GLOBALS['stopwatch'][$key], 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional Echo; if the condition is true, echo the value. If the condition is false, echo the $or value.
|
||||
*/
|
||||
|
@ -322,22 +285,6 @@ function is_htmx(): bool
|
|||
return isset($_SERVER['HTTP_HX_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the request is an AJAX (fetch) request.
|
||||
*/
|
||||
function is_ajax(): bool
|
||||
{
|
||||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit a request to AJAX only.
|
||||
*/
|
||||
function ajax_only(): void
|
||||
{
|
||||
if (!is_ajax()) error_response(418);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a JSON response with the given data.
|
||||
*/
|
||||
|
@ -435,17 +382,6 @@ function parse_bbcode(string $text): array
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand to verify auth, a character is selected, CSRF is correct, and it is an AJAX request. Used for
|
||||
* front-end API routes.
|
||||
*/
|
||||
function ui_guard()
|
||||
{
|
||||
auth_only_and_must_have_character();
|
||||
ajax_only();
|
||||
csrf_ensure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand to call fetchArray() on a SQLite3Result. Defaults to SQLITE3_ASSOC but can pass any constant to $mode.
|
||||
*/
|
||||
|
@ -453,3 +389,65 @@ function db_fetch_array(SQLite3Result $result, int $mode = SQLITE3_ASSOC): array
|
|||
{
|
||||
return $result->fetchArray($mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is a valid remember cookie, and whether that cookie has a valid session token.
|
||||
*/
|
||||
function validate_session(): bool
|
||||
{
|
||||
if (!isset($_COOKIE['remember_me'])) return false;
|
||||
if (($session = Session::find($_COOKIE['remember_me'])) && $session->validate()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure this is an AJAX-only request.
|
||||
*/
|
||||
function ajax_only(): void
|
||||
{
|
||||
$header = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
|
||||
if (!strtolower($header) === 'xmlhttprequest') error_response(418);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a request as guest only.
|
||||
*/
|
||||
function guest_only(): void
|
||||
{
|
||||
if ($GLOBALS['state']['logged_in']) redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a request as auth'd only.
|
||||
*/
|
||||
function auth_only(): void
|
||||
{
|
||||
if (!$GLOBALS['state']['logged_in']) redirect('/auth/login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a request as needing to have a character selected. Automatically checks auth_only as well.
|
||||
*/
|
||||
function must_have_character(): void
|
||||
{
|
||||
auth_only();
|
||||
$user = user();
|
||||
|
||||
// If there is a character selected, make sure the session is up to date.
|
||||
if ($user->char_id !== 0) {
|
||||
char();
|
||||
return;
|
||||
}
|
||||
|
||||
// if no characters, redirect to create first
|
||||
if ($user->char_count() === 0) redirect('/character/create-first');
|
||||
|
||||
// if no character selected, select the first one
|
||||
if ($user->char_id === 0) {
|
||||
$char = live_db()->query(
|
||||
'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1',
|
||||
[':u' => $user->id]
|
||||
)->fetchArray(SQLITE3_ASSOC);
|
||||
change_user_character($char['id']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,11 @@ class Router
|
|||
*/
|
||||
private array $routes = [];
|
||||
|
||||
/**
|
||||
* Store the last inserted node so we can register middleware and attributes to it.
|
||||
*/
|
||||
private array $last_inserted_node;
|
||||
|
||||
/**
|
||||
* Add a route to the route tree. The route must be a URI path, and contain dynamic segments
|
||||
* using a colon prefix. (:id, :slug, etc)
|
||||
|
@ -42,7 +47,10 @@ class Router
|
|||
}
|
||||
|
||||
// Add the handler to the last node
|
||||
$node[$method] = $handler;
|
||||
$node[$method] = ['handler' => $handler, 'middleware' => []];
|
||||
|
||||
// Store a reference to the node so we can add middleware to it.
|
||||
$this->last_inserted_node = &$node[$method];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -65,8 +73,8 @@ class Router
|
|||
// if the URI is just a slash, we can return the handler for the root node
|
||||
if ($uri === '/') {
|
||||
return isset($node[$method])
|
||||
? ['code' => 200, 'handler' => $node[$method], 'params' => null]
|
||||
: ['code' => 405, 'handler' => null, 'params' => null];
|
||||
? ['code' => 200, 'handler' => $node[$method]['handler']]
|
||||
: ['code' => 405];
|
||||
}
|
||||
|
||||
// We'll split up the URI into segments and traverse the node tree
|
||||
|
@ -85,13 +93,22 @@ class Router
|
|||
}
|
||||
|
||||
// if we can't find a node for this segment, return 404
|
||||
return ['code' => 404, 'handler' => null, 'params' => []];
|
||||
return ['code' => 404];
|
||||
}
|
||||
|
||||
// if we found a handler for the method, return it and any params. if not, return a 405
|
||||
return isset($node[$method])
|
||||
? ['code' => 200, 'handler' => $node[$method], 'params' => $params ?? []]
|
||||
: ['code' => 405, 'handler' => null, 'params' => []];
|
||||
? ['code' => 200, 'handler' => $node[$method]['handler'], 'params' => $params ?? [], 'middleware' => $node[$method]['middleware']]
|
||||
: ['code' => 405];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a middleware function to the last inserted node's stack.
|
||||
*/
|
||||
public function middleware(callable $middleware): Router
|
||||
{
|
||||
$this->last_inserted_node['middleware'][] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user