From 88ea01e72304bb0f71cd115483a776977230c178 Mon Sep 17 00:00:00 2001 From: Clarth Date: Fri, 16 Oct 2026 16:33:25 -0400 Subject: [PATCH] Upload files to "/" --- extensions.php | 209 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 extensions.php diff --git a/extensions.php b/extensions.php new file mode 100644 index 0000000..44d3177 --- /dev/null +++ b/extensions.php @@ -0,0 +1,209 @@ + '1 (normal)', 2 => '2', 4 => '4', 6 => '6', 9 => '9'] as $n => $label) { + $opts .= ''; + } + return ''; +} + +function ext_lp_args(string $ext): array { + $n = (int)($_POST['nup'] ?? 1); + return (EXT_PAGES_PER_SHEET && in_array($n, [2, 4, 6, 9], true)) ? ['-o', 'number-up=' . $n] : []; +} + +function ext_file_html(string $name): string { + if (!EXT_PREVIEW) { return ''; } + return 'Preview'; +} + +/** + * Called for every upload. Returns null to leave the file alone, ['error' => msg] on failure, + * or ['path' => converted pdf, 'name' => new filename] after converting an Office file. + */ +function ext_convert(string $tmp, string $orig): ?array { + $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); + if (!EXT_OFFICE || !in_array($ext, EXT_OFFICE_TYPES, true)) { return null; } + if (!is_executable(EXT_SOFFICE)) { return ['error' => 'can’t be converted: LibreOffice isn’t installed on the server.']; } + + @set_time_limit(EXT_TIMEOUT * 2); + $tmpdir = sys_get_temp_dir(); + $profile = $tmpdir . '/print-lo-profile'; + $work = $tmpdir . '/print-lo-' . bin2hex(random_bytes(6)); + $lock = fopen($tmpdir . '/print-lo.lock', 'c'); + if ($lock) { flock($lock, LOCK_EX); } // LibreOffice allows one run per profile at a time + + $result = ['error' => 'could not be converted to PDF. Try saving it as a PDF first.']; + if (@mkdir($work, 0700, true) && copy($tmp, $work . '/document.' . $ext)) { + $cmd = ['/usr/bin/timeout', '-k', '5', (string)EXT_TIMEOUT, EXT_SOFFICE, + '-env:UserInstallation=file://' . $profile, '--headless', '--norestore', + '--convert-to', 'pdf', '--outdir', $work, $work . '/document.' . $ext]; + $p = @proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $work, + ['PATH' => '/usr/bin:/bin', 'HOME' => $work, 'LC_ALL' => 'C.UTF-8']); + if (is_resource($p)) { + stream_get_contents($pipes[1]); stream_get_contents($pipes[2]); + fclose($pipes[1]); fclose($pipes[2]); proc_close($p); + } + $pdf = $work . '/document.pdf'; + $dest = tempnam($tmpdir, 'prt'); + if (is_file($pdf) && filesize($pdf) > 0 && $dest !== false && copy($pdf, $dest)) { + $result = ['path' => $dest, 'name' => cut(pathinfo($orig, PATHINFO_FILENAME), 80) . '.pdf']; + } else { + ext_rmdir($profile); // a crashed run can leave a broken profile; start fresh next time + } + } + ext_rmdir($work); + if ($lock) { flock($lock, LOCK_UN); fclose($lock); } + return $result; +} + +function ext_css(): string { + return '[hidden]{display:none!important}' + . '@media (max-width:480px){.file{flex-wrap:wrap}.fn{flex-basis:100%}}' + . '#pv{position:fixed;inset:0;z-index:50;display:flex;flex-direction:column;background:var(--bg)}' + . '.pvbar{display:flex;align-items:center;gap:8px;padding:max(10px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 10px max(12px,env(safe-area-inset-left));background:var(--card);border-bottom:1px solid var(--line)}' + . '.pvbar b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' + . '.pvbar span{color:var(--mute);font-size:.88rem;white-space:nowrap}' + . '#pvbody{flex:1;overflow:auto;padding:12px;text-align:center;-webkit-overflow-scrolling:touch}' + . '#pvbody canvas,#pvbody img{display:block;height:auto;max-width:100%;margin:0 auto 12px;background:#fff;border:1px solid var(--line)}' + . '#pvbody pre{margin:0 auto;max-width:90ch;text-align:left;white-space:pre-wrap;overflow-wrap:anywhere;font:.9rem/1.45 ui-monospace,monospace;color:var(--ink)}'; +} + +/** Preview overlay + small scripts (remembers each device's option choices). */ +function ext_page_html(): string { + $preview = !EXT_PREVIEW ? '' : <<<'HTML' + + +HTML; + + $remember = <<<'HTML' + +HTML; + return $preview . "\n" . $remember; +} + +// ---------- Internals ---------- + +function ext_serve(string $name): void { + $path = stored_path($name); + $ext = $path === null ? '' : strtolower(pathinfo($path, PATHINFO_EXTENSION)); + if ($path === null || !isset(ALLOWED[$ext])) { http_response_code(404); exit('Not found'); } + header_remove('Content-Security-Policy'); + header('Content-Type: ' . ALLOWED[$ext][0] . ($ext === 'txt' ? '; charset=utf-8' : '')); + header('Content-Length: ' . (string)filesize($path)); + header("Content-Disposition: inline; filename*=UTF-8''" . rawurlencode(basename($path))); + readfile($path); + exit; +} + +function ext_asset(string $name): void { + $file = EXT_VENDOR . '/' . $name; + if (!in_array($name, ['pdf.min.mjs', 'pdf.worker.min.mjs'], true) || !is_file($file)) { http_response_code(404); exit('Not found'); } + header_remove('Content-Security-Policy'); + header('Content-Type: text/javascript; charset=utf-8'); + header('Cache-Control: public, max-age=86400'); + header('Content-Length: ' . (string)filesize($file)); + readfile($file); + exit; +} + +function ext_rmdir(string $dir): void { + if (!is_dir($dir)) { return; } + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST); + foreach ($it as $f) { ($f->isDir() && !$f->isLink()) ? @rmdir($f->getPathname()) : @unlink($f->getPathname()); } + @rmdir($dir); +}