367 lines
9.1 KiB
PHP
367 lines
9.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Generate a pretty dope token.
|
|
*/
|
|
function token($length = 32): string
|
|
{
|
|
return bin2hex(random_bytes($length));
|
|
}
|
|
|
|
/**
|
|
* Redirect to a new location.
|
|
*/
|
|
function redirect($location): void
|
|
{
|
|
header("Location: $location");
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Flash data to the session, or retrieve an existing flash value. Returns false if the key does not exist.
|
|
*/
|
|
function flash($key, $value = '')
|
|
{
|
|
if ($value === '') return $_SESSION["flash_$key"] ?? false;
|
|
$_SESSION["flash_$key"] = $value;
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Clear a specific flash message.
|
|
*/
|
|
function unflash($key)
|
|
{
|
|
unset($_SESSION["flash_$key"]);
|
|
}
|
|
|
|
/**
|
|
* Clear all flash messages.
|
|
*/
|
|
function clear_flashes()
|
|
{
|
|
foreach ($_SESSION as $key => $_) {
|
|
if (str_starts_with($key, 'flash_')) unset($_SESSION[$key]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a CSRF token.
|
|
*/
|
|
function csrf()
|
|
{
|
|
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = token();
|
|
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.
|
|
*/
|
|
function csrf_field()
|
|
{
|
|
return '<input type="hidden" name="csrf" value="' . csrf() . '">';
|
|
}
|
|
|
|
/**
|
|
* Kill the current request with a 418 error, if $_POST['csrf'] is invalid.
|
|
*/
|
|
function csrf_ensure()
|
|
{
|
|
if (!csrf_verify($_POST['csrf'] ?? '')) error_response(418);
|
|
}
|
|
|
|
/**
|
|
* 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 user's object from SESSION if it exists.
|
|
*/
|
|
function user(): User|false
|
|
{
|
|
if (empty($_SESSION['user'])) return false;
|
|
return unserialize($_SESSION['user']);
|
|
}
|
|
|
|
/**
|
|
* Modify a field in the user session object. Returns true on success and false on failure.
|
|
*/
|
|
function modify_user_session(string $field, mixed $value): bool
|
|
{
|
|
$user = user();
|
|
if ($user === false || !property_exists($user, $field)) return false;
|
|
$user->{$field} = $value;
|
|
$_SESSION['user'] = serialize($user);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* If the current user has a selected char and the data is in the session, retrieve the character object. If there
|
|
* is no character data, populate it.
|
|
*/
|
|
function char(): Character|false
|
|
{
|
|
if (empty($_SESSION['user'])) return false;
|
|
if (empty($GLOBALS['char'])) $GLOBALS['char'] = user()->current_char();
|
|
return $GLOBALS['char'];
|
|
}
|
|
|
|
/**
|
|
* Update the user's selected character. Returns true on success, false on failure. Database
|
|
* is updated if the character ID is different from the current session.
|
|
*/
|
|
function change_user_character(int $char_id): bool
|
|
{
|
|
// If the character does not exist, return false
|
|
if (($char = Character::find($char_id)) === false) return false;
|
|
$GLOBALS['char'] = $char;
|
|
|
|
// If the character ID is different, update the session and database
|
|
if (user()->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]);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get a percent between two ints, rounded to the nearest whole number or return 0.
|
|
*/
|
|
function percent(float $num, float $denom, int $precision = 4): float
|
|
{
|
|
if ($denom === 0) return 0;
|
|
$p = ($num / $denom) * 100;
|
|
return $p < 0 ? 0 : round($p, $precision);
|
|
}
|
|
|
|
/**
|
|
* Access the account wallet. On first execution it will populate $GLOBALS['wallet'] with the wallet data. This way
|
|
* the data is up to date with every request without having to query the database every use within, for example, a
|
|
* template. Will return false if the user or wallet does not exist.
|
|
*/
|
|
function wallet(): Wallet|false
|
|
{
|
|
if (user() === false) return false;
|
|
if (empty($GLOBALS['wallet'])) $w = user()->wallet();
|
|
if ($w === false) return false;
|
|
return $GLOBALS['wallet'] = $w;
|
|
}
|
|
|
|
/**
|
|
* Access the character location. On first execution it will populate $GLOBALS['location'] with the location data. This
|
|
* way the data is up to date with every request without having to query the database every use within, for example, a
|
|
* template. Will return false if the field does not exist, or the entire location array if no field is specified.
|
|
*/
|
|
function location($field = '')
|
|
{
|
|
if (empty($GLOBALS['location'])) {
|
|
$GLOBALS['location'] = db_query(
|
|
db_live(),
|
|
"SELECT * FROM char_locations WHERE char_id = :c",
|
|
[':c' => user()->char_id]
|
|
)->fetchArray(SQLITE3_ASSOC);
|
|
}
|
|
|
|
if ($field === '') return $GLOBALS['location'];
|
|
return $GLOBALS['location'][$field] ?? false;
|
|
}
|
|
|
|
/**
|
|
* Format an array of strings to a ul element.
|
|
*/
|
|
function array_to_ul($array)
|
|
{
|
|
$html = '';
|
|
foreach ($array as $item) $html .= "<li>$item</li>";
|
|
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.
|
|
*/
|
|
function ce(bool $condition, mixed $value, mixed $or = ''): void
|
|
{
|
|
echo $condition ? $value : $or;
|
|
}
|
|
|
|
/**
|
|
* Get whether the request is an HTMX request.
|
|
*/
|
|
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.
|
|
*/
|
|
function json_response(mixed $data): void
|
|
{
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($data);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Handle an error by setting the response code and echoing an error message
|
|
*/
|
|
function error_response(int $code): void
|
|
{
|
|
http_response_code($code);
|
|
echo match ($code) {
|
|
403 => 'Forbidden',
|
|
404 => 'Not Found',
|
|
405 => 'Method Not Allowed',
|
|
418 => 'I\'m a teapot',
|
|
999 => 'Cheating attempt detected',
|
|
default => 'Unknown Error',
|
|
};
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Return a title's [name, lore].
|
|
*/
|
|
function title(int $title_id): array|false
|
|
{
|
|
return db_query(db_live(), 'SELECT * FROM titles WHERE id = ?', [$title_id])->fetchArray();
|
|
}
|
|
|
|
/**
|
|
* Abbreviate a number in text notation.
|
|
*/
|
|
function abb_num(int $num): string
|
|
{
|
|
$d = $num % 100 === 0 ? 0 : 1;
|
|
return match(true) {
|
|
$num >= 1000000000 => number_format($num / 1000000000, $d) . 'B',
|
|
$num >= 1000000 => number_format($num / 1000000, $d) . 'M',
|
|
default => number_format($num, 0)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if all keys of an array are numeric.
|
|
*/
|
|
function all_keys_numeric(array $a): bool
|
|
{
|
|
foreach (array_keys($a) as $k) if (!is_int($k)) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse simple BBCode to HTML, and keep count of characters.
|
|
*/
|
|
function parse_bbcode(string $text): array
|
|
{
|
|
$html = $text;
|
|
|
|
// Store the original text without tags and whitespace for counting
|
|
$textForCounting = preg_replace('/\[.*?\]/', '', $text); // Remove BBCode tags
|
|
$textForCounting = preg_replace('/\s+/', '', $textForCounting); // Remove whitespace
|
|
$charCount = strlen($textForCounting);
|
|
|
|
// Basic BBCode conversions
|
|
$replacements = [
|
|
'/\[b\](.*?)\[\/b\]/is' => '<strong>$1</strong>',
|
|
'/\[i\](.*?)\[\/i\]/is' => '<em>$1</em>',
|
|
'/\[u\](.*?)\[\/u\]/is' => '<u>$1</u>',
|
|
'/\[url=(.*?)\](.*?)\[\/url\]/is' => '<a href="$1">$2</a>',
|
|
'/\[url\](.*?)\[\/url\]/is' => '<a href="$1">$1</a>',
|
|
//'/\[img\](.*?)\[\/img\]/is' => '<img src="$1" alt="" />',
|
|
'/\[size=([1-7])\](.*?)\[\/size\]/is' => '<font size="$1">$2</font>',
|
|
'/\[color=(#[A-F0-9]{6}|[a-z]+)\](.*?)\[\/color\]/is' => '<span style="color:$1">$2</span>',
|
|
'/\[quote\](.*?)\[\/quote\]/is' => '<blockquote>$1</blockquote>',
|
|
'/\[code\](.*?)\[\/code\]/is' => '<pre><code>$1</code></pre>',
|
|
'/\[list\](.*?)\[\/list\]/is' => '<ul>$1</ul>',
|
|
'/\[list=1\](.*?)\[\/list\]/is' => '<ol>$1</ol>',
|
|
'/\[\*\](.*?)(?=\[\*\]|\[\/list\])/is' => '<li>$1</li>'
|
|
];
|
|
|
|
foreach ($replacements as $pattern => $replacement) $html = preg_replace($pattern, $replacement, $html);
|
|
|
|
// Convert newlines to <br> tags
|
|
$html = nl2br($html);
|
|
|
|
return [
|
|
'html' => $html,
|
|
'char_count' => $charCount
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|