DK2/src/helpers.php
2024-11-13 18:53:05 -08:00

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();
}