- 1<?php
- 2/*
- 3 * Paste $v3.2 2025/09/08 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 */
- 18declare(strict_types=1);
- 19
- 20require_once __DIR__ . '/includes/session.php';
- 21require_once __DIR__ . '/config.php';
- 22require_once __DIR__ . '/includes/functions.php';
- 23// Error handling
- 24paste_enable_themed_errors();
- 25
- 26// Highlighter bootstrap + language lists
- 27require_once __DIR__ . '/includes/hlbootstrap.php';
- 28require_once __DIR__ . '/includes/list_languages.php';
- 29
- 30ini_set('display_errors','0');
- 31ini_set('log_errors','1');
- 32date_default_timezone_set('UTC');
- 33
- 34// Load paste by ID, decrypting as needed. Returns ['title','content','code'] or null
- 35function load_paste(PDO $pdo, int $id): ?array {
- 36 try {
- 37 $st = $pdo->prepare("SELECT id,title,content,code,encrypt FROM pastes WHERE id=? LIMIT 1");
- 38 $st->execute([$id]);
- 39 $r = $st->fetch();
- 40 if (!$r) return null;
- 41
- 42 $title = (string)$r['title'];
- 43 $content = (string)$r['content'];
- 44
- 45 if ((string)$r['encrypt'] === "1" && defined('SECRET')) {
- 46 $title = decrypt($title, hex2bin(SECRET)) ?? $title;
- 47 $content = decrypt($content, hex2bin(SECRET)) ?? $content;
- 48 }
- 49
- 50 // Stored content is HTML-escaped before encrypt; decode for diffing
- 51 $content = html_entity_decode($content, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8');
- 52
- 53 return [
- 54 'title' => $title,
- 55 'content' => $content,
- 56 'code' => (string)($r['code'] ?? 'text'),
- 57 ];
- 58 } catch (Throwable $e) {
- 59 error_log("diff.php load_paste($id): ".$e->getMessage());
- 60 return null;
- 61 }
- 62}
- 63
- 64// Inline (word/char) diff > [leftHTML, rightHTML] with <span class="diff-inside-...">
- 65function inline_diff(string $a, string $b): array {
- 66 $split = static function(string $s): array {
- 67 preg_match_all('/\s+|[^\s]+/u', $s, $m);
- 68 return $m[0] ?: [$s];
- 69 };
- 70 $Aw = $split($a); $Bw = $split($b);
- 71 $useChar = (count($Aw) <= 4 || count($Bw) <= 4);
- 72 if ($useChar) {
- 73 $Aw = preg_split('//u', $a, -1, PREG_SPLIT_NO_EMPTY);
- 74 $Bw = preg_split('//u', $b, -1, PREG_SPLIT_NO_EMPTY);
- 75 }
- 76 $n=count($Aw); $m=count($Bw);
- 77 $L = array_fill(0,$n+1,array_fill(0,$m+1,0));
- 78 for($i=$n-1;$i>=0;$i--) for($j=$m-1;$j>=0;$j--) {
- 79 $L[$i][$j]=($Aw[$i]===$Bw[$j])?($L[$i+1][$j+1]+1):max($L[$i+1][$j],$L[$i][$j+1]);
- 80 }
- 81 $i=0;$j=0;$left='';$right='';
- 82 $esc = static fn($s)=>htmlspecialchars($s, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8');
- 83 while($i<$n && $j<$m){
- 84 if($Aw[$i]===$Bw[$j]){ $left.=$esc($Aw[$i]); $right.=$esc($Bw[$j]); $i++; $j++; }
- 85 elseif($L[$i+1][$j] >= $L[$i][$j+1]){ $left.='<span class="diff-inside-del">'.$esc($Aw[$i]).'</span>'; $i++; }
- 86 else { $right.='<span class="diff-inside-add">'.$esc($Bw[$j]).'</span>'; $j++; }
- 87 }
- 88 while($i<$n){ $left.='<span class="diff-inside-del">'.$esc($Aw[$i]).'</span>'; $i++; }
- 89 while($j<$m){ $right.='<span class="diff-inside-add">'.$esc($Bw[$j]).'</span>'; $j++; }
- 90 return [$left,$right];
- 91}
- 92
- 93/**
- 94 * Index-based diff at line level > opcodes referencing original arrays.
- 95 * Opcodes:
- 96 * - ['op'=>'eq','ai'=>i,'bi'=>j]
- 97 * - ['op'=>'del','ai'=>i]
- 98 * - ['op'=>'add','bi'=>j]
- 99 *
- 100 * 1) xdiff accelerator (parse unified diff) if available
- 101 * 2) Myers O((N+M)D) fallback in pure PHP
- 102 */
- 103function diff_lines_idx(array $A, array $B, ?callable $normalizer=null): array {
- 104 $An = $normalizer ? array_map($normalizer, $A) : $A;
- 105 $Bn = $normalizer ? array_map($normalizer, $B) : $B;
- 106
- 107 if (function_exists('xdiff_string_diff')) {
- 108 return _diff_idx_via_xdiff($An, $Bn);
- 109 }
- 110 return _diff_idx_via_myers($An, $Bn);
- 111}
- 112
- 113/* ---------- Fast path: use xdiff to compute unified diff, parse to opcodes ---------- */
- 114function _diff_idx_via_xdiff(array $A, array $B): array {
- 115 $N = count($A); $M = count($B);
- 116
- 117 // Join as text; add trailing newline to stabilize EOF handling
- 118 $left = ($N ? implode("\n", $A) : '') . "\n";
- 119 $right = ($M ? implode("\n", $B) : '') . "\n";
- 120
- 121 // Use context for stable headers + ' ' lines; non-minimal for speed
- 122 $ctx = 3;
- 123 $ud = xdiff_string_diff($left, $right, $ctx, false);
- 124 if ($ud === false) {
- 125 // Trivial fallback: align common prefix, then tail adds/dels
- 126 $ops = [];
- 127 $eq = min($N, $M);
- 128 for ($i=0; $i<$eq; $i++) $ops[] = ['op'=>'eq','ai'=>$i,'bi'=>$i];
- 129 for ($i=$eq; $i<$N; $i++) $ops[] = ['op'=>'del','ai'=>$i];
- 130 for ($j=$eq; $j<$M; $j++) $ops[] = ['op'=>'add','bi'=>$j];
- 131 return $ops;
- 132 }
- 133 if ($ud === '') {
- 134 $ops = [];
- 135 for ($i=0; $i<min($N,$M); $i++) $ops[] = ['op'=>'eq','ai'=>$i,'bi'=>$i];
- 136 for ($i=$M; $i<$N; $i++) $ops[] = ['op'=>'del','ai'=>$i];
- 137 for ($j=$N; $j<$M; $j++) $ops[] = ['op'=>'add','bi'=>$j];
- 138 return $ops;
- 139 }
- 140
- 141 $ops = [];
- 142 $ai = 0; $bi = 0;
- 143
- 144 $lines = preg_split("/\R/u", $ud);
- 145 foreach ($lines as $ln) {
- 146 if ($ln === '' && $ln !== '0') continue;
- 147
- 148 // Hunk header: @@ -aStart,aLen +bStart,bLen @@
- 149 if (($ln[0] ?? '') === '@' &&
- 150 preg_match('/^@@\s+-([0-9]+)(?:,([0-9]+))?\s+\+([0-9]+)(?:,([0-9]+))?\s+@@/', $ln, $m)) {
- 151
- 152 $startA = max(0, (int)$m[1] - 1); // convert to 0-based
- 153 $startB = max(0, (int)$m[3] - 1);
- 154
- 155 // Between hunks: move forward by equal lines only
- 156 $gap = min(max(0, $startA - $ai), max(0, $startB - $bi));
- 157 for ($k=0; $k<$gap; $k++) { $ops[] = ['op'=>'eq','ai'=>$ai,'bi'=>$bi]; $ai++; $bi++; }
- 158 continue;
- 159 }
- 160
- 161 $tag = $ln[0] ?? '';
- 162 if ($tag === ' ') { // context (equal)
- 163 $ops[] = ['op'=>'eq','ai'=>$ai,'bi'=>$bi]; $ai++; $bi++;
- 164 } elseif ($tag === '-') { // deletion
- 165 $ops[] = ['op'=>'del','ai'=>$ai]; $ai++;
- 166 } elseif ($tag === '+') { // addition
- 167 $ops[] = ['op'=>'add','bi'=>$bi]; $bi++;
- 168 } elseif ($tag === '\\') {
- 169 // "\ No newline at end of file" > ignore
- 170 } else {
- 171 // headers '---' / '+++' or noise > ignore
- 172 }
- 173 }
- 174
- 175 // Trailing equals after the last hunk
- 176 $tailEq = min($N - $ai, $M - $bi);
- 177 for ($k=0; $k<$tailEq; $k++) { $ops[] = ['op'=>'eq','ai'=>$ai,'bi'=>$bi]; $ai++; $bi++; }
- 178 for (; $ai<$N; $ai++) $ops[] = ['op'=>'del','ai'=>$ai];
- 179 for (; $bi<$M; $bi++) $ops[] = ['op'=>'add','bi'=>$bi];
- 180
- 181 return $ops;
- 182}
- 183
- 184/* ---------- Fallback: Myers O((N+M)D) with path reconstruction ---------- */
- 185function _diff_idx_via_myers(array $A, array $B): array {
- 186 $N = count($A); $M = count($B);
- 187
- 188 if ($N === 0 && $M === 0) return [];
- 189 if ($N === 0) { $ops=[]; for($j=0;$j<$M;$j++) $ops[]=['op'=>'add','bi'=>$j]; return $ops; }
- 190 if ($M === 0) { $ops=[]; for($i=0;$i<$N;$i++) $ops[]=['op'=>'del','ai'=>$i]; return $ops; }
- 191
- 192 $max = $N + $M;
- 193 $off = $max;
- 194 $V = array_fill(0, 2 * $max + 1, 0);
- 195 $trace = [];
- 196
- 197 $Dend = 0;
- 198 for ($d = 0; $d <= $max; $d++) {
- 199 $trace[$d] = $V;
- 200
- 201 for ($k = -$d; $k <= $d; $k += 2) {
- 202 if ($k == -$d || ($k != $d && $V[$off + $k - 1] < $V[$off + $k + 1])) {
- 203 $x = $V[$off + $k + 1]; // down (insert B)
- 204 } else {
- 205 $x = $V[$off + $k - 1] + 1; // right (delete A)
- 206 }
- 207 $y = $x - $k;
- 208
- 209 while ($x < $N && $y < $M && $A[$x] === $B[$y]) { $x++; $y++; }
- 210
- 211 $V[$off + $k] = $x;
- 212
- 213 if ($x >= $N && $y >= $M) {
- 214 $trace[$d] = $V;
- 215 $Dend = $d;
- 216 break 2;
- 217 }
- 218 }
- 219 }
- 220
- 221 $ops = [];
- 222 $x = $N; $y = $M;
- 223 for ($d = $Dend; $d > 0; $d--) {
- 224 $Vprev = $trace[$d-1];
- 225 $k = $x - $y;
- 226
- 227 $down = ($k == -$d) || ($k != $d && $Vprev[$off + $k - 1] < $Vprev[$off + $k + 1]);
- 228 $kPrev = $down ? $k + 1 : $k - 1;
- 229 $xStart = $down ? $Vprev[$off + $kPrev] : $Vprev[$off + $kPrev] + 1;
- 230 $yStart = $xStart - $kPrev;
- 231
- 232 while ($x > $xStart && $y > $yStart) { $x--; $y--; $ops[] = ['op'=>'eq','ai'=>$x,'bi'=>$y]; }
- 233
- 234 if ($down) {
- 235 $yStart--;
- 236 $ops[] = ['op'=>'add','bi'=>$yStart];
- 237 } else {
- 238 $xStart--;
- 239 $ops[] = ['op'=>'del','ai'=>$xStart];
- 240 }
- 241
- 242 $x = $xStart; $y = $yStart;
- 243 }
- 244
- 245 while ($x > 0 && $y > 0) { $x--; $y--; $ops[] = ['op'=>'eq','ai'=>$x,'bi'=>$y]; }
- 246 while ($x > 0) { $x--; $ops[] = ['op'=>'del','ai'=>$x]; }
- 247 while ($y > 0) { $y--; $ops[] = ['op'=>'add','bi'=>$y]; }
- 248
- 249 return array_reverse($ops);
- 250}
- 251
- 252// Build side-by-side & unified row arrays from ops (index-based).
- 253function build_tables_idx(array $ops, array $leftLines, array $rightLines): array {
- 254 $side=[]; $uni=[]; $li=1; $ri=1;
- 255 foreach ($ops as $op) {
- 256 if ($op['op']==='eq') {
- 257 $L=(string)($leftLines[$op['ai']] ?? '');
- 258 $R=(string)($rightLines[$op['bi']] ?? '');
- 259 $side[]=['lno'=>$li,'rno'=>$ri,'lclass'=>'ctx','rclass'=>'ctx','lhtml'=>$L,'rhtml'=>$R,'l_intra'=>false,'r_intra'=>false];
- 260 $uni[] =['lno'=>$li,'rno'=>$ri,'class'=>'ctx','html'=>$L,'intra'=>false];
- 261 $li++; $ri++;
- 262 } elseif ($op['op']==='del') {
- 263 $L=(string)($leftLines[$op['ai']] ?? '');
- 264 $side[]=['lno'=>$li,'rno'=>'','lclass'=>'del','rclass'=>'empty','lhtml'=>$L,'rhtml'=>'','l_intra'=>false,'r_intra'=>false];
- 265 $uni[] =['lno'=>$li,'rno'=>'','class'=>'del','html'=>$L,'intra'=>false];
- 266 $li++;
- 267 } else { // add
- 268 $R=(string)($rightLines[$op['bi']] ?? '');
- 269 $side[]=['lno'=>'','rno'=>$ri,'lclass'=>'empty','rclass'=>'add','lhtml'=>'','rhtml'=>$R,'l_intra'=>false,'r_intra'=>false];
- 270 $uni[] =['lno'=>'','rno'=>$ri,'class'=>'add','html'=>$R,'intra'=>false];
- 271 $ri++;
- 272 }
- 273 }
- 274 return [$side,$uni];
- 275}
- 276
- 277// Apply inline word/char diff across adjacent del/add in side-by-side rows.
- 278function apply_inline_sxs(array &$sideRows): void {
- 279 for ($i=0; $i<count($sideRows)-1; $i++) {
- 280 $a=$sideRows[$i]; $b=$sideRows[$i+1];
- 281 if ($a['lclass']==='del' && $b['rclass']==='add') {
- 282 [$L,$R] = inline_diff((string)$a['lhtml'], (string)$b['rhtml']);
- 283 $sideRows[$i]['lhtml']=$L; $sideRows[$i]['l_intra']=true;
- 284 $sideRows[$i+1]['rhtml']=$R; $sideRows[$i+1]['r_intra']=true;
- 285 $i++;
- 286 }
- 287 }
- 288}
- 289
- 290// Proper unified .diff (xdiff if present; else POSIX-ish fallback)
- 291function unified_diff_download(string $left, string $right, string $nameA='a', string $nameB='b', int $ctx=3): string {
- 292 if (function_exists('xdiff_string_diff')) {
- 293 // Use non-minimal for speed; we rewrite headers below
- 294 $ud = xdiff_string_diff($left, $right, $ctx, false);
- 295 if ($ud !== false) {
- 296 $ts = gmdate('Y-m-d H:i:s O');
- 297 $hdr = "--- {$nameA}\t{$ts}\n+++ {$nameB}\t{$ts}\n";
- 298 $ud = preg_replace('~^--- .*\R\+\+\+ .*\R~', $hdr, $ud, 1);
- 299 return $ud;
- 300 }
- 301 }
- 302
- 303 // Manual fallback
- 304 $aOrig = preg_split("/\R/u", $left);
- 305 $bOrig = preg_split("/\R/u", $right);
- 306 if ($aOrig === false) $aOrig = [$left];
- 307 if ($bOrig === false) $bOrig = [$right];
- 308
- 309 $ops = diff_lines_idx($aOrig, $bOrig, null);
- 310
- 311 $hunks = [];
- 312 $flush = static function (&$hunks, &$buf) {
- 313 if (empty($buf['lines'])) return;
- 314 $oldLen = max(0, $buf['oldLen']);
- 315 $newLen = max(0, $buf['newLen']);
- 316 $h = "@@ -{$buf['oldStart']}" . ($oldLen===1?'':",$oldLen")
- 317 . " +{$buf['newStart']}" . ($newLen===1?'':",$newLen") . " @@\n";
- 318 $h .= implode('', $buf['lines']);
- 319 $hunks[] = $h;
- 320 $buf = ['oldStart'=>0,'newStart'=>0,'oldLen'=>0,'newLen'=>0,'lines'=>[],'open'=>false];
- 321 };
- 322
- 323 $buf = ['oldStart'=>0,'newStart'=>0,'oldLen'=>0,'newLen'=>0,'lines'=>[],'open'=>false];
- 324 $ctxAhead = 0;
- 325 $ai=1; $bi=1;
- 326
- 327 $grab_context = static function($aOrig, $bOrig, $ai, $bi, $ctx) use (&$buf) {
- 328 $startA = max(1, $ai - $ctx);
- 329 $startB = max(1, $bi - $ctx);
- 330 $buf['oldStart'] = $startA;
- 331 $buf['newStart'] = $startB;
- 332 for ($k=0; $k<($ai-$startA); $k++) {
- 333 $buf['lines'][] = ' ' . rtrim((string)$aOrig[$startA-1+$k], "\r") . "\n";
- 334 }
- 335 $buf['oldLen'] += ($ai-$startA);
- 336 $buf['newLen'] += ($bi-$startB);
- 337 };
- 338
- 339 foreach ($ops as $op) {
- 340 if ($op['op'] === 'eq') {
- 341 if ($buf['open']) {
- 342 if ($ctxAhead < 3) {
- 343 $line = rtrim((string)$aOrig[$op['ai']], "\r");
- 344 $buf['lines'][] = ' ' . $line . "\n";
- 345 $buf['oldLen']++; $buf['newLen']++; $ctxAhead++;
- 346 } else {
- 347 $flush($hunks, $buf);
- 348 $ctxAhead = 0;
- 349 }
- 350 }
- 351 $ai++; $bi++;
- 352 } elseif ($op['op'] === 'del') {
- 353 if (!$buf['open']) { $buf['open']=true; $ctxAhead=0; $grab_context($aOrig, $bOrig, $ai, $bi, 3); }
- 354 $line = rtrim((string)$aOrig[$op['ai']], "\r");
- 355 $buf['lines'][] = '-' . $line . "\n";
- 356 $buf['oldLen']++; $ai++;
- 357 } else { // add
- 358 if (!$buf['open']) { $buf['open']=true; $ctxAhead=0; $grab_context($aOrig, $bOrig, $ai, $bi, 3); }
- 359 $line = rtrim((string)$bOrig[$op['bi']], "\r");
- 360 $buf['lines'][] = '+' . $line . "\n";
- 361 $buf['newLen']++; $bi++;
- 362 }
- 363 }
- 364 if ($buf['open']) $flush($hunks, $buf);
- 365
- 366 $ts = gmdate('Y-m-d H:i:s O');
- 367 $out = "--- {$nameA}\t{$ts}\n+++ {$nameB}\t{$ts}\n";
- 368 $out .= implode('', $hunks);
- 369 return $out;
- 370}
- 371
- 372// Render a single line with highlight.php if available (else plain-escaped).
- 373function hl_render_line(string $text, string $lang='text'): string {
- 374 global $highlighter;
- 375 static $hl = null;
- 376
- 377 $esc = static fn($s)=>htmlspecialchars($s, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8');
- 378
- 379 if (($highlighter ?? 'geshi') === 'highlight') {
- 380 if ($hl === null) $hl = make_highlighter();
- 381 if ($hl) {
- 382 try {
- 383 if ($lang && !in_array(strtolower($lang), ['autodetect','text','plaintext'], true)) {
- 384 $res = $hl->highlight($lang, $text);
- 385 return '<span class="hljs">'.$res->value.'</span>';
- 386 }
- 387 $res = $hl->highlightAuto($text);
- 388 return '<span class="hljs">'.$res->value.'</span>';
- 389 } catch (Throwable $e) { /* fall through */ }
- 390 }
- 391 }
- 392 return $esc($text);
- 393}
- 394
- 395/* =========================================================
- 396 * Page inputs
- 397 * =======================================================*/
- 398
- 399$left = '';
- 400$right = '';
- 401$leftLabel = 'Old';
- 402$rightLabel = 'New';
- 403
- 404/* ---------- Minimal site/bootstrap so header/footer look right ---------- */
- 405$mod_rewrite = $mod_rewrite ?? '0';
- 406$baseurl = $baseurl ?? '/';
- 407$site_name = $site_name ?? '';
- 408$title = 'Diff';
- 409$ges_style = ''; // keep themes CSS-only
- 410$ads_1 = $ads_2 = $text_ads = ''; // optional ad slots
- 411
- 412try {
- 413 // site_info
- 414 $stmt = $pdo->query("SELECT * FROM site_info WHERE id='1'");
- 415 if ($stmt) {
- 416 $si = $stmt->fetch() ?: [];
- 417 $title = trim($si['title'] ?? $title);
- 418 $des = trim($si['des'] ?? '');
- 419 $baseurl = rtrim(trim($si['baseurl'] ?? $baseurl), '/') . '/';
- 420 $keyword = trim($si['keyword'] ?? '');
- 421 $site_name = trim($si['site_name'] ?? $site_name);
- 422 $email = trim($si['email'] ?? '');
- 423 $twit = trim($si['twit'] ?? '');
- 424 $face = trim($si['face'] ?? '');
- 425 $gplus = trim($si['gplus'] ?? '');
- 426 $ga = trim($si['ga'] ?? '');
- 427 $additional_scripts = trim($si['additional_scripts'] ?? '');
- 428 if (isset($si['mod_rewrite']) && $si['mod_rewrite'] !== '') {
- 429 $mod_rewrite = (string)$si['mod_rewrite'];
- 430 }
- 431 }
- 432
- 433 // interface
- 434 $stmt = $pdo->query("SELECT * FROM interface WHERE id='1'");
- 435 if ($stmt) {
- 436 $iface = $stmt->fetch() ?: [];
- 437 $default_lang = trim($iface['lang'] ?? 'en.php');
- 438 $default_theme = trim($iface['theme'] ?? 'default');
- 439 if (is_file(__DIR__ . "/langs/$default_lang")) {
- 440 require_once __DIR__ . "/langs/$default_lang";
- 441 }
- 442 } else {
- 443 $default_theme = $default_theme ?? 'default';
- 444 }
- 445
- 446 // permissions
- 447 $stmt = $pdo->query("SELECT * FROM site_permissions WHERE id='1'");
- 448 if ($stmt) {
- 449 $perm = $stmt->fetch() ?: [];
- 450 $disableguest = trim($perm['disableguest'] ?? 'off');
- 451 $siteprivate = trim($perm['siteprivate'] ?? 'off');
- 452 }
- 453
- 454 // ads (optional)
- 455 $stmt = $pdo->query("SELECT * FROM ads WHERE id='1'");
- 456 if ($stmt) {
- 457 $ads = $stmt->fetch() ?: [];
- 458 $text_ads = trim($ads['text_ads'] ?? '');
- 459 $ads_1 = trim($ads['ads_1'] ?? '');
- 460 $ads_2 = trim($ads['ads_2'] ?? '');
- 461 }
- 462} catch (Throwable $e) {
- 463 // keep sane defaults, but don't break the page
- 464 error_log('diff.php bootstrap: ' . $e->getMessage());
- 465 $default_theme = $default_theme ?? 'default';
- 466}
- 467
- 468/* ---------- Paste IDs from query (supports ?a & ?b) ---------- */
- 469$lid = isset($_GET['a']) ? (int)$_GET['a'] : (isset($_GET['left_id']) ? (int)$_GET['left_id'] : 0);
- 470$rid = isset($_GET['b']) ? (int)$_GET['b'] : (isset($_GET['right_id']) ? (int)$_GET['right_id'] : 0);
- 471
- 472if ($lid) { $p = load_paste($pdo, $lid); if ($p){ $left=$p['content']; $leftLabel='Paste #'.$lid; } }
- 473if ($rid) { $p = load_paste($pdo, $rid); if ($p){ $right=$p['content']; $rightLabel='Paste #'.$rid; } }
- 474
- 475/* ---------- POST inputs (compare / download keeps buffers) ---------- */
- 476if ($_SERVER['REQUEST_METHOD']==='POST') {
- 477 $left = (string)($_POST['left_text'] ?? $left);
- 478 $right = (string)($_POST['right_text'] ?? $right);
- 479 $leftLabel = trim((string)($_POST['left_label'] ?? $leftLabel)) ?: $leftLabel;
- 480 $rightLabel = trim((string)($_POST['right_label'] ?? $rightLabel)) ?: $rightLabel;
- 481}
- 482
- 483/* ---------- Language engine + maps ---------- */
- 484$engine = function_exists('paste_current_engine') ? paste_current_engine() : ($highlighter ?? 'geshi');
- 485
- 486if ($engine === 'highlight') {
- 487 $langs = highlight_supported_languages();
- 488 $language_map = highlight_language_map($langs);
- 489 $alias_map = highlight_alias_map($langs);
- 490 $popular_langs = paste_popular_formats_highlight();
- 491} else {
- 492 $language_map = geshi_language_map();
- 493 $alias_map = geshi_alias_map($language_map);
- 494 $popular_langs = paste_popular_formats_geshi();
- 495}
- 496
- 497/* ---------- Picked languages ---------- */
- 498$lang_left = strtolower((string)($_POST['left_lang'] ?? $_GET['left_lang'] ?? 'autodetect'));
- 499$lang_right = strtolower((string)($_POST['right_lang'] ?? $_GET['right_lang'] ?? 'autodetect'));
- 500$lang_left = $alias_map[$lang_left] ?? 'autodetect';
- 501$lang_right = $alias_map[$lang_right] ?? 'autodetect';
- 502
- 503$lang_left_label = $language_map[$lang_left] ?? ucfirst($lang_left);
- 504$lang_right_label = $language_map[$lang_right] ?? ucfirst($lang_right);
- 505
- 506/* ---------- highlight.php default style (no picker) ---------- */
- 507if (($highlighter ?? 'geshi') === 'highlight') {
- 508 $hl_style = $hl_style ?? 'hybrid.css'; // header.php reads this
- 509}
- 510
- 511/* ---------- View options ---------- */
- 512$view_mode = ($_GET['view'] ?? 'side') === 'unified' ? 'unified' : 'side';
- 513$wrap = isset($_GET['wrap']) ? (int)$_GET['wrap'] : 0;
- 514$lineno = isset($_GET['lineno']) ? (int)$_GET['lineno'] : 1;
- 515
- 516/* ---------- Ignore trailing whitespace (toggle via ?ignore_ws=1) ---------- */
- 517$ignore_ws = isset($_GET['ignore_ws']) ? (int)$_GET['ignore_ws'] : 0;
- 518$normalizer = $ignore_ws ? static fn($s) => rtrim($s, " \t") : null;
- 519
- 520/* ---------- Persisted split percentage ---------- */
- 521$split_pct = 50.0;
- 522if (isset($_POST['split_pct'])) {
- 523 $split_pct = (float)$_POST['split_pct'];
- 524} elseif (isset($_COOKIE['diffSplitPct'])) {
- 525 $split_pct = (float)$_COOKIE['diffSplitPct'];
- 526}
- 527$split_pct = max(20.0, min(80.0, (float)$split_pct));
- 528setcookie('diffSplitPct', (string)$split_pct, [
- 529 'expires' => time() + 60*60*24*30,
- 530 'path' => '/',
- 531 'secure' => !empty($_SERVER['HTTPS']),
- 532 'httponly' => false,
- 533 'samesite' => 'Lax',
- 534]);
- 535
- 536/* ---------- Download unified diff ---------- */
- 537if (isset($_GET['download']) && $_GET['download'] === '1') {
- 538 $nameA = $leftLabel ?: 'Old';
- 539 $nameB = $rightLabel ?: 'New';
- 540 $ud = unified_diff_download($left, $right, $nameA, $nameB, 3);
- 541 // Surface engine in headers for debug
- 542 header('X-Diff-Engine: '.(function_exists('xdiff_string_diff') ? 'xdiff' : 'myers'));
- 543 header('X-Diff-Ignore-WS: '.($ignore_ws ? '1':'0'));
- 544 header('Content-Type: text/x-diff; charset=utf-8');
- 545 header('Content-Disposition: attachment; filename="paste.diff"');
- 546 echo $ud;
- 547 exit;
- 548}
- 549
- 550/* ---------- Compute opcodes ---------- */
- 551$leftLines = preg_split("/\R/u", $left);
- 552$rightLines = preg_split("/\R/u", $right);
- 553if ($leftLines === false) $leftLines = [$left];
- 554if ($rightLines === false) $rightLines = [$right];
- 555
- 556$ops = diff_lines_idx($leftLines, $rightLines, $normalizer);
- 557
- 558/* ---------- change counts & exposure ----------
- 559 * Treat an adjacent del-run followed by an add-run (or vice-versa) as "mods".
- 560 * Each paired line counts as 1 change in the total.
- 561 * Returns: [$adds, $dels, $mods, $total, $no_changes]
- 562 */
- 563function compute_change_counts(array $ops): array {
- 564 $adds = 0; $dels = 0; $mods = 0;
- 565 $n = count($ops);
- 566 for ($i = 0; $i < $n; ) {
- 567 $op = $ops[$i]['op'] ?? 'eq';
- 568 if ($op !== 'add' && $op !== 'del') { $i++; continue; }
- 569
- 570 // First run (all adds or all dels)
- 571 $t1 = $op;
- 572 $c1 = 0;
- 573 $j = $i;
- 574 while ($j < $n && ($ops[$j]['op'] ?? 'eq') === $t1) { $c1++; $j++; }
- 575
- 576 // Optional immediately-adjacent opposite run
- 577 $t2 = ($t1 === 'add') ? 'del' : 'add';
- 578 $c2 = 0;
- 579 $k = $j;
- 580 while ($k < $n && ($ops[$k]['op'] ?? 'eq') === $t2) { $c2++; $k++; }
- 581
- 582 // Pair min(c1,c2) as modifications
- 583 $pair = min($c1, $c2);
- 584 $mods += $pair;
- 585
- 586 if ($t1 === 'add') {
- 587 $adds += $c1 - $pair;
- 588 $dels += $c2 - $pair;
- 589 } else {
- 590 $dels += $c1 - $pair;
- 591 $adds += $c2 - $pair;
- 592 }
- 593
- 594 // Advance past both runs
- 595 $i = ($c2 > 0) ? $k : $j;
- 596 }
- 597
- 598 $total = $adds + $dels + $mods; // modified lines count as 1
- 599 $no_changes = ($total === 0);
- 600 return [$adds, $dels, $mods, $total, $no_changes];
- 601}
- 602
- 603/* ---------- change counts (mods collapse -/+ into 1) ---------- */
- 604[$adds, $dels, $mods, $changed_total, $no_changes] = compute_change_counts($ops);
- 605
- 606header('X-Diff-No-Changes: ' . ($no_changes ? '1' : '0'));
- 607header('X-Diff-Change-Add: ' . $adds);
- 608header('X-Diff-Change-Del: ' . $dels);
- 609header('X-Diff-Change-Mod: ' . $mods);
- 610header('X-Diff-Change-Total: ' . $changed_total);
- 611
- 612$GLOBALS['diff_no_changes'] = $no_changes;
- 613$GLOBALS['diff_changes_add'] = $adds;
- 614$GLOBALS['diff_changes_del'] = $dels;
- 615$GLOBALS['diff_changes_mod'] = $mods; // available if you want a separate badge
- 616$GLOBALS['diff_changes_total'] = $changed_total; // theme uses this for ±T
- 617
- 618/* ---------- Build tables server-side ---------- */
- 619[$sideRows, $uniRows] = build_tables_idx($ops, $leftLines, $rightLines);
- 620
- 621/* ---------- Limit expensive inline diff pass for very large diffs ---------- */
- 622$perform_inline = true;
- 623$totalBytes = strlen($left) + strlen($right);
- 624if (count($sideRows) > 4000 || $totalBytes > 4*1024*1024) {
- 625 $perform_inline = false;
- 626}
- 627if ($perform_inline) {
- 628 apply_inline_sxs($sideRows);
- 629}
- 630
- 631/* ---------- Expose engine + toggle info to theme and headers ---------- */
- 632$engine_is_xdiff = function_exists('xdiff_string_diff');
- 633$engine_label = $engine_is_xdiff ? 'xdiff' : 'myers';
- 634header('X-Diff-Engine: '.$engine_label);
- 635header('X-Diff-Ignore-WS: '.($ignore_ws ? '1':'0'));
- 636
- 637// Convenience strings the theme can show in the toolbar:
- 638$diff_engine_badge = '<span class="badge bg-secondary" title="Diff engine">'.$engine_label.'</span>';
- 639$ignore_ws_on = (bool)$ignore_ws;
- 640
- 641// Build toggle URL for ignore_ws (preserve other query params)
- 642$qs = $_GET;
- 643$qs['ignore_ws'] = $ignore_ws ? 0 : 1;
- 644$ignore_ws_toggle_url = strtok($_SERVER['REQUEST_URI'], '?') . '?' . http_build_query($qs);
- 645
- 646/* ---------- Render theme ---------- */
- 647$themeDir = 'theme/' . htmlspecialchars($default_theme ?? 'default', ENT_QUOTES, 'UTF-8');
- 648
- 649// expose split pct to the view if needed by JS
- 650$GLOBALS['split_pct'] = $split_pct;
- 651
- 652// badges/toggle url for the theme
- 653$GLOBALS['diff_engine_badge'] = $diff_engine_badge;
- 654$GLOBALS['ignore_ws_on'] = $ignore_ws_on;
- 655$GLOBALS['ignore_ws_toggle'] = $ignore_ws_toggle_url;
- 656
- 657require_once $themeDir . '/header.php';
- 658require_once $themeDir . '/diff.php';
- 659require_once $themeDir . '/footer.php';
Raw Paste