$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,
    ]);
    $twigEnvConfig = [];
    if (!DEBUG) {
        $twigEnvConfig["cache"] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "mybb-template-cache";
    }
    $twig = new Environment($loader, $twigEnvConfig);
    $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("getQueryCount", $GLOBALS["db"]->getQueryCount(...)));
    $twig->addFunction(new TwigFunction("getRunTime", fn() => microtime(true) - $GLOBALS["startTime"]));
    $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"; require_once __DIR__ . "/application/common.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 = get_db(); } 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); $db->ensureTable(PendingEmail::class); $db->ensureTable(Subscription::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()); }