Dragon-Knight/src/lib.php

621 lines
19 KiB
PHP

<?php
require_once __DIR__ . '/database.php';
define('VERSION', '1.2.5');
define('BUILD', 'Reawaken');
define('START', microtime(true));
/**
* Open or get SQLite database connection.
*/
function db(): Database
{
return $GLOBALS['database'] ??= new Database(__DIR__ . '/../database.db');
}
/**
* Redirect to a different URL, exit.
*/
function redirect(string $location): void
{
header("Location: $location");
exit;
}
/**
* Return the path to a view file.
*/
function template(string $name): string
{
return "../templates/$name.php";
}
/**
* Render a view with the given data. Looks for `$path_to_base_view` through `template()`. Can be used redundantly
* within the 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();
}
/**
* Replace tags with given content.
*/
function parse(string $template, array $array): string
{
return strtr($template, array_combine(
array_map(fn($key) => "{{{$key}}}", array_keys($array)),
array_values($array)
));
}
/**
* Change the SQLite3 datetime format (YYYY-MM-DD HH:MM:SS) into something friendlier.
*/
function pretty_date(string $uglydate): string
{
return date("l, F j, Y", mktime(
0,
0,
0,
substr($uglydate, 5, 2), // Month
substr($uglydate, 8, 2), // Day
substr($uglydate, 0, 4) // Year
));
}
/**
* Use htmlentities with UTF-8 encoding to ensure we're only outputting healthy, safe and effective HTML.
*/
function make_safe(string $content): string
{
return htmlentities($content, ENT_QUOTES, 'UTF-8');
}
/**
* Finalize admin page and output to browser.
*/
function display_admin($content, $title)
{
echo render('admin', [
"title" => $title,
"content" => $content,
"totaltime" => round(microtime(true) - START, 4),
"numqueries" => db()->count,
"version" => VERSION,
"build" => BUILD
]);
exit;
}
/**
* Finalize page and output to browser.
*/
function display($content, $title, bool $topnav = true, bool $leftnav = true, bool $rightnav = true): void
{
global $userrow, $controlrow;
if ($topnav == true) {
if ($userrow !== false) { // user should be logged in
$topnav = <<<HTML
<a href='/logout'><img src='/img/button_logout.gif' alt='Log Out' title='Log Out'></a>
<a href='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>
HTML;
} else {
$topnav = <<<HTML
<a href='/login'><img src='/img/button_login.gif' alt='Log In' title='Log In'></a>
<a href='/register'><img src='/img/button_register.gif' alt='Register' title='Register'></a>
<a href='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>
HTML;
}
} else {
$topnav = '';
}
if (isset($userrow) && $userrow !== false) {
// Get userrow again, in case something has been updated.
$userquery = db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$userrow['id']]);
unset($userrow);
$userrow = $userquery->fetchArray(SQLITE3_ASSOC);
// Current town name.
if ($userrow['currentaction'] == 'In Town') {
$townrow = get_town_by_xy($userrow['latitude'], $userrow['longitude']);
$userrow['currenttown'] = "Welcome to <b>{$townrow['name']}</b>.<br><br>";
} else {
$userrow['currenttown'] = '';
}
$userrow['forumslink'] = '<a href="/forum">Forum</a><br>';
// Format various userrow stuffs...
if ($userrow["latitude"] < 0) { $userrow["latitude"] = $userrow["latitude"] * -1 . "S"; } else { $userrow["latitude"] .= "N"; }
if ($userrow["longitude"] < 0) { $userrow["longitude"] = $userrow["longitude"] * -1 . "W"; } else { $userrow["longitude"] .= "E"; }
$userrow["experience"] = number_format($userrow["experience"]);
$userrow["gold"] = number_format($userrow["gold"]);
if ($userrow["authlevel"] == 1) { $userrow["adminlink"] = "<a href=\"/admin\">Admin</a><br>"; } else { $userrow["adminlink"] = ""; }
// HP/MP/TP bars.
$userrow['statbars'] = create_stat_table($userrow);
// Now make numbers stand out if they're low.
if ($userrow["currenthp"] <= ($userrow["maxhp"]/5)) { $userrow["currenthp"] = "<blink><span class=\"highlight\"><b>*".$userrow["currenthp"]."*</b></span></blink>"; }
if ($userrow["currentmp"] <= ($userrow["maxmp"]/5)) { $userrow["currentmp"] = "<blink><span class=\"highlight\"><b>*".$userrow["currentmp"]."*</b></span></blink>"; }
$user_spells = explode(',', $userrow['spells']);
$spellquery = get_spells_from_list($user_spells);
$userrow['magiclist'] = '';
while ($spell = $spellquery->fetchArray(SQLITE3_ASSOC)) {
$spell = false;
foreach($user_spells as $id) {
if ($id === $spell['id'] && $spell['type'] == 1) $spell = true;
}
if ($spell == true) {
$userrow['magiclist'] .= "<a href=\"/spell/{$spell['id']}\">".$spell['name']."</a><br>";
}
}
if ($userrow["magiclist"] == "") { $userrow["magiclist"] = "None"; }
// Travel To list.
$townslist = explode(",",$userrow["towns"]);
$townquery2 = db()->query('SELECT * FROM towns ORDER BY id;');
$userrow["townslist"] = "";
while ($townrow2 = $townquery2->fetchArray(SQLITE3_ASSOC)) {
$town = false;
foreach($townslist as $a => $b) {
if ($b == $townrow2["id"]) { $town = true; }
}
if ($town == true) {
$userrow["townslist"] .= "<a href=\"/gotown/{$townrow2["id"]}\">".$townrow2["name"]."</a><br>\n";
}
}
} else {
$userrow = [];
}
echo render('primary', [
"dkgamename" => $controlrow["gamename"],
"title" => $title,
"content" => $content,
"game_skin" => $userrow['game_skin'] ??= '0',
'rightnav' => $rightnav ? render('rightnav', ['user' => $userrow]) : '',
"leftnav" => $leftnav ? render('leftnav', ['user' => $userrow]) : '',
"topnav" => $topnav,
"totaltime" => round(microtime(true) - START, 4),
"numqueries" => db()->count,
"version" => VERSION,
"build" => BUILD,
"querylog" => env('debug', false) ? db()->log : []
]);
exit;
}
function checkcookies()
{
$row = false;
if (isset($_COOKIE["dkgame"])) {
// COOKIE FORMAT:
// {ID} {USERNAME} {PASSWORDHASH} {REMEMBERME}
$theuser = explode(" ",$_COOKIE["dkgame"]);
$query = db()->query('SELECT * FROM users WHERE id = ? AND username = ? AND password = ? LIMIT 1;', [$theuser[0], $theuser[1], $theuser[2]]);
if ($query === false) {
set_cookie('dkgame', '', -3600);
die("Invalid cookie data. Please log in again.");
}
$row = $query->fetchArray(SQLITE3_ASSOC);
set_cookie('dkgame', implode(" ", $theuser), (int) $theuser[3] === 1 ? time() + 31536000 : 0);
db()->query('UPDATE users SET onlinetime = CURRENT_TIMESTAMP WHERE id = ?;', [$theuser[0]]);
}
return $row;
}
/**
* Set a cookie with secure and HTTP-only flags.
*/
function set_cookie($name, $value, $expires)
{
setcookie($name, $value, [
'expires' => $expires,
'path' => '/',
'domain' => '', // Defaults to the current domain
'secure' => true, // Ensure the cookie is only sent over HTTPS
'httponly' => true, // Prevent access to cookie via JavaScript
'samesite' => 'Strict' // Enforce SameSite=Strict
]);
}
/**
* Get the current control row from the database.
*/
function get_control_row(): array|false
{
$query = db()->query('SELECT * FROM control WHERE id = 1 LIMIT 1;');
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a town's data by it's coordinates.
*/
function get_town_by_xy(int $x, int $y): array|false
{
$cache_tag = "town-$x-$y";
if (!isset($GLOBALS['cache'][$cache_tag])) {
$query = db()->query('SELECT * FROM towns WHERE longitude = ? AND latitude = ? LIMIT 1;', [$x, $y]);
if ($query === false) return false;
$GLOBALS['cache'][$cache_tag] = $query->fetchArray(SQLITE3_ASSOC);
}
return $GLOBALS['cache'][$cache_tag];
}
/**
* Get a town's data by it's ID.
*/
function get_town_by_id(int $id): array|false
{
$query = db()->query('SELECT * FROM towns WHERE id = ? LIMIT 1;', [$id]);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a user's data by their ID.
*/
function get_user_by_id(int $id): array|false
{
$query = db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$id]);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get an item by it's ID.
*/
function get_item(int $id): array|false
{
$query = db()->query('SELECT * FROM items WHERE id=? LIMIT 1;', [$id]);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a drop by it's ID.
*/
function get_drop(int $id): array|false
{
$query = db()->query('SELECT * FROM drops WHERE id=? LIMIT 1;', [$id]);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a spell by it's ID.
*/
function get_spell(int $id): array|false
{
$query = db()->query('SELECT * FROM spells WHERE id=? LIMIT 1;', [$id]);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Get a monster by it's ID.
*/
function get_monster(int $id): array|false
{
$query = db()->query('SELECT * FROM monsters WHERE id=? LIMIT 1;', [$id]);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
/**
* Translate a Specials keyword to it's string.
*/
function special_to_string(string $special): string
{
return match ($special) {
'maxhp' => 'Max HP',
'maxmp' => 'Max MP',
'maxtp' => 'Max TP',
'goldbonus' => 'Gold Bonus (%)',
'expbonus' => 'Experience Bonus (%)',
'strength' => 'Strength',
'dexterity' => 'Dexterity',
'attackpower' => 'Attack Power',
'defensepower' => 'Defense Power',
default => $special
};
}
/**
* Generate a pretty dope token.
*/
function token($length = 32): string
{
return bin2hex(random_bytes($length));
}
/**
* Validate any given array of data against rules. Returns [valid, data, error]. Data contains the trimmed
* values from the input array. Note: all fields with rules are assumed to be required, unless the optional
* rule is used.
*
* Example: ['required', 'no-trim', 'length:5-20', 'alphanum-spaces']
*/
function validate(array $input_data, array $rules): array
{
$data = [];
$errors = [];
foreach ($rules as $field => $field_rules) {
$value = $input_data[$field] ?? null;
$field_name = ucfirst(str_replace('_', ' ', $field));
$is_required = true;
$default_value = null;
if (in_array('optional', $field_rules)) {
$is_required = false;
}
foreach ($field_rules as $rule) {
if (strpos($rule, 'default:') === 0) {
$default_value = substr($rule, 8);
break;
}
}
if (($value === null || $value === '') && $default_value !== null) {
$value = $default_value;
}
if (($value === null || $value === '') && !$is_required) continue;
if ($is_required && ($value === null || $value === '')) {
$errors[$field][] = "{$field_name} is required.";
continue;
}
if (!in_array('no-trim', $field_rules)) {
$value = trim($value);
}
$data[$field] = $value;
foreach ($field_rules as $rule) {
// Parse rule and arguments
if (strpos($rule, ':') !== false) {
list($rule_name, $rule_args) = explode(':', $rule, 2);
} else {
$rule_name = $rule;
$rule_args = null;
}
if ($rule_name === 'optional') continue;
switch ($rule_name) {
case 'bool':
if (!isset($input_data[$field]) || empty($value)) {
$value = false;
} else {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($value === null) {
$errors[$field][] = "{$field_name} must be a valid boolean value.";
}
}
break;
case 'length':
list($min, $max) = explode('-', $rule_args);
$len = strlen((string)$value);
if ($len < $min || $len > $max) {
$errors[$field][] = "{$field_name} must be between {$min} and {$max} characters.";
}
break;
case 'alphanum':
if (!preg_match('/^[a-zA-Z0-9]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alpha':
if (!preg_match('/^[a-zA-Z]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters and numbers.";
}
break;
case 'alphanum-spaces':
if (!preg_match('/^[a-zA-Z0-9\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'alpha-spaces':
if (!preg_match('/^[a-zA-Z\s_]+$/', $value)) {
$errors[$field][] = "{$field_name} must contain only letters, numbers, spaces, and underscores.";
}
break;
case 'email':
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field][] = "{$field_name} must be a valid email address.";
}
break;
case 'int':
if (!filter_var($value, FILTER_VALIDATE_INT)) {
$errors[$field][] = "{$field_name} must be an integer.";
}
break;
case 'min':
if ($value < $rule_args) {
$errors[$field][] = "{$field_name} must be at least {$rule_args}.";
}
break;
case 'max':
if ($value > $rule_args) {
$errors[$field][] = "{$field_name} must be no more than {$rule_args}.";
}
break;
case 'regex':
if (!preg_match($rule_args, $value)) {
$errors[$field][] = "{$field_name} does not match the required pattern.";
}
break;
case 'in':
$options = explode(',', $rule_args);
if (!in_array($value, $options)) {
$errors[$field][] = "{$field_name} must be one of: " . implode(', ', $options);
}
break;
case 'confirm':
$field_to_confirm = substr($field, 8);
$confirm_value = $data[$field_to_confirm] ?? '';
$confirm_field_name = ucfirst(str_replace('_', ' ', $field_to_confirm));
if ($value !== $confirm_value) {
$errors[$field][] = "{$field_name} must match {$confirm_field_name}.";
}
break;
case 'unique':
list($table, $column) = explode(',', $rule_args, 2);
if (db()->exists($table, $column, $value)) {
$errors[$field][] = "{$field_name} must be unique.";
}
break;
}
}
}
foreach ($input_data as $field => $value) {
if (!isset($data[$field])) $data[$field] = trim($value);
}
return [
'valid' => empty($errors),
'data' => $data,
'errors' => $errors
];
}
/**
* Generates a ul list from `validate()`'s errors.
*/
function ul_from_validate_errors(array $errors): string
{
$string = '<ul>';
foreach ($errors as $field => $errors) {
$string .= '<li>';
foreach ($errors as $error) $string .= $error;
$string .= '</li>';
}
return $string . '</ul>';
}
/**
* Get the URI, broken up into chunks.
*/
function uri(): array
{
return explode('/', trim($_SERVER['REQUEST_URI'], '/'));
}
/**
* Load the environment variables from the .env file.
*/
function env_load(string $filePath): void
{
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(string $key, mixed $default = null): mixed
{
$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
};
}
/**
* Get the data on spells from a given list of IDs.
*/
function get_spells_from_list(array|string $spell_ids): SQLite3Result|false
{
if (is_string($spell_ids)) $spell_ids = explode(',', $spell_ids);
$placeholders = implode(',', array_fill(0, count($spell_ids), '?'));
$query = db()->query("SELECT id, name, type FROM spells WHERE id IN($placeholders)", $spell_ids);
if ($query === false) return false;
return $query;
}
function generate_stat_bar($current, $max)
{
$percent = ($max === 0) ? 0 : ceil($current / $max * 100);
$color = $percent >= 66 ? 'green' : ($percent >= 33 ? 'yellow' : 'red');
return '<div class="stat-bar" style="width: 15px; height: 100px; border: solid 1px black;">' .
'<div style="height: ' . $percent . 'px; background-image: url(/img/bars_' . $color . '.gif);"></div>' .
'</div>';
}
function create_stat_table($userrow)
{
$stat_table = '<div class="stat-table">' .
'<div class="stat-row">' .
'<div class="stat-col">' . generate_stat_bar($userrow['currenthp'], $userrow['maxhp']) . '<div>HP</div></div>' .
'<div class="stat-col">' . generate_stat_bar($userrow['currentmp'], $userrow['maxmp']) . '<div>MP</div></div>' .
'<div class="stat-col">' . generate_stat_bar($userrow['currenttp'], $userrow['maxtp']) . '<div>TP</div></div>' .
'</div>' .
'</div>';
return $stat_table;
}