0, 'path' => '/', 'domain' => '', 'secure' => $secureCookies, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); // ------------------------------ // Vendor autoload (Markdown + Sanitizer) // ------------------------------ require_once __DIR__ . '/vendor/autoload.php'; // Lightweight JSON responder function json_out(array $data, int $code = 200): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } // CSRF token helpers function csrf_token(): string { if (empty($_SESSION['__csrf'])) { $_SESSION['__csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['__csrf']; } function require_csrf(): void { $t = $_POST['csrf'] ?? ''; if (!hash_equals($_SESSION['__csrf'] ?? '', (string)$t)) { json_out(['ok' => false, 'error' => 'CSRF token invalid'], 419); } } // ------------------------------ // 1) Backend: LLM function stub // ------------------------------ if (!function_exists('llm')) { /** * LLM call * @param string $prompt * @param string|null $system_message * @param array $previousConversation */ require_once __DIR__."/../LlamaCli.func.php"; function llm(string $prompt, ?string $system_message = "You are a helpful assistant", array $previousConversation = []): string { $opts = []; if (!empty($previousConversation)) { // Optionally cap context length to the last N turns $N = 20; if (count($previousConversation) > $N) { $previousConversation = array_slice($previousConversation, -$N); } $opts['previousConversation'] = $previousConversation; } return LlamaCli_raw($prompt, $system_message ?? "", $opts); } } // ------------------------------ // Markdown -> HTML + sanitize // ------------------------------ /** @return array{html:string, used_sanitizer:bool} */ function md_to_safe_html(string $markdown): array { $html = $markdown; $used = false; // Convert Markdown to HTML (GitHub-flavored if available) if (class_exists(League\CommonMark\GithubFlavoredMarkdownConverter::class)) { $converter = new League\CommonMark\GithubFlavoredMarkdownConverter([ 'html_input' => 'strip', // ignore raw HTML from model 'allow_unsafe_links' => false, 'max_nesting_level' => 20, ]); $html = (string)$converter->convert($markdown); } else { // Fallback: escape ->
 (so we never inject unsafe HTML)
        //$html = '
'.htmlspecialchars($markdown, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').'
'; $html = '
ERROR: GitHubFlavoredMarkdownConverter not available
'; } // Sanitize HTML if (class_exists(\HTMLPurifier::class)) { $config = \HTMLPurifier_Config::createDefault(); // allow common formatting + code + tables + kbd $config->set('Cache.DefinitionImpl', null); $config->set('HTML.Allowed', implode(',', [ 'p','br','hr','blockquote','strong','em','del','ins','u','s','sup','sub','kbd', 'pre','code','span','ol','ul','li', 'h1','h2','h3','h4','h5','h6', 'table','thead','tbody','tfoot','tr','th','td', 'a[href|title|target]','img[src|alt|title|width|height]', ])); $config->set('URI.SafeIframeRegexp', '%^https?://%'); // if you ever allow iframes later $config->set('Attr.AllowedFrameTargets', ['_blank']); $config->set('AutoFormat.AutoParagraph', false); $purifier = new \HTMLPurifier($config); $html = $purifier->purify($html); $used = true; } return ['html' => $html, 'used_sanitizer' => $used]; } // -------------------------------- // 2) Helpers for chat persistence // -------------------------------- const SESSION_KEY = 'mini_chatgpt_history'; if (!isset($_SESSION[SESSION_KEY])) { $_SESSION[SESSION_KEY] = []; } /** * @return array */ function chat_history(): array { return $_SESSION[SESSION_KEY]; } /** * @param array $extra */ function chat_append(string $role, string $content, array $extra = []): void { $_SESSION[SESSION_KEY][] = array_merge([ 'role' => $role, 'content' => $content, 'ts' => time(), ], $extra); } function chat_reset(): void { $_SESSION[SESSION_KEY] = []; } // ------------------------------ // 3) AJAX endpoints (POST only) // ------------------------------ if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_csrf(); $action = $_POST['action'] ?? ''; // Reset if ($action === 'reset') { chat_reset(); json_out(['ok' => true]); } // Theme change if ($action === 'set_theme') { $theme = $_POST['theme'] ?? ''; if (!in_array($theme, ['light','dark'], true)) { json_out(['ok' => false, 'error' => 'Invalid theme'], 422); } $_SESSION['__theme'] = $theme; json_out(['ok' => true, 'theme' => $theme]); } // Chat if ($action === 'chat') { $message = trim((string)($_POST['message'] ?? '')); $system = isset($_POST['system']) ? trim((string)$_POST['system']) : null; if ($message === '') { json_out(['ok' => false, 'error' => 'Empty message'], 422); } if (mb_strlen($message) > 4000) { json_out(['ok' => false, 'error' => 'Message too long'], 413); } if ($system !== null && mb_strlen($system) > 4000) { json_out(['ok' => false, 'error' => 'System too long'], 413); } // Persist optional system prompt if ($system !== null && $system !== '') { $_SESSION['__system_prompt'] = $system; } $effectiveSystem = $_SESSION['__system_prompt'] ?? ($system ?: null); // Build previousConversation from existing session history $historyRaw = chat_history(); $previousConversation = []; foreach ($historyRaw as $turn) { if (!isset($turn['role'], $turn['content'])) continue; if ($turn['role'] !== 'user' && $turn['role'] !== 'assistant') continue; $previousConversation[] = [ 'role' => $turn['role'], 'content' => (string)$turn['content'], ]; } // Record current user message chat_append('user', $message); // Ask the model $reply_raw = llm($message, $effectiveSystem, $previousConversation); // Convert markdown -> sanitized HTML $md = md_to_safe_html($reply_raw); $reply_html = $md['html']; // Save assistant reply (store both raw + html) chat_append('assistant', $reply_raw, ['content_html' => $reply_html]); json_out([ 'ok' => true, 'reply' => $reply_raw, 'reply_html'=> $reply_html, 'history' => chat_history(), ]); } json_out(['ok' => false, 'error' => 'Unknown action'], 400); } // ------------------------------ // 4) HTML (Bootstrap 5) // ------------------------------ $history = chat_history(); $systemPrefill = htmlspecialchars($_SESSION['__system_prompt'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $theme = htmlspecialchars($_SESSION['__theme'] ?? 'dark', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $csrf = csrf_token();