__("Error")]); _view("template_navigation_start"); if (!$skipLoginCheck) _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($GLOBALS["db"])]); _view("template_navigation_end"); _view("alert_error", ["message" => $err]); _view("template_end", [...getThemeAndLangInfo()]); } function msg_info(string $msg, bool $skipLoginCheck = false): void { _view("template_start", ["_title" => __("Information")]); _view("template_navigation_start"); if (!$skipLoginCheck) _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($GLOBALS["db"])]); _view("template_navigation_end"); _view("alert_info", ["message" => $msg]); _view("template_end", [...getThemeAndLangInfo()]); } function generateCaptchaText(): string { $phrase = ""; for ($i = 0; $i < CAPTCHA_PHRASE_LENGTH; ++$i) $phrase .= CAPTCHA_CHARSET[random_int(0, strlen(CAPTCHA_CHARSET) - 1)]; return $phrase; } function generatePasswordResetLink(Database &$db, User &$user): ?string { $token = $db->generateId(20); $user->passwordResetToken = $token; $user->passwordResetTokenCreated = new \DateTimeImmutable(); if (!$db->update($user)) { return null; } return env("PUBLIC_URL") . "?_action=pwreset&token=" . urlencode($user->passwordResetToken) . "&sig=" . urlencode(base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true))); } function decodePasswordResetLink(Database &$db, string $token, string $signature): ?User { $user = new User(); $user->passwordResetToken = $token; if (!$db->fetchWhere($user, "password_reset_token")) { return null; } $then = $user->passwordResetTokenCreated; $now = new \DateTimeImmutable(); if (($now->getTimestamp() - $then->getTimestamp()) >= 3600) { return null; } $expectedSignature = base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true)); if ($expectedSignature !== $signature) { return null; } return $user; } function getThemeAndLangInfo(): array { $availableThemes = []; $currentTheme = $_GET["theme"] ?? $_COOKIE["theme"] ?? env("MYSTIC_FORUM_THEME") ?? "default"; foreach (scandir($dir = __DIR__ . '/themes/') as $ent) { if ($ent[0] === "." || !is_dir($dir . "/" . $ent) || !is_file($theme_file = $dir . "/" . $ent . "/theme.json")) continue; $theme_info = json_decode(file_get_contents($theme_file)); $availableThemes[$ent] = $theme_info; } $availableLangs = [ "en" => "English", ]; foreach (i18n_get_available_locales() as $loc) { if (isset($availableLangs[$loc])) continue; $metadata = i18n_metadata($loc); $availableLangs[$loc] = $metadata["langName"] ?? $loc; } return [ "availableThemes" => $availableThemes, "currentTheme" => $currentTheme, "availableLangs" => $availableLangs, "currentLang" => i18n_get_current_locale(), ]; } function _view(string $___NAME, array $___PARAMS = []): void { $___PATH = __DIR__ . "/application/views/" . $___NAME . ".php"; if (!is_file($___PATH)) { echo "\n"; echo "

Failed to include " . htmlentities($___NAME) . "


\n"; echo "\n"; } else { extract($___PARAMS); echo "\n"; include $___PATH; echo "\n"; } } function isTrue(string $str): bool { $str = strtolower($str); return in_array($str, ["yes","true","y","t","on","enabled","1","?1"]); } function reArrayFiles(&$file_post) { $file_ary = []; $file_count = count($file_post['name']); $file_keys = array_keys($file_post); for ($i=0; $i<$file_count; $i++) { if ($file_post["error"][$i] !== UPLOAD_ERR_OK) continue; foreach ($file_keys as $key) { $file_ary[$i][$key] = $file_post[$key][$i]; } } return $file_ary; } const TAGS_REGEX = '/\[(b|i|u|s|\\^|_|spoiler)\](.+?)\[\/\1\]/s'; function expandTags(string $contents): string { return preg_replace_callback(TAGS_REGEX, function(array $m) { $randomId = "target-" . bin2hex(random_bytes(8)); $inner = $m[2]; $tag = [ "b" => ["", "" ], "i" => ["", "" ], "s" => ["", "" ], "^" => ["", "" ], "_" => ["", "" ], "spoiler" => [ "
", "
" ], ][$m[1]] ?? $m[1]; if (preg_match(TAGS_REGEX, $inner)) $inner = expandTags($inner); if (!is_array($tag)) $tag = [ "<$tag>", "" ]; $tagsBefore = $tag[0]; $tagsAfter = $tag[1]; return "{$tagsBefore}{$inner}{$tagsAfter}"; }, $contents); } function renderPost(string $contents): string { $contents = preg_replace('~\R~u', "\n", $contents); $contents = trim($contents); $contents = preg_replace('/\n{3,}/', "\n\n", $contents); $contents = htmlentities($contents); $contents = nl2br($contents, false); $lines = explode("\n", $contents); $contents = ""; $lineBuf = []; $inQuote = false; foreach ($lines as $ln) { if (str_starts_with($ln, "> ")) { if (!$inQuote) { $contents .= implode("\n", $lineBuf); $lineBuf = []; $inQuote = true; } $lineBuf []= substr($ln, 5); } else { if ($inQuote) { $contents .= "
\n" . implode("\n", $lineBuf) . "
\n"; $lineBuf = []; $inQuote = false; if (trim($ln) === "
") continue; } $lineBuf []= $ln; } } if ($inQuote) { $contents .= "
" . implode("\n", $lineBuf) . "
"; } else { $contents .= implode("\n", $lineBuf); } $contents = expandTags($contents); return $contents; } function env(string $key): ?string { $val = getenv($key); if ($val === false) return null; return $val; } require_once __DIR__ . "/vendor/autoload.php"; require_once __DIR__ . "/application/i18n.php"; if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["lang"])) { parse_str($_SERVER["QUERY_STRING"], $query); if (empty($query["lang"])) { setcookie("lang", "", 100); } else { setcookie("lang", $query["lang"], time()+60*60*24*30); } unset($query["lang"]); $path = strtok($_SERVER["REQUEST_URI"], "?"); header("Location: $path?" . http_build_query($query)); exit; } $user_locale = env("LOCALE") ?? $_COOKIE["lang"] ?? locale_accept_from_http($_SERVER["HTTP_ACCEPT_LANGUAGE"] ?? ""); $chosen_locale = locale_lookup(i18n_get_available_locales(), $user_locale, true, "en"); i18n_locale($user_locale); $db = null; try { $db = new Database(Database::getConnectionString("db", getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD"), getenv("POSTGRES_DBNAME"))); } catch (DatabaseConnectionException $ex) { msg_error( __("Failed to connect to database:\n%details%", [ "details" => $ex->getMessage(), ]), true ); exit; } $GLOBALS["db"] = &$db; $db->ensureTable(User::class); $db->ensureTable(Topic::class); $db->ensureTable(Post::class); $db->ensureTable(Attachment::class); $db->ensureTable(TopicLogMessage::class); $superuser = new User(); $superuser->id = "SUPERUSER"; if (!$db->fetch($superuser)) { $superUserPassword = base64_encode(random_bytes(12)); $suEmail = env("MYSTIC_FORUM_SUPERUSER_EMAIL"); if ($suEmail === null) { http_response_code(500); Messaging::error("No superuser email defined. Please set the 'MYSTIC_FORUM_SUPERUSER_EMAIL' environment variable accordingly"); exit; } $superuser->name = "superuser"; $superuser->email = $suEmail; $superuser->passwordHash = password_hash($superUserPassword, PASSWORD_DEFAULT); $superuser->displayName = "SuperUser"; $superuser->created = new \DateTimeImmutable(); $superuser->permissionMask = PHP_INT_MAX; $superuser->passwordResetRequired = false; $superuser->activated = true; $superuser->activationToken = ""; $db->insert($superuser); Messaging::info([ Messaging::bold("Superuser account created"), [ "Username" => $superuser->name, "Password" => $superUserPassword, ], "Please note that the password can only be shown this time, so please note it down!", ]); exit; } $currentUser = RequestUtils::getAuthorizedUser($db); $GLOBALS["currentUser"] = &$currentUser; // initialization finished function invalid_action(string $_action): never { http_response_code(404); msg_error(__("Invalid or unknown action \"%action%\"", [ "action" => $_action, ])); exit; } if ($_action !== null && !preg_match('/^[a-z][a-z0-9_]*$/', $_action)) invalid_action($_action); $_action ??= "_default"; $actionDir = __DIR__ . "/application/actions/$_action"; if (!is_dir($actionDir)) invalid_action($_action); $actionMethod = strtolower(preg_replace('/[^A-Za-z]/', '', $_rq_method)); if (is_file($commonFile = $actionDir . "/_common.php")) include $commonFile; if (is_file($anyFile = $actionDir . "/_any.php")) include $anyFile; elseif (is_file($methodFile = $actionDir . "/$actionMethod.php")) include $methodFile; else { http_response_code(405); Messaging::error("Invalid request method " . RequestUtils::getRequestMethod()); }