Paste.php example

PHP Detected boxlabs 88 Views Size: 45.96 KB Posted on: Sep 2, 25 @ 9:18 PM
  1. <?php
  2. /*
  3.  * Paste $v3.2 2025/09/01 https://github.com/boxlabss/PASTE
  4.  * demo: https://paste.boxlabs.uk/
  5.  *
  6.  * https://phpaste.sourceforge.io/
  7.  *
  8.  * This program is free software; you can redistribute it and/or
  9.  * modify it under the terms of the GNU General Public License
  10.  * as published by the Free Software Foundation; either version 3
  11.  * of the License, or (at your option) any later version.
  12.  *
  13.  * This program is distributed in the hope that it will be useful,
  14.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  16.  * GNU General Public License in LICENCE for more details.
  17.  */
  18. declare(strict_types=1);
  19.  
  20. require_once 'includes/session.php';
  21. require_once 'config.php';
  22.  
  23. // Load highlighter engine libs conditionally
  24. if (($highlighter ?? 'geshi') === 'geshi') {
  25.     require_once 'includes/geshi.php';
  26. } else {
  27.     require_once __DIR__ . '/includes/hlbootstrap.php';
  28. }
  29.  
  30. require_once 'includes/functions.php';
  31.  
  32. // ensure these are visible to all included templates (header/footer/sidebar)
  33. global $pdo, $mod_rewrite;
  34.  
  35. // default to avoid notices if config hasn't set it (DB can override later)
  36. if (!isset($mod_rewrite)) {
  37.     $mod_rewrite = '0';
  38. }
  39.  
  40. $path             = 'includes/geshi/';                        // GeSHi language files
  41. $parsedown_path   = 'includes/Parsedown/Parsedown.php';       // Markdown
  42. $ges_style        = '';                                       // no inline CSS injection
  43. $require_password = false; // errors.php shows password box when true
  44.  
  45. // ---------------- Helpers ----------------
  46.  
  47. // --- highlight theme override (?theme= or ?highlight=) ---
  48. if (($highlighter ?? 'geshi') === 'highlight') {
  49.     $param = $_GET['theme'] ?? $_GET['highlight'] ?? null;
  50.     if ($param !== null) {
  51.         // normalize: accept "dracula", "dracula.css", or "atelier estuary dark"
  52.         $t = strtolower((string)$param);
  53.         $t = str_replace(['+', ' ', '_'], '-', $t);
  54.         $t = preg_replace('~\.css$~', '', $t);
  55.         $t = preg_replace('~[^a-z0-9.-]~', '', $t);
  56.  
  57.         $stylesRel = 'includes/Highlight/styles';
  58.         $fs = __DIR__ . '/' . $stylesRel . '/' . $t . '.css';
  59.         if (is_file($fs)) {
  60.             // header.php will read this to seed the initial <link>
  61.             $hl_style = $t . '.css';
  62.         }
  63.     }
  64. }
  65.  
  66. // Map some GeSHi-style names to highlight.js ids
  67. function map_to_hl_lang(string $code): string {
  68.     static $map = [
  69.         'text'        => 'plaintext',
  70.         'html5'       => 'xml',
  71.         'html4strict' => 'xml',
  72.         'php-brief'   => 'php',
  73.         'pycon'       => 'python',
  74.         'postgresql'  => 'pgsql',
  75.         'dos'         => 'dos',
  76.         'vb'          => 'vbnet',
  77.     ];
  78.     $code = strtolower($code);
  79.     return $map[$code] ?? $code;
  80. }
  81.  
  82. // Wrap hljs tokens in a line-numbered <ol> so togglev() works
  83. function hl_wrap_with_lines(string $value, string $hlLang, array $highlight_lines): string {
  84.     $lines  = explode("\n", $value);
  85.     $digits = max(2, strlen((string) count($lines))); // how many digits do we need?
  86.     $hlset  = $highlight_lines ? array_flip($highlight_lines) : [];
  87.  
  88.     $out   = [];
  89.     $out[] = '<pre class="hljs"><code class="hljs language-' . htmlspecialchars($hlLang, ENT_QUOTES, 'UTF-8') . '">';
  90.     $out[] = '<ol class="hljs-ln" style="--ln-digits:' . (int)$digits . '">';
  91.     foreach ($lines as $i => $lineHtml) {
  92.         $ln  = $i + 1;
  93.         $cls = isset($hlset[$ln]) ? ' class="hljs-ln-line hljs-hl"' : ' class="hljs-ln-line"';
  94.         $out[] = '<li' . $cls . '><span class="hljs-ln-n">' . $ln . '</span><span class="hljs-ln-c">' . $lineHtml . '</span></li>';
  95.     }
  96.     $out[] = '</ol></code></pre>';
  97.     return implode('', $out);
  98. }
  99.  
  100. // Add a class to specific <li> lines in GeSHi output (no inline styles)
  101. function geshi_add_line_highlight_class(string $html, array $highlight_lines, string $class = 'hljs-hl'): string {
  102.     if (!$highlight_lines) return $html;
  103.     $targets = array_flip($highlight_lines);
  104.     $i = 0;
  105.     return preg_replace_callback('/<li\b([^>]*)>/', static function($m) use (&$i, $targets, $class) {
  106.         $i++;
  107.         $attrs = $m[1];
  108.         if (!isset($targets[$i])) return '<li' . $attrs . '>';
  109.         if (preg_match('/\bclass="([^"]*)"/i', $attrs, $cm)) {
  110.             $new = trim($cm[1] . ' ' . $class);
  111.             $attrs = preg_replace('/\bclass="[^"]*"/i', 'class="' . htmlspecialchars($new, ENT_QUOTES, 'UTF-8') . '"', $attrs, 1);
  112.         } else {
  113.             $attrs .= ' class="' . htmlspecialchars($class, ENT_QUOTES, 'UTF-8') . '"';
  114.         }
  115.         return '<li' . $attrs . '>';
  116.     }, $html) ?? $html;
  117. }
  118.  
  119. // ---------- Explain how autodetect made a decision ----------
  120. function paste_build_explain(string $source, string $title, string $sample, string $lang): string {
  121.     $sample = (string) $sample;
  122.     if ($sample !== '') $sample = substr($sample, 0, 2048);
  123.     $lang   = strtolower($lang);
  124.     $ext    = strtolower((string) pathinfo((string)$title, PATHINFO_EXTENSION));
  125.  
  126.     switch ($source) {
  127.         case 'php-tag':
  128.             $phpCount  = preg_match_all('/<\?(php|=)/i', $sample, $m1);
  129.             return "Found PHP opening tag(s) (" . (int)$phpCount . ") such as '<?php' or '<?='. Locked to PHP.";
  130.         case 'filename':
  131.             return $ext ? "Paste title ends with .{$ext}. Mapped extension → {$lang}." : "Filename hint used. Mapped to {$lang}.";
  132.         case 'shebang':
  133.             if (preg_match('/^#![^\r\n]+/m', $sample, $m)) return "Shebang line detected: {$m[0]}{$lang}.";
  134.             return "Shebang detected → {$lang}.";
  135.         case 'modeline':
  136.             if (preg_match('/-\*-\s*mode:\s*([a-z0-9#+-]+)\s*;?/i', $sample, $m)) return "Emacs modeline ‘mode: {$m[1]}’ → {$lang}.";
  137.             if (preg_match('/(?:^|\n)[ \t]*(?:vi|vim):[^\n]*\bfiletype=([a-z0-9#+-]+)/i', $sample, $m)) return "Vim modeline ‘filetype={$m[1]}’ → {$lang}.";
  138.             return "Editor modeline matched → {$lang}.";
  139.         case 'fence':
  140.             if (preg_match('/```([a-z0-9#+._-]{1,32})/i', $sample, $m)) return "Markdown code fence language tag ‘{$m[1]}’ → {$lang}.";
  141.             return "Markdown code fence language tag → {$lang}.";
  142.         case 'markdown': {
  143.             $h = preg_match_all('/^(?:#{1,6}\s|>|-{3,}\s*$|\*\s|\d+\.\s|\[.+?\]\(.+?\))/m', $sample);
  144.             return "Markdown structure detected (headings/lists/links: ~{$h} signals). Rendered as Markdown.";
  145.         }
  146.         case 'heuristic': {
  147.             if (in_array($lang, ['yaml','yml'], true)) {
  148.                 $kv   = preg_match_all('/^[ \t\-]*[A-Za-z0-9_.-]+:\s/m', $sample);
  149.                 $list = preg_match_all('/^[ \t]*-\s/m', $sample);
  150.                 return "YAML heuristics: key:value pairs (~{$kv}) and list items (~{$list}).";
  151.             }
  152.             if (in_array($lang, ['sql','pgsql','mysql','tsql'], true)) {
  153.                 $kw = preg_match_all('/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|FROM|WHERE|JOIN)\b/i', $sample);
  154.                 return "SQL keywords matched (~{$kw}). Guessed {$lang}.";
  155.             }
  156.             if ($lang === 'makefile' || $lang === 'make') {
  157.                 $t = preg_match_all('/^[A-Za-z0-9_.-]+:.*$/m', $sample);
  158.                 return "Makefile targets detected (~{$t}).";
  159.             }
  160.             if ($lang === 'dos' || $lang === 'batch') {
  161.                 $b = preg_match_all('/^\s*(?:@?echo|rem|set|call|goto)\b/i', $sample);
  162.                 return "Batch/DOS commands matched (~{$b}).";
  163.             }
  164.             return "Heuristic rules matched characteristic tokens for {$lang}.";
  165.         }
  166.         case 'hljs':
  167.         case 'hljs-auto':
  168.             return "highlight.php auto-guess selected ‘{$lang}’ after other hints were inconclusive.";
  169.         case 'fallback':
  170.             return "Generic fallback selected a safe default (‘{$lang}’).";
  171.         case 'explicit':
  172.             return "Language was explicitly chosen by the author.";
  173.         default:
  174.             return ucfirst($source) . " → {$lang}.";
  175.     }
  176. }
  177.  
  178. // ---------- autodetect: filename/extension hint ----------
  179. function paste_pick_from_filename(?string $title, string $engine = 'highlight', $hl = null): ?string {
  180.     $title = (string) ($title ?? '');
  181.     $base  = trim(basename($title));
  182.     if ($base === '') return null;
  183.  
  184.     // No extension cases
  185.     if (strcasecmp($base, 'Makefile') === 0) return ($engine === 'geshi') ? 'make' : 'makefile';
  186.     if (strcasecmp($base, 'Dockerfile') === 0) return 'dockerfile';
  187.  
  188.     $ext = strtolower((string) pathinfo($base, PATHINFO_EXTENSION));
  189.     if ($ext === '') return null;
  190.  
  191.     static $ext2hl = [
  192.         'php'=>'php','phtml'=>'php','inc'=>'php',
  193.         'js'=>'javascript','mjs'=>'javascript','cjs'=>'javascript',
  194.         'ts'=>'typescript','tsx'=>'typescript','jsx'=>'javascript',
  195.         'py'=>'python','rb'=>'ruby','pl'=>'perl','pm'=>'perl','raku'=>'perl',
  196.         'sh'=>'bash','bash'=>'bash','zsh'=>'bash','ksh'=>'bash',
  197.         'ps1'=>'powershell',
  198.         'c'=>'c','h'=>'c','i'=>'c',
  199.         'cpp'=>'cpp','cxx'=>'cpp','cc'=>'cpp','hpp'=>'cpp','hxx'=>'cpp','hh'=>'cpp',
  200.         'cs'=>'csharp','java'=>'java','go'=>'go','rs'=>'rust','kt'=>'kotlin','kts'=>'kotlin',
  201.         'm'=>'objectivec','mm'=>'objectivec',
  202.         'json'=>'json','yml'=>'yaml','yaml'=>'yaml','ini'=>'ini','toml'=>'toml','properties'=>'properties',
  203.         'xml'=>'xml','xhtml'=>'xml','xq'=>'xquery','xqy'=>'xquery',
  204.         'html'=>'xml','htm'=>'xml','svg'=>'xml',
  205.         'md'=>'markdown','markdown'=>'markdown',
  206.         'sql'=>'sql','psql'=>'pgsql',
  207.         'bat'=>'dos','cmd'=>'dos','nginx'=>'nginx','conf'=>'ini','cfg'=>'ini','txt'=>'plaintext',
  208.         'make'=>'makefile','mk'=>'makefile','mak'=>'makefile','gradle'=>'gradle','dockerfile'=>'dockerfile'
  209.     ];
  210.  
  211.     $hlId = $ext2hl[$ext] ?? null;
  212.     if ($hlId === null) return null;
  213.  
  214.     if ($engine === 'highlight') {
  215.         if ($hl && method_exists($hl, 'listLanguages')) {
  216.             $set = array_map('strtolower', (array)$hl->listLanguages());
  217.             if (in_array($hlId, $set, true)) return $hlId;
  218.             if ($hlId === 'pgsql' && in_array('sql', $set, true)) return 'sql';
  219.             if ($hlId === 'plaintext' && in_array('text', $set, true)) return 'text';
  220.             return null;
  221.         }
  222.         return $hlId;
  223.     }
  224.  
  225.     // GeSHi: normalize if helper exists
  226.     if (function_exists('paste_normalize_lang')) {
  227.         $g = paste_normalize_lang($hlId, 'geshi', null);
  228.         return $g ?: null;
  229.     }
  230.     return null;
  231. }
  232.  
  233. function is_probable_php_tag(string $code): bool {
  234.     return (bool) preg_match('/<\?(php|=)/i', $code);
  235. }
  236.  
  237. // --- Safe themed error renderers (header -> errors -> footer) ---
  238. function themed_error_render(string $msg, int $http_code = 404, bool $show_password_form = false): void {
  239.     global $default_theme, $lang, $baseurl, $site_name, $pdo, $mod_rewrite, $require_password, $paste_id;
  240.  
  241.     $site_name   = $site_name   ?? '';
  242.     $p_title     = $lang['error'] ?? 'Error';
  243.     $enablegoog  = 'no';
  244.     $enablefb    = 'no';
  245.  
  246.     if (!headers_sent()) {
  247.         http_response_code($http_code);
  248.         header('Content-Type: text/html; charset=utf-8');
  249.     }
  250.  
  251.     $require_password = $show_password_form;
  252.     $error = $msg;
  253.  
  254.     $theme = 'theme/' . htmlspecialchars($default_theme ?? 'default', ENT_QUOTES, 'UTF-8');
  255.     require_once $theme . '/header.php';
  256.     require_once $theme . '/errors.php';
  257.     require_once $theme . '/footer.php';
  258.     exit;
  259. }
  260.  
  261. function render_error_and_exit(string $msg, string $http = '404'): void {
  262.     $code = ($http === '403') ? 403 : 404;
  263.     themed_error_render($msg, $code, false);
  264. }
  265.  
  266. function render_password_required_and_exit(string $msg): void {
  267.     themed_error_render($msg, 403, true);
  268. }
  269.  
  270. // --- Inputs ---
  271. $p_password = '';
  272. $paste_id   = null;
  273. if (isset($_GET['id']) && $_GET['id'] !== '') {
  274.     $paste_id = (int) trim((string) $_GET['id']);
  275. } elseif (isset($_POST['id']) && $_POST['id'] !== '') {
  276.     $paste_id = (int) trim((string) $_POST['id']);
  277. }
  278.  
  279. try {
  280.     // site_info
  281.     $stmt = $pdo->query("SELECT * FROM site_info WHERE id='1'");
  282.     $si   = $stmt->fetch() ?: [];
  283.     $title       = trim($si['title'] ?? '');
  284.     $des         = trim($si['des'] ?? '');
  285.     $baseurl     = rtrim(trim($si['baseurl'] ?? ''), '/') . '/';
  286.     $keyword     = trim($si['keyword'] ?? '');
  287.     $site_name   = trim($si['site_name'] ?? '');
  288.     $email       = trim($si['email'] ?? '');
  289.     $twit        = trim($si['twit'] ?? '');
  290.     $face        = trim($si['face'] ?? '');
  291.     $gplus       = trim($si['gplus'] ?? '');
  292.     $ga          = trim($si['ga'] ?? '');
  293.     $additional_scripts = trim($si['additional_scripts'] ?? '');
  294.  
  295.     // allow DB to define mod_rewrite
  296.     if (isset($si['mod_rewrite']) && $si['mod_rewrite'] !== '') {
  297.         $mod_rewrite = (string) $si['mod_rewrite'];
  298.     }
  299.  
  300.     // interface
  301.     $stmt = $pdo->query("SELECT * FROM interface WHERE id='1'");
  302.     $iface = $stmt->fetch() ?: [];
  303.     $default_lang  = trim($iface['lang'] ?? 'en.php');
  304.     $default_theme = trim($iface['theme'] ?? 'default');
  305.     require_once("langs/$default_lang");
  306.  
  307.     // ban check
  308.     $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
  309.     if (is_banned($pdo, $ip)) {
  310.         render_error_and_exit($lang['banned'] ?? 'You are banned from this site.', '403');
  311.     }
  312.  
  313.     // site permissions
  314.     $stmt = $pdo->query("SELECT * FROM site_permissions WHERE id='1'");
  315.     $perm = $stmt->fetch() ?: [];
  316.     $disableguest = trim($perm['disableguest'] ?? 'off');
  317.     $siteprivate  = trim($perm['siteprivate'] ?? 'off');
  318.     if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $siteprivate === "on") {
  319.         $privatesite = "on";
  320.     }
  321.  
  322.     // page views (best effort)
  323.     $date = date('Y-m-d');
  324.     try {
  325.         $stmt = $pdo->prepare("SELECT id, tpage, tvisit FROM page_view WHERE date = ?");
  326.         $stmt->execute([$date]);
  327.         $pv = $stmt->fetch();
  328.         if ($pv) {
  329.             $page_view_id = (int) $pv['id'];
  330.             $tpage  = (int) $pv['tpage'] + 1;
  331.             $tvisit = (int) $pv['tvisit'];
  332.  
  333.             $stmt = $pdo->prepare("SELECT COUNT(*) FROM visitor_ips WHERE ip = ? AND visit_date = ?");
  334.             $stmt->execute([$ip, $date]);
  335.             if ((int) $stmt->fetchColumn() === 0) {
  336.                 $tvisit++;
  337.                 $stmt = $pdo->prepare("INSERT INTO visitor_ips (ip, visit_date) VALUES (?, ?)");
  338.                 $stmt->execute([$ip, $date]);
  339.             }
  340.             $stmt = $pdo->prepare("UPDATE page_view SET tpage = ?, tvisit = ? WHERE id = ?");
  341.             $stmt->execute([$tpage, $tvisit, $page_view_id]);
  342.         } else {
  343.             $stmt = $pdo->prepare("INSERT INTO page_view (date, tpage, tvisit) VALUES (?, ?, ?)");
  344.             $stmt->execute([$date, 1, 1]);
  345.             $stmt = $pdo->prepare("INSERT INTO visitor_ips (ip, visit_date) VALUES (?, ?)");
  346.             $stmt->execute([$ip, $date]);
  347.         }
  348.     } catch (PDOException $e) {
  349.         error_log("Page view tracking error: " . $e->getMessage());
  350.     }
  351.  
  352.     // ads
  353.     $stmt = $pdo->query("SELECT * FROM ads WHERE id='1'");
  354.     $ads  = $stmt->fetch() ?: [];
  355.     $text_ads = trim($ads['text_ads'] ?? '');
  356.     $ads_1    = trim($ads['ads_1'] ?? '');
  357.     $ads_2    = trim($ads['ads_2'] ?? '');
  358.  
  359.     // Guard ID
  360.     if (!$paste_id) {
  361.         render_error_and_exit($lang['notfound'] ?? 'Paste not found.');
  362.     }
  363.  
  364.     // load paste
  365.     $stmt = $pdo->prepare("SELECT * FROM pastes WHERE id = ?");
  366.     $stmt->execute([$paste_id]);
  367.     if ($stmt->rowCount() === 0) {
  368.         render_error_and_exit($lang['notfound'] ?? 'Paste not found.');
  369.     }
  370.     $row = $stmt->fetch();
  371.  
  372.     // paste fields
  373.     $p_title    = (string) ($row['title'] ?? '');
  374.     $p_content  = (string) ($row['content'] ?? '');
  375.     $p_visible  = (string) ($row['visible'] ?? '0');
  376.     $p_code     = (string) ($row['code'] ?? 'text');
  377.     $p_expiry   = trim((string) ($row['expiry'] ?? 'NULL'));
  378.     $p_password = (string) ($row['password'] ?? 'NONE');
  379.     $p_member   = (string) ($row['member'] ?? '');
  380.     $p_date     = (string) ($row['date'] ?? '');
  381.     $p_encrypt  = (string) ($row['encrypt'] ?? '0');
  382.     $p_views    = getPasteViewCount($pdo, (int) $paste_id);
  383.  
  384.     // ---- comments config for view (AFTER $p_password is known) ----
  385.     // Read site-wide flags from config.php (with safe defaults)
  386.     $comments_enabled       = isset($comments_enabled) ? (bool)$comments_enabled : true;
  387.     $comments_require_login = isset($comments_require_login) ? (bool)$comments_require_login : true;
  388.     $comments_on_protected  = isset($comments_on_protected) ? (bool)$comments_on_protected : false;
  389.  
  390.     // Should we render the comments section for this paste?
  391.     $show_comments = $comments_enabled && ($comments_on_protected || $p_password === "NONE");
  392.     // Is the current user allowed to post a comment?
  393.     $can_comment   = $show_comments && ( !$comments_require_login || isset($_SESSION['username']) );
  394.  
  395.     $comment_error   = '';
  396.     $comment_success = '';
  397.  
  398.     // private?
  399.     if ($p_visible === "2") {
  400.         if (!isset($_SESSION['username']) || $p_member !== (string) ($_SESSION['username'] ?? '')) {
  401.             render_error_and_exit($lang['privatepaste'] ?? 'This is a private paste.', '403');
  402.         }
  403.     }
  404.  
  405.     // expiry
  406.     if ($p_expiry !== "NULL" && $p_expiry !== "SELF") {
  407.         $input_time = (int) $p_expiry;
  408.         if ($input_time > 0 && $input_time < time()) {
  409.             render_error_and_exit($lang['expired'] ?? 'This paste has expired.');
  410.         }
  411.     }
  412.  
  413.     // decrypt if needed
  414.     if ($p_encrypt === "1") {
  415.         if (!defined('SECRET')) {
  416.             render_error_and_exit(($lang['error'] ?? 'Error') . ': Missing SECRET.', '403');
  417.         }
  418.         $dec = decrypt($p_content, hex2bin(SECRET));
  419.         if ($dec === null || $dec === '') {
  420.             render_error_and_exit(($lang['error'] ?? 'Error') . ': Decryption failed.', '403');
  421.         }
  422.         $p_content = $dec;
  423.     }
  424.     $op_content = trim(htmlspecialchars_decode($p_content));
  425.  
  426.     // download/raw/embed
  427.     if (isset($_GET['download'])) {
  428.         if ($p_password === "NONE" || (isset($_GET['password']) && password_verify((string) $_GET['password'], $p_password))) {
  429.             doDownload((int) $paste_id, $p_title, $op_content, $p_code);
  430.             exit;
  431.         }
  432.         render_password_required_and_exit(
  433.             isset($_GET['password'])
  434.                 ? ($lang['wrongpassword'] ?? 'Incorrect password.')
  435.                 : ($lang['pwdprotected'] ?? 'This paste is password-protected.')
  436.         );
  437.     }
  438.  
  439.     if (isset($_GET['raw'])) {
  440.         if ($p_password === "NONE" || (isset($_GET['password']) && password_verify((string) $_GET['password'], $p_password))) {
  441.             rawView((int) $paste_id, $p_title, $op_content, $p_code);
  442.             exit;
  443.         }
  444.         render_password_required_and_exit(
  445.             isset($_GET['password'])
  446.                 ? ($lang['wrongpassword'] ?? 'Incorrect password.')
  447.                 : ($lang['pwdprotected'] ?? 'This paste is password-protected.')
  448.         );
  449.     }
  450.  
  451.     if (isset($_GET['embed'])) {
  452.         if ($p_password === "NONE" || (isset($_GET['password']) && password_verify((string) $_GET['password'], $p_password))) {
  453.             // Embed view is standalone; we pass empty $ges_style as we don't inject CSS here.
  454.             embedView((int) $paste_id, $p_title, $p_content, $p_code, $title, $baseurl, $ges_style, $lang);
  455.             exit;
  456.         }
  457.         render_password_required_and_exit(
  458.             isset($_GET['password'])
  459.                 ? ($lang['wrongpassword'] ?? 'Incorrect password.')
  460.                 : ($lang['pwdprotected'] ?? 'This paste is password-protected.')
  461.         );
  462.     }
  463.  
  464.     // highlight extraction
  465.     $highlight = [];
  466.     $prefix = '!highlight!';
  467.     if ($prefix !== '') {
  468.         $lines = explode("\n", $p_content);
  469.         $p_content = '';
  470.         foreach ($lines as $idx => $line) {
  471.             if (strncmp($line, $prefix, strlen($prefix)) === 0) {
  472.                 $highlight[] = $idx + 1;
  473.                 $line = substr($line, strlen($prefix));
  474.             }
  475.             $p_content .= $line . "\n";
  476.         }
  477.         $p_content = rtrim($p_content);
  478.     }
  479.  
  480.     // -------------- transform content --------------
  481.     $p_code_explain = ''; // will be filled when we detect
  482.  
  483.     if ($p_code === "markdown") {
  484.         // ---------- Markdown (keep using Parsedown, safe) ----------
  485.         require_once $parsedown_path;
  486.         $Parsedown = new Parsedown();
  487.  
  488.         $md_input = htmlspecialchars_decode($p_content);
  489.  
  490.         // Disable raw HTML and sanitize URLs during Markdown rendering
  491.         if (method_exists($Parsedown, 'setSafeMode')) {
  492.             $Parsedown->setSafeMode(true);
  493.             if (method_exists($Parsedown, 'setMarkupEscaped')) {
  494.                 $Parsedown->setMarkupEscaped(true);
  495.             }
  496.         } else {
  497.             // Fallback for very old Parsedown: escape raw HTML tags BEFORE parsing
  498.             $md_input = preg_replace_callback('/<[^>]*>/', static function($m){
  499.                 return htmlspecialchars($m[0], ENT_QUOTES, 'UTF-8');
  500.             }, $md_input);
  501.         }
  502.  
  503.         $rendered  = $Parsedown->text($md_input);
  504.         $p_content = '<div class="md-body">'.sanitize_allowlist_html($rendered).'</div>';
  505.  
  506.         $p_code_effective = 'markdown';
  507.         $p_code_label     = $geshiformats['markdown'] ?? 'Markdown';
  508.         $p_code_source    = 'explicit';
  509.         $p_code_explain   = 'Language was explicitly chosen by the author.';
  510.  
  511.     } else {
  512.         // ---------- Code (choose engine) ----------
  513.         $code_input = htmlspecialchars_decode($p_content);
  514.  
  515.         if (($highlighter ?? 'geshi') === 'highlight') {
  516.             // ---- Highlight.php ----
  517.             $hlId     = map_to_hl_lang($p_code);          // map geshi -> hljs ids
  518.             $use_auto = ($hlId === 'auto' || $hlId === 'autodetect' || $hlId === '' || $hlId === 'text');
  519.  
  520.             try {
  521.                 $hl = function_exists('make_highlighter') ? make_highlighter() : null;
  522.                 if (!$hl) { throw new \RuntimeException('Highlighter not available'); }
  523.  
  524.                 // 1) Filename strong hint if auto
  525.                 if ($use_auto) {
  526.                     $fname = paste_pick_from_filename($p_title ?? '', 'highlight', $hl);
  527.                     if ($fname) {
  528.                         $langTry          = $fname;
  529.                         $p_code_source    = 'filename';
  530.                         $p_code_explain   = paste_build_explain('filename', $p_title ?? '', $code_input, $langTry);
  531.                         $res              = $hl->highlight($langTry, $code_input);
  532.                         $inner            = $res->value ?: htmlspecialchars($code_input, ENT_QUOTES, 'UTF-8');
  533.                         $p_content        = hl_wrap_with_lines($inner, $langTry, $highlight);
  534.                         $p_code_effective = $langTry;
  535.                         $p_code_label     = $geshiformats[$langTry] ?? (function_exists('paste_friendly_label') ? paste_friendly_label($langTry) : strtoupper($langTry));
  536.                         goto HL_DONE;
  537.                     }
  538.                 }
  539.  
  540.                 // 2) PHP tag hard rule if auto
  541.                 if ($use_auto && is_probable_php_tag($code_input)) {
  542.                     $langTry          = 'php';
  543.                     $p_code_source    = 'php-tag';
  544.                     $p_code_explain   = paste_build_explain('php-tag', $p_title ?? '', $code_input, $langTry);
  545.                     $res              = $hl->highlight($langTry, $code_input);
  546.                     $inner            = $res->value ?: htmlspecialchars($code_input, ENT_QUOTES, 'UTF-8');
  547.                     $p_content        = hl_wrap_with_lines($inner, $langTry, $highlight);
  548.                     $p_code_effective = $langTry;
  549.                     $p_code_label     = $geshiformats[$langTry] ?? 'PHP';
  550.                     goto HL_DONE;
  551.                 }
  552.  
  553.                 if ($use_auto && function_exists('paste_autodetect_language')) {
  554.                     // Unified autodetect
  555.                     $det = paste_autodetect_language($code_input, 'highlight', $hl);
  556.                     $langTry          = $det['id'];
  557.                     $p_code_effective = $langTry;
  558.                     $p_code_label     = $geshiformats[$langTry] ?? $det['label'];
  559.                     $p_code_source    = $det['source'];
  560.                     $p_code_explain   = $det['explain'] ?? '';
  561.                     if ($p_code_explain === '') {
  562.                         $p_code_explain = paste_build_explain($p_code_source, $p_title ?? '', $code_input, $langTry);
  563.                     }
  564.  
  565.                     if ($langTry === 'markdown') {
  566.                         // Render Markdown via Parsedown
  567.                         require_once $parsedown_path;
  568.                         $Parsedown = new Parsedown();
  569.                         if (method_exists($Parsedown, 'setSafeMode')) {
  570.                             $Parsedown->setSafeMode(true);
  571.                             if (method_exists($Parsedown, 'setMarkupEscaped')) $Parsedown->setMarkupEscaped(true);
  572.                         }
  573.                         $rendered  = $Parsedown->text($code_input);
  574.                         $p_content = '<div class="md-body">' . sanitize_allowlist_html($rendered) . '</div>';
  575.                     } else {
  576.                         $res   = $hl->highlight($langTry, $code_input);
  577.                         $inner = $res->value ?: htmlspecialchars($code_input, ENT_QUOTES, 'UTF-8');
  578.                         $p_content = hl_wrap_with_lines($inner, $langTry, $highlight);
  579.                     }
  580.                 } else {
  581.                     // Explicit language requested OR fallback to built-in auto
  582.                     $langTry = function_exists('paste_normalize_lang') ? paste_normalize_lang($hlId, 'highlight', $hl) : $hlId;
  583.                     $p_code_source = 'explicit';
  584.                     try {
  585.                         $res = $hl->highlight($langTry, $code_input);
  586.                     } catch (\Throwable $e) {
  587.                         // Fallbacks: pgsql -> sql, otherwise shared autodetect
  588.                         if ($langTry === 'pgsql') {
  589.                             $res = $hl->highlight('sql', $code_input);
  590.                         } elseif (function_exists('paste_autodetect_language')) {
  591.                             $det = paste_autodetect_language($code_input, 'highlight', $hl);
  592.                             $langTry          = $det['id'];
  593.                             $p_code_label     = $geshiformats[$langTry] ?? $det['label'];
  594.                             $p_code_source    = $det['source'];
  595.                             $p_code_explain   = $det['explain'] ?? '';
  596.                             if ($p_code_explain === '') {
  597.                                 $p_code_explain = paste_build_explain($p_code_source, $p_title ?? '', $code_input, $langTry);
  598.                             }
  599.  
  600.                             if ($langTry === 'markdown') {
  601.                                 require_once $parsedown_path;
  602.                                 $Parsedown = new Parsedown();
  603.                                 if (method_exists($Parsedown, 'setSafeMode')) {
  604.                                     $Parsedown->setSafeMode(true);
  605.                                     if (method_exists($Parsedown, 'setMarkupEscaped')) $Parsedown->setMarkupEscaped(true);
  606.                                 }
  607.                                 $rendered         = $Parsedown->text($code_input);
  608.                                 $p_content        = '<div class="md-body">' . sanitize_allowlist_html($rendered) . '</div>';
  609.                                 $p_code_effective = 'markdown';
  610.                                 goto HL_DONE;
  611.                             } else {
  612.                                 $res = $hl->highlight($langTry, $code_input);
  613.                             }
  614.                         } else {
  615.                             // last resort: hljs auto
  616.                             $p_code_source = 'hljs';
  617.                             $res    = $hl->highlightAuto($code_input);
  618.                             $langTry = strtolower((string)($res->language ?? $langTry));
  619.                             $p_code_explain = paste_build_explain('hljs', $p_title ?? '', $code_input, $langTry ?: 'plaintext');
  620.                         }
  621.                     }
  622.                     $inner            = $res->value ?: htmlspecialchars($code_input, ENT_QUOTES, 'UTF-8');
  623.                     $p_content        = hl_wrap_with_lines($inner, $langTry, $highlight);
  624.                     $p_code_effective = $langTry;
  625.                     $p_code_label     = $geshiformats[$langTry] ?? (function_exists('paste_friendly_label') ? paste_friendly_label($langTry) : strtoupper($langTry));
  626.                 }
  627.                 HL_DONE: ;
  628.             } catch (\Throwable $t) {
  629.                 // Last resort: plain escaped
  630.                 $esc              = htmlspecialchars($code_input, ENT_QUOTES, 'UTF-8');
  631.                 $p_content        = hl_wrap_with_lines($esc, 'plaintext', $highlight);
  632.                 $p_code_effective = 'plaintext';
  633.                 $p_code_label     = $geshiformats['plaintext'] ?? 'Plain Text';
  634.                 $p_code_source    = $p_code_source ?? 'fallback';
  635.                 $p_code_explain   = $p_code_explain ?: paste_build_explain('fallback', $p_title ?? '', $code_input, 'plaintext');
  636.             }
  637.  
  638.         } else {
  639.             // ---- GeSHi ----
  640.             $use_auto = ($p_code === 'auto' || $p_code === 'autodetect' || $p_code === '' || $p_code === 'text');
  641.             $lang_for_geshi = $p_code;
  642.  
  643.             // 1) Filename hint if auto
  644.             if ($use_auto) {
  645.                 $fname = paste_pick_from_filename($p_title ?? '', 'geshi', null);
  646.                 if ($fname) {
  647.                     $lang_for_geshi   = $fname;
  648.                     $p_code_effective = $lang_for_geshi;
  649.                     $p_code_label     = $geshiformats[$lang_for_geshi] ?? (function_exists('paste_friendly_label') ? paste_friendly_label($lang_for_geshi) : strtoupper($lang_for_geshi));
  650.                     $p_code_source    = 'filename';
  651.                     $p_code_explain   = paste_build_explain('filename', $p_title ?? '', $code_input, $lang_for_geshi);
  652.                 }
  653.             }
  654.             // 2) PHP tag hard rule if auto and not already set by filename
  655.             if ($use_auto && empty($p_code_source) && is_probable_php_tag($code_input)) {
  656.                 $lang_for_geshi   = function_exists('paste_normalize_lang') ? paste_normalize_lang('php', 'geshi', null) : 'php';
  657.                 $p_code_effective = $lang_for_geshi;
  658.                 $p_code_label     = $geshiformats[$lang_for_geshi] ?? 'PHP';
  659.                 $p_code_source    = 'php-tag';
  660.                 $p_code_explain   = paste_build_explain('php-tag', $p_title ?? '', $code_input, $lang_for_geshi);
  661.             }
  662.  
  663.             if ($use_auto && empty($p_code_source) && function_exists('paste_autodetect_language')) {
  664.                 $det = paste_autodetect_language($code_input, 'geshi', null);
  665.                 $lang_for_geshi   = $det['id'];                // GeSHi id (mapped)
  666.                 $p_code_effective = $lang_for_geshi;
  667.                 $p_code_label     = $det['label'];
  668.                 $p_code_source    = $det['source'];
  669.                 $p_code_explain   = $det['explain'] ?? '';
  670.                 if ($p_code_explain === '') {
  671.                     $p_code_explain = paste_build_explain($p_code_source, $p_title ?? '', $code_input, $lang_for_geshi);
  672.                 }
  673.                 if ($lang_for_geshi === 'markdown') {
  674.                     // For Markdown, keep Parsedown path for consistent rendering
  675.                     require_once $parsedown_path;
  676.                     $Parsedown = new Parsedown();
  677.                     if (method_exists($Parsedown, 'setSafeMode')) {
  678.                         $Parsedown->setSafeMode(true);
  679.                         if (method_exists($Parsedown, 'setMarkupEscaped')) $Parsedown->setMarkupEscaped(true);
  680.                     }
  681.                     $rendered  = $Parsedown->text($code_input);
  682.                     $p_content = '<div class="md-body">' . sanitize_allowlist_html($rendered) . '</div>';
  683.                     goto GESHI_DONE;
  684.                 }
  685.             } elseif ($use_auto && empty($p_code_source) && function_exists('paste_probable_markdown') && paste_probable_markdown($code_input)) {
  686.                 // Minimal fallback
  687.                 require_once $parsedown_path;
  688.                 $Parsedown = new Parsedown();
  689.                 if (method_exists($Parsedown, 'setSafeMode')) {
  690.                     $Parsedown->setSafeMode(true);
  691.                     if (method_exists($Parsedown, 'setMarkupEscaped')) $Parsedown->setMarkupEscaped(true);
  692.                 }
  693.                 $rendered         = $Parsedown->text($code_input);
  694.                 $p_content        = '<div class="md-body">' . sanitize_allowlist_html($rendered) . '</div>';
  695.                 $p_code_effective = 'markdown';
  696.                 $p_code_label     = 'Markdown';
  697.                 $p_code_source    = 'markdown';
  698.                 $p_code_explain   = "Markdown probability based on headings/lists/links; rendering as Markdown.";
  699.                 goto GESHI_DONE;
  700.             }
  701.  
  702.             $geshi = new GeSHi($code_input, $lang_for_geshi, $path);
  703.  
  704.             // Use classes, not inline CSS; let theme CSS style everything
  705.             if (method_exists($geshi, 'enable_classes')) $geshi->enable_classes();
  706.             if (method_exists($geshi, 'set_header_type')) $geshi->set_header_type(GESHI_HEADER_DIV);
  707.  
  708.             // Line numbers (NORMAL to avoid rollovers). No inline style.
  709.             if (method_exists($geshi, 'enable_line_numbers')) $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
  710.             if (!empty($highlight) && method_exists($geshi, 'highlight_lines_extra')) {
  711.                 $geshi->highlight_lines_extra($highlight);
  712.             }
  713.  
  714.             // force plain integer formatting
  715.             if (method_exists($geshi, 'set_line_number_format')) {
  716.                 $geshi->set_line_number_format('%d', 0);
  717.             }
  718.  
  719.             // Parse HTML (class-based markup)
  720.             $p_content = $geshi->parse_code();
  721.  
  722.             // Add a class to the requested lines so theme CSS can style them
  723.             if (!empty($highlight)) {
  724.                 $p_content = geshi_add_line_highlight_class($p_content, $highlight, 'hljs-hl');
  725.             }
  726.  
  727.             // No stylesheet injection here; theme CSS handles it.
  728.             $ges_style = '';
  729.  
  730.             // Effective values for UI
  731.             $p_code_effective = $lang_for_geshi;
  732.             if (!isset($p_code_label)) {
  733.                 $p_code_label = $geshiformats[$p_code_effective] ?? (function_exists('paste_friendly_label') ? paste_friendly_label($p_code_effective) : strtoupper($p_code_effective));
  734.             }
  735.  
  736.             GESHI_DONE: ;
  737.         }
  738.     }
  739.  
  740.     // ======= New comment submit (PRG) — supports parent_id for replies =======
  741.     if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_comment') {
  742.         if (!$paste_id) {
  743.             $comment_error = 'Invalid paste.';
  744.         } elseif (!$comments_enabled) {
  745.             $comment_error = $lang['comments_off'] ?? 'Comments are disabled.';
  746.         } elseif (!$show_comments) {
  747.             $comment_error = $lang['comments_blocked_here'] ?? 'Comments are not available for this paste.';
  748.         } elseif ($comments_require_login && !isset($_SESSION['username'])) {
  749.             $comment_error = $lang['commentlogin'] ?? 'You must be logged in to comment.';
  750.         } elseif (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'] ?? '', (string)$_POST['csrf_token'])) {
  751.             $comment_error = $lang['invalidtoken'] ?? 'Invalid CSRF token.';
  752.         } else {
  753.             $uid = 0;
  754.             $uname = 'Guest';
  755.             if (isset($_SESSION['username'])) {
  756.                 $uname = (string)$_SESSION['username'];
  757.                 // fetch id to store as FK
  758.                 try {
  759.                     $q = $pdo->prepare("SELECT id FROM users WHERE username = ?");
  760.                     $q->execute([$uname]);
  761.                     $uid = (int)($q->fetchColumn() ?: 0);
  762.                 } catch (Throwable $e) { $uid = 0; }
  763.             }
  764.  
  765.             $parent_id = null;
  766.             if (isset($_POST['parent_id']) && $_POST['parent_id'] !== '') {
  767.                 $parent_id = (int)$_POST['parent_id'];
  768.             }
  769.  
  770.             // Backward/forward compatible call to addPasteComment()
  771.             $okId = null;
  772.             try {
  773.                 if (function_exists('addPasteComment')) {
  774.                     $rf = new ReflectionFunction('addPasteComment');
  775.                     if ($rf->getNumberOfParameters() >= 7) {
  776.                         // Signature with parent_id
  777.                         $okId = addPasteComment(
  778.                             $pdo,
  779.                             (int)$paste_id,
  780.                             $uid ?: null,
  781.                             $uname,
  782.                             $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
  783.                             (string)($_POST['comment_body'] ?? ''),
  784.                             $parent_id
  785.                         );
  786.                     } else {
  787.                         // no parent support
  788.                         $okId = addPasteComment(
  789.                             $pdo,
  790.                             (int)$paste_id,
  791.                             $uid ?: null,
  792.                             $uname,
  793.                             $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
  794.                             (string)($_POST['comment_body'] ?? '')
  795.                         );
  796.                     }
  797.                 }
  798.             } catch (Throwable $e) {
  799.                 error_log('add_comment error: ' . $e->getMessage());
  800.                 $okId = null;
  801.             }
  802.  
  803.             if ($okId) {
  804.                 // Avoid resubmit on refresh
  805.                 $to = ($mod_rewrite == '1')
  806.                     ? $baseurl . $paste_id . '#comments'
  807.                     : $baseurl . 'paste.php?id=' . (int)$paste_id . '#comments';
  808.                 header('Location: ' . $to);
  809.                 exit;
  810.             }
  811.             $comment_error = 'Could not add comment.';
  812.         }
  813.     }
  814.  
  815.     // ======= Delete comment (PRG) — hard delete; replies removed via FK CASCADE) =======
  816.     // (Allowed even if comments are currently disabled — so users can still remove their content.)
  817.         if ($_SERVER['REQUEST_METHOD'] === 'POST'
  818.                 && isset($_POST['action']) && $_POST['action'] === 'delete_comment') {
  819.  
  820.                 $isAjax = (
  821.                         (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
  822.                         || (isset($_POST['ajax']) && $_POST['ajax'] === '1')
  823.                 );
  824.  
  825.                 $err = '';
  826.                 do {
  827.                         if (empty($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'] ?? '', (string)$_POST['csrf_token'])) {
  828.                                 $err = $lang['invalidtoken'] ?? 'Invalid CSRF token.'; break;
  829.                         }
  830.                         if (!isset($_SESSION['username'])) {
  831.                                 $err = $lang['commentlogin'] ?? 'Log in required.'; break;
  832.                         }
  833.  
  834.                         // Ensure current paste context (accept id from GET OR POST for robustness)
  835.                         if (!$paste_id) {
  836.                                 $paste_id = (isset($_POST['id']) && $_POST['id'] !== '') ? (int)$_POST['id'] : $paste_id;
  837.                         }
  838.                         $cid = isset($_POST['comment_id']) ? (int)$_POST['comment_id'] : 0;
  839.                         if ($cid <= 0 || !$paste_id) { $err = 'Invalid comment.'; break; }
  840.  
  841.                         // Resolve current user id
  842.                         $uid = 0;
  843.                         try {
  844.                                 $q = $pdo->prepare("SELECT id FROM users WHERE username = ?");
  845.                                 $q->execute([$_SESSION['username']]);
  846.                                 $uid = (int)($q->fetchColumn() ?: 0);
  847.                         } catch (Throwable $e) {}
  848.  
  849.                         if (!userOwnsComment($pdo, $cid, $uid, (string)$_SESSION['username'])) {
  850.                                 $err = $lang['forbidden'] ?? 'Not allowed.'; break;
  851.                         }
  852.  
  853.                         $ok = false;
  854.                         if (function_exists('deleteComment')) {
  855.                                 $ok = (bool) deleteComment($pdo, $cid);
  856.                         } else {
  857.                                 // Guard by paste_id to avoid cross-paste deletes
  858.                                 $stmt = $pdo->prepare("DELETE FROM paste_comments WHERE id = ? AND paste_id = ?");
  859.                                 $ok = $stmt->execute([$cid, (int)$paste_id]);
  860.                         }
  861.                         if (!$ok) { $err = $lang['error'] ?? 'Delete failed.'; }
  862.                 } while (false);
  863.  
  864.                 if ($isAjax) {
  865.                         header('Content-Type: application/json; charset=utf-8');
  866.                         echo json_encode([
  867.                                 'success' => ($err === ''),
  868.                                 'message' => $err,
  869.                         ]);
  870.                         exit;
  871.                 }
  872.  
  873.                 // Redirect back to #comments (PRG). If error, carry as c_err=
  874.                 $to = ($mod_rewrite == '1')
  875.                         ? $baseurl . (int)$paste_id . '#comments'
  876.                         : $baseurl . 'paste.php?id=' . (int)$paste_id . '#comments';
  877.         if ($err !== '') {
  878.             $to .= (strpos($to, '?') === false ? '?' : '&') . 'c_err=' . rawurlencode($err);
  879.         }
  880.                 header('Location: ' . $to);
  881.                 exit;
  882.         }
  883.  
  884.     // header
  885.     $theme = 'theme/' . htmlspecialchars($default_theme, ENT_QUOTES, 'UTF-8');
  886.     require_once $theme . '/header.php';
  887.  
  888.     // Fetch comments (tree if available), then decorate
  889.     if (function_exists('getPasteCommentsTree')) {
  890.         $comments = getPasteCommentsTree($pdo, (int)$paste_id);
  891.         // decorate recursively
  892.         $decorate = function (&$node) use (&$decorate, $pdo) {
  893.             $node['body_html'] = render_comment_html((string)($node['body'] ?? ''));
  894.             $node['can_delete'] = isset($_SESSION['username']) && isset($_SESSION['csrf_token']) && (function() use ($pdo, $node) {
  895.                 $uid = 0;
  896.                 try {
  897.                     $q = $pdo->prepare("SELECT id FROM users WHERE username = ?");
  898.                     $q->execute([$_SESSION['username']]);
  899.                     $uid = (int)($q->fetchColumn() ?: 0);
  900.                 } catch (Throwable $e) { $uid = 0; }
  901.                 return userOwnsComment($pdo, (int)$node['id'], $uid, (string)$_SESSION['username']);
  902.             })();
  903.             if (!empty($node['children'])) {
  904.                 foreach ($node['children'] as &$ch) $decorate($ch);
  905.                 unset($ch);
  906.             }
  907.         };
  908.         foreach ($comments as &$c) $decorate($c);
  909.         unset($c);
  910.     } else {
  911.         // flat fetch
  912.         $comments = getPasteComments($pdo, (int)$paste_id, 200, 0);
  913.         foreach ($comments as &$c) {
  914.             $c['body_html'] = render_comment_html((string)$c['body']);
  915.             $c['can_delete'] = isset($_SESSION['username']) && isset($_SESSION['csrf_token']) && (function() use ($pdo, $c) {
  916.                 $uid = 0;
  917.                 try {
  918.                     $q = $pdo->prepare("SELECT id FROM users WHERE username = ?");
  919.                     $q->execute([$_SESSION['username']]);
  920.                     $uid = (int)($q->fetchColumn() ?: 0);
  921.                 } catch (Throwable $e) { $uid = 0; }
  922.                 return userOwnsComment($pdo, (int)$c['id'], $uid, (string)$_SESSION['username']);
  923.             })();
  924.         }
  925.         unset($c);
  926.     }
  927.  
  928.     // Carry any error from PRG redirect
  929.     if ($comment_error === '' && isset($_GET['c_err']) && $_GET['c_err'] !== '') {
  930.         $comment_error = (string)$_GET['c_err'];
  931.     }
  932.  
  933.     // view OR password prompt
  934.     if ($p_password === "NONE") {
  935.         updateMyView($pdo, (int) $paste_id);
  936.  
  937.         $p_download = $mod_rewrite == '1' ? $baseurl . "download/$paste_id" : $baseurl . "paste.php?download&id=$paste_id";
  938.         $p_raw      = $mod_rewrite == '1' ? $baseurl . "raw/$paste_id"      : $baseurl . "paste.php?raw&id=$paste_id";
  939.         $p_embed    = $mod_rewrite == '1' ? $baseurl . "embed/$paste_id"    : $baseurl . "paste.php?embed&id=$paste_id";
  940.  
  941.         require_once $theme . '/view.php';
  942.  
  943.         // View-once (SELF) cleanup after increment
  944.         $current_views = getPasteViewCount($pdo, (int) $paste_id);
  945.         if ($p_expiry === "SELF" && $current_views >= 2) {
  946.             deleteMyPaste($pdo, (int) $paste_id);
  947.         }
  948.     } else {
  949.         // Password-protected flow shows the prompt via errors.php
  950.         $require_password = true;
  951.  
  952.         $p_password_input = isset($_POST['mypass'])
  953.             ? trim((string) $_POST['mypass'])
  954.             : (string) ($_SESSION['p_password'] ?? '');
  955.  
  956.         // Prebuild convenience links that carry the typed password
  957.         $p_download = $mod_rewrite == '1'
  958.             ? $baseurl . "download/$paste_id?password=" . rawurlencode($p_password_input)
  959.             : $baseurl . "paste.php?download&id=$paste_id&password=" . rawurlencode($p_password_input);
  960.         $p_raw = $mod_rewrite == '1'
  961.             ? $baseurl . "raw/$paste_id?password=" . rawurlencode($p_password_input)
  962.             : $baseurl . "paste.php?raw&id=$paste_id&password=" . rawurlencode($p_password_input);
  963.         $p_embed = $mod_rewrite == '1'
  964.             ? $baseurl . "embed/$paste_id?password=" . rawurlencode($p_password_input)
  965.             : $baseurl . "paste.php?embed&id=$paste_id&password=" . rawurlencode($p_password_input);
  966.  
  967.         if ($p_password_input !== '' && password_verify($p_password_input, $p_password)) {
  968.             updateMyView($pdo, (int) $paste_id);
  969.             require_once $theme . '/view.php';
  970.  
  971.             $current_views = getPasteViewCount($pdo, (int) $paste_id);
  972.             if ($p_expiry === "SELF" && $current_views >= 2) {
  973.                 deleteMyPaste($pdo, (int) $paste_id);
  974.             }
  975.         } else {
  976.             $error = $p_password_input !== ''
  977.                 ? ($lang['wrongpwd'] ?? 'Incorrect password.')
  978.                 : ($lang['pwdprotected'] ?? 'This paste is password-protected.');
  979.             $_SESSION['p_password'] = $p_password_input;
  980.  
  981.             require_once $theme . '/errors.php'; // partial renders password prompt
  982.         }
  983.     }
  984.  
  985.     // footer
  986.     require_once $theme . '/footer.php';
  987.  
  988. } catch (PDOException $e) {
  989.     error_log("paste.php: Database error: " . $e->getMessage());
  990.  
  991.     // Still render a readable error page (no password box)
  992.     $error = ($lang['error'] ?? 'Database error.') . ': ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
  993.  
  994.     global $default_theme, $baseurl, $mod_rewrite, $pdo, $require_password;
  995.     $require_password = false;
  996.  
  997.     $theme = 'theme/' . htmlspecialchars($default_theme ?? 'default', ENT_QUOTES, 'UTF-8');
  998.     require_once $theme . '/header.php';
  999.     require_once $theme . '/errors.php';
  1000.     require_once $theme . '/footer.php';
  1001. }
  1002. ?>

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.