logfile = LOGFILE; $this->loadData(); $this->handleRequest(); } private function loadData() { if (!file_exists($this->logfile)) { file_put_contents($this->logfile, ''); return; } $lines = file($this->logfile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $data = explode("\t", $line); if (count($data) >= 10) { $post = [ 'id' => $data[0], 'thread' => $data[1], 'datetime' => $data[2], 'name' => $data[3], 'email' => $data[4], 'title' => $data[5], 'body' => $data[6], 'ip' => $data[7], 'delkey' => $data[8], 'markup' => $data[9] ?? 'waka' ]; if ($post['thread'] == '0') { $this->threads[$post['id']] = $post; } $this->posts[] = $post; } } } private function saveData() { $data = ''; foreach ($this->posts as $post) { $data .= implode("\t", [ $post['id'], $post['thread'], $post['datetime'], $post['name'], $post['email'], $post['title'], $post['body'], $post['ip'], $post['delkey'], $post['markup'] ]) . "\n"; } file_put_contents($this->logfile, $data, LOCK_EX); } private function handleRequest() { $task = $_POST['task'] ?? $_GET['task'] ?? 'default'; if (NIKKI && $task == 'post') { $this->showError('Board is closed'); return; } switch ($task) { case 'post': $this->handlePost(); break; case 'admin': $this->handleAdmin(); break; default: $this->showBoard(); break; } } private function handlePost() { if (!$this->checkRateLimit()) { $this->showError('Please wait ' . RENZOKU . ' seconds between posts'); return; } $thread = $_POST['thread'] ?? '0'; $name = $this->sanitize($_POST['field_a'] ?? 'Nameless'); $email = $this->sanitize($_POST['field_b'] ?? ''); $title = $this->sanitize($_POST['title'] ?? ''); $comment = $_POST['comment'] ?? ''; $markup = $_POST['markup'] ?? MARKUP_DEFAULT; // Spam trap check if (!empty($_POST['name']) || !empty($_POST['link'])) { $this->showError('Spam detected'); return; } if (empty($comment) && $thread == '0' && empty($title)) { $this->showError('Comment and title cannot both be empty'); return; } if ($thread == '0' && empty($title)) { $this->showError('Thread title is required'); return; } $ip = $_SERVER['REMOTE_ADDR']; $delkey = $this->generateDelKey(); $id = $this->generateId(); $datetime = date('Y/m/d H:i'); // Process comment $body = $this->processMarkup($comment, $markup); $post = [ 'id' => $id, 'thread' => $thread, 'datetime' => $datetime, 'name' => $name, 'email' => $email, 'title' => $title, 'body' => $body, 'ip' => $ip, 'delkey' => password_hash($delkey, PASSWORD_DEFAULT), 'markup' => $markup ]; $this->posts[] = $post; if ($thread == '0') { $this->threads[$id] = $post; } $this->pruneOldPosts(); $this->saveData(); $this->setRateLimit(); // Redirect to prevent double posting header('Location: ' . PHP_SELF); exit; } private function handleAdmin() { $pass = $_POST['admin_pass'] ?? ''; $action = $_POST['admin_action'] ?? ''; if ($pass !== ADMIN_PASS) { $this->showError('Invalid admin password'); return; } switch ($action) { case 'delete': $delete_id = $_POST['delete_id'] ?? ''; $this->deletePost($delete_id); break; case 'ban': $ban_ip = $_POST['ban_ip'] ?? ''; $this->banIP($ban_ip); break; } header('Location: ' . PHP_SELF); exit; } private function deletePost($id) { $this->posts = array_filter($this->posts, function($post) use ($id) { return $post['id'] != $id; }); if (isset($this->threads[$id])) { unset($this->threads[$id]); } $this->saveData(); } private function banIP($ip) { $banfile = 'banned_ips.txt'; file_put_contents($banfile, $ip . "\n", FILE_APPEND | LOCK_EX); } private function checkBanned() { $banfile = 'banned_ips.txt'; if (!file_exists($banfile)) return false; $banned_ips = file($banfile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); return in_array($_SERVER['REMOTE_ADDR'], $banned_ips); } private function checkRateLimit() { $ip = $_SERVER['REMOTE_ADDR']; $last_post = $_SESSION['last_post_' . $ip] ?? 0; return (time() - $last_post) >= RENZOKU; } private function setRateLimit() { $ip = $_SERVER['REMOTE_ADDR']; $_SESSION['last_post_' . $ip] = time(); } private function generateId() { return time() . rand(100, 999); } private function generateDelKey() { return substr(md5(uniqid(rand(), true)), 0, 8); } private function sanitize($str) { return htmlspecialchars(trim($str), ENT_QUOTES, 'UTF-8'); } private function processMarkup($text, $markup) { $text = $this->sanitize($text); switch ($markup) { case 'waka': return $this->processWakabaMarkup($text); case 'html': return $this->processHTMLMarkup($text); case 'aa': return $this->processAAMarkup($text); default: return $this->processNoneMarkup($text); } } private function processWakabaMarkup($text) { // Convert line breaks $text = nl2br($text); // Bold **text** $text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text); // Italic *text* $text = preg_replace('/\*(.*?)\*/', '$1', $text); // Greentext $text = preg_replace('/^(>.*)$/m', '$1', $text); // Auto-link URLs $text = preg_replace('/https?:\/\/[^\s<>"]+/', '$0', $text); // Quote links >>123 $text = preg_replace('/>>(\d+)/', '>>$1', $text); return $text; } private function processHTMLMarkup($text) { // Allow limited HTML tags $allowed_tags = '

      • '; return strip_tags($text, $allowed_tags); } private function processAAMarkup($text) { $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); return '
        ' . $text . '
        '; } private function processNoneMarkup($text) { $text = nl2br($text); // Auto-link URLs only $text = preg_replace('/https?:\/\/[^\s<>"]+/', '
        $0', $text); // Quote links $text = preg_replace('/>>(\d+)/', '>>$1', $text); return $text; } private function pruneOldPosts() { if (count($this->posts) > LOG_MAX) { $this->posts = array_slice($this->posts, -LOG_MAX); } // Remove old threads if (count($this->threads) > MAX_THREADS) { $sorted_threads = $this->threads; uasort($sorted_threads, function($a, $b) { return strtotime($a['datetime']) - strtotime($b['datetime']); }); $to_remove = array_slice(array_keys($sorted_threads), 0, count($sorted_threads) - MAX_THREADS); foreach ($to_remove as $thread_id) { unset($this->threads[$thread_id]); $this->posts = array_filter($this->posts, function($post) use ($thread_id) { return $post['thread'] != $thread_id && $post['id'] != $thread_id; }); } } } private function getThreadPosts($thread_id) { return array_filter($this->posts, function($post) use ($thread_id) { return $post['thread'] == $thread_id || $post['id'] == $thread_id; }); } private function getThreadReplies($thread_id) { return array_filter($this->posts, function($post) use ($thread_id) { return $post['thread'] == $thread_id; }); } private function getLastBumpTime($thread_id) { $posts = $this->getThreadPosts($thread_id); $latest = 0; foreach ($posts as $post) { $time = strtotime($post['datetime']); if ($time > $latest) { $latest = $time; } } return $latest; } private function getThreadsByBumpOrder() { $threads_with_bump = []; foreach ($this->threads as $thread_id => $thread) { $threads_with_bump[$thread_id] = [ 'thread' => $thread, 'bump_time' => $this->getLastBumpTime($thread_id) ]; } // Sort by bump time (newest first) uasort($threads_with_bump, function($a, $b) { return $b['bump_time'] - $a['bump_time']; }); return $threads_with_bump; } private function showError($message) { $this->showHeader(); echo '
        ' . $this->sanitize($message) . '
        '; $this->showFooter(); } private function showBoard() { if ($this->checkBanned()) { $this->showError('You have been banned'); return; } $this->showHeader(); $this->showThreadList(); $this->showThreads(); $this->showNewThreadForm(); $this->showFooter(); } private function showHeader() { ?> <?= TITLE ?>

        '; echo '
        '; echo '
        '; // Get threads sorted by bump order $threads_by_bump = $this->getThreadsByBumpOrder(); $bump_order = 1; foreach ($threads_by_bump as $thread_id => $thread_data) { $thread = $thread_data['thread']; $reply_count = count($this->getThreadReplies($thread_id)); echo ''; echo '' . $bump_order . ': '; echo ' ' . ($thread['title'] ?: 'No Title') . ' (' . ($reply_count + 1) . ')'; echo ''; $bump_order++; } echo '
        '; echo ''; echo '
        '; } private function showThreads() { echo '
        '; // Get threads sorted by bump order for display $threads_by_bump = $this->getThreadsByBumpOrder(); $shown_threads = array_slice($threads_by_bump, 0, PAGE_DEF, true); $bump_order = 1; foreach ($shown_threads as $thread_id => $thread_data) { $thread = $thread_data['thread']; $posts = $this->getThreadPosts($thread_id); $reply_count = count($this->getThreadReplies($thread_id)); $sorted_posts = []; foreach ($posts as $post) { $sorted_posts[] = $post; } usort($sorted_posts, function($a, $b) { return strtotime($a['datetime']) - strtotime($b['datetime']); }); echo ''; echo '
        '; echo '

        【' . $bump_order . ':' . $reply_count . '】 ' . $this->sanitize($thread['title']) . '

        '; echo '
        '; echo '
        '; $reply_num = 1; foreach ($sorted_posts as $post) { $this->showPost($post, $reply_num, $thread_id); $reply_num++; } echo '
        '; $this->showReplyForm($thread_id); echo '
        '; $bump_order++; } echo '
        '; } private function showPost($post, $reply_num, $thread_id) { echo '
        '; echo '

        '; echo '' . $reply_num . ' '; echo 'Name: '; if (!empty($post['email'])) { echo '' . $this->sanitize($post['name']) . ''; } else { echo '' . $this->sanitize($post['name']) . ''; } echo ' : ' . $this->sanitize($post['datetime']); // Admin controls if ($_SERVER['REMOTE_ADDR'] === ADMIN_IP) { echo ' [Del]'; } echo '

        '; echo '
        ' . $post['body'] . '
        '; echo '
        '; } private function showReplyForm($thread_id) { ?>
        Name: E-Mail:
        Leave these fields empty (spam trap):

        New thread

        Thread Title:
        Name: E-Mail:
        Leave these fields empty (spam trap):

        Admin Panel