$_) { 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 ''; } /** * Kill the current request with a 418 error, if $_POST['csrf'] is invalid. */ function csrf_ensure() { if (!csrf_verify($_POST['csrf'] ?? '')) router_error(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 array from SESSION if it exists. Specify a key to get a specific value. */ function user($field = '') { if (empty($_SESSION['user'])) return false; if ($field === '') return $_SESSION['user']; return $_SESSION['user'][$field] ?? false; } /** * Check whether the user has selected a character. */ function user_selected_char() { return user('char_id') > 0 ? true : false; } /** * If the current user has a selected char and the data is in the session, retrieve either the full array of data * or a specific field. If there is no character data, populate it. */ function char($field = '') { // If there is no user, return false if (empty($_SESSION['user'])) return false; if (empty($GLOBALS['char'])) { $GLOBALS['char'] = db_query( db_live(), "SELECT * FROM characters WHERE id = :c", [':c' => user('char_id')] )->fetchArray(SQLITE3_ASSOC); } if ($field === '') return $GLOBALS['char']; return $GLOBALS['char'][$field] ?? false; } /** * Shorthand to 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($char_id) { // If the character does not exist, return false if (($char = char_find($char_id)) === false) return false; $GLOBALS['char'] = $char; // If the character ID is different, update the session and database if ($_SESSION['user']['char_id'] !== $char_id) { $_SESSION['user']['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($num, $denom, $precision = 4): int { 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 field does not exist, or the entire wallet array if no field is specified. */ function wallet($field = '') { if (empty($GLOBALS['wallet'])) { $GLOBALS['wallet'] = db_query( db_live(), "SELECT * FROM wallets WHERE user_id = :u", [':u' => user('id')] )->fetchArray(SQLITE3_ASSOC); } if ($field === '') return $GLOBALS['wallet']; return $GLOBALS['wallet'][$field] ?? false; } /** * 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 .= "
  • $item
  • "; return ""; } /** * 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($key) { if (!env('debug', false)) return; if (empty($GLOBALS['stopwatch'][$key])) return 0; 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($condition, $value, $or = '') { echo $condition ? $value : $or; } /** * Get whether the request is an HTMX request. */ function is_htmx() { return isset($_SERVER['HTTP_HX_REQUEST']); } /** * Get whether the request is an AJAX (fetch) request. */ function is_ajax() { return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; } /** * Limit a request to AJAX only. */ function ajax_only() { if (!is_ajax()) router_error(418); } /** * Return a JSON response with the given data. */ function json_response($data) { header('Content-Type: application/json; charset=utf-8'); echo json_encode($data); exit; } /** * Return a title's [name, lore]. */ function title($title_id) { return db_query(db_live(), 'SELECT * FROM titles WHERE id = :i', [':i' => $title_id])->fetchArray(); }