$err,
"skipLoginCheck" => $skipLoginCheck,
]);
}
function msg_info(string $msg, bool $skipLoginCheck = false): void {
render("info_page.twig", [
"message" => $msg,
"skipLoginCheck" => $skipLoginCheck,
]);
}
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 = __ROOT__ . '/application/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 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;
}
function render(string $page, array $context = [], array $functions = []): void {
$defaultTemplate = "bootstrap-3"; // bootstrap-3 is the default for backwards compatibility
$templateDir = __ROOT__ . "/application/templates";
[
"availableThemes" => $availableThemes,
"currentTheme" => $currentTheme,
] = getThemeAndLangInfo();
$currentThemeInfo = $availableThemes[$currentTheme] ?? (object)[];
$currentTemplate = $currentThemeInfo->template ?? $defaultTemplate;
$availableTemplates = array_values(array_filter(scandir($templateDir), function($theme) use ($templateDir) {
return $theme[0] !== "." && is_dir($templateDir . "/" . $theme);
}));
if (!in_array($currentTemplate, $availableTemplates))
$currentTemplate = $defaultTemplate;
$loader = new FilesystemLoader([
$templateDir . "/" . $currentTemplate,
]);
$twig = new Environment($loader, [
// TODO Enable caching
]);
$twig->addFunction(new TwigFunction("__", __(...), [ "is_safe" => ["html"] ]));
$twig->addFunction(new TwigFunction("___", ___(...), [ "is_safe" => ["html"] ]));
$twig->addFunction(new TwigFunction("renderPost", renderPost(...), [ "is_safe" => ["html"] ]));
$twig->addFunction(new TwigFunction("renderPostSummary", renderPostSummary(...), [ "is_safe" => ["html"] ]));
$twig->addFunction(new TwigFunction("permission", fn(string $name): int => constant(UserPermissions::class . "::" . $name)));
$twig->addFunction(new TwigFunction("getAndClearFormError", RequestUtils::getAndClearFormError(...)));
$twig->addFunction(new TwigFunction("lastFormField", function(string $formId, string $field): ?string {
$lastFormId = "";
$lastForm = RequestUtils::getLastForm($lastFormId) ?? null;
if ($lastForm === null || $lastFormId !== $formId)
return null;
return $lastForm[$field] ?? null;
}));
$twig->addFilter(new TwigFilter("hash", fn(string $data, string $algo, bool $binary = false, array $options = []) => hash($algo, $data, $binary, $options)));
$twig->addFilter(new TwigFilter("base64_encode", base64_encode(...)));
$twig->addFilter(new TwigFilter("base64_decode", base64_decode(...)));
foreach ($functions as $fnName => $callable)
$twig->addFunction(new TwigFunction("ctx_$fnName", $callable));
echo $twig->render($page, [
"g" => [
"get" => &$_GET,
"post" => &$_POST,
"request" => &$_REQUEST,
"session" => &$_SESSION,
"cookie" => &$_COOKIE,
"env" => &$_ENV,
"server" => &$_SERVER,
"globals" => $GLOBALS,
],
"ctx" => &$context,
"currentUser" => &$GLOBALS["currentUser"],
...getThemeAndLangInfo(),
]);
RequestUtils::clearLastForm();
}
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" => [ "
\n" . implode("\n", $lineBuf) . "\n"; $lineBuf = []; $inQuote = false; if (trim($ln) === "
" . 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()); }