MIME types accepted (CUPS handles these without extra packages) const ALLOWED = [ 'pdf' => ['application/pdf'], 'txt' => ['text/plain'], 'jpg' => ['image/jpeg'], 'jpeg' => ['image/jpeg'], 'png' => ['image/png'], 'gif' => ['image/gif'], ]; // ---------- Optional add-ons (extensions.php). The portal works fine without it. ---------- if (is_file(__DIR__ . '/extensions.php')) { require __DIR__ . '/extensions.php'; } if (!function_exists('ext_boot')) { function ext_boot(): void {} } if (!function_exists('ext_css')) { function ext_css(): string { return ''; } } if (!function_exists('ext_form_html')) { function ext_form_html(): string { return ''; } } if (!function_exists('ext_file_html')) { function ext_file_html(string $name): string { return ''; } } if (!function_exists('ext_lp_args')) { function ext_lp_args(string $ext): array { return []; } } if (!function_exists('ext_accept')) { function ext_accept(): string { return ''; } } if (!function_exists('ext_hint')) { function ext_hint(): string { return ''; } } if (!function_exists('ext_page_html')) { function ext_page_html(): string { return ''; } } if (!function_exists('ext_convert')) { function ext_convert(string $tmp, string $orig): ?array { return null; } } // ---------- Helpers ---------- function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function run(array $cmd): array { $env = ['PATH' => '/usr/bin:/bin', 'LC_ALL' => 'C']; if (CUPS_SERVER !== '') { $env['CUPS_SERVER'] = CUPS_SERVER; } $p = @proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, null, $env); if (!is_resource($p)) { return [1, '', 'Could not start ' . basename($cmd[0])]; } $out = stream_get_contents($pipes[1]); $err = stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); return [proc_close($p), (string)$out, trim((string)$err)]; } function printers(): array { [, $out] = run([LPSTAT, '-p']); $list = []; if (preg_match_all('/^printer (\S+) (.*)$/m', $out, $m, PREG_SET_ORDER)) { foreach ($m as $row) { $state = 'Ready'; if (stripos($row[2], 'disabled') !== false) { $state = 'Offline'; } elseif (stripos($row[2], 'now printing') !== false) { $state = 'Printing'; } $list[$row[1]] = $state; } } return $list; } function default_printer(): string { [, $out] = run([LPSTAT, '-d']); return preg_match('/destination: (\S+)/', $out, $m) ? $m[1] : ''; } function jobs(): array { [, $out] = run([LPSTAT, '-o']); $list = []; foreach (preg_split('/\R/', trim($out)) as $line) { if (preg_match('/^(\S+-\d+)\s+(\S+)\s+(\d+)/', $line, $m)) { $list[] = ['id' => $m[1], 'user' => $m[2], 'kb' => max(1, (int)round(((int)$m[3]) / 1024))]; } } return $list; } function flash(string $type, string $msg): void { $_SESSION['flash'][] = [$type, $msg]; } function cut(string $s, int $n): string { return preg_match('/^.{0,' . $n . '}/su', $s, $m) ? $m[0] : substr($s, 0, $n); } function fmt_size(int $b): string { return $b >= 1048576 ? round($b / 1048576, 1) . ' MB' : max(1, (int)round($b / 1024)) . ' KB'; } function storage_ready(): bool { return (is_dir(STORE_DIR) || @mkdir(STORE_DIR, 0775, true)) && is_writable(STORE_DIR); } function safe_name(string $orig, string $ext): string { $base = trim((string)preg_replace('/[^\w .()-]+/u', '_', pathinfo($orig, PATHINFO_FILENAME)), " ._-"); return cut($base === '' ? 'file' : $base, 80) . '.' . $ext; } function unique_path(string $name): string { $p = STORE_DIR . '/' . $name; if (!file_exists($p)) { return $p; } $base = pathinfo($name, PATHINFO_FILENAME); $ext = pathinfo($name, PATHINFO_EXTENSION); for ($i = 2; $i < 1000; $i++) { $p = STORE_DIR . "/$base-$i.$ext"; if (!file_exists($p)) { return $p; } } return STORE_DIR . "/$base-" . bin2hex(random_bytes(3)) . ".$ext"; } function stored_path(string $name): ?string { if ($name === '' || $name[0] === '.' || $name !== basename($name) || strpos($name, "\0") !== false) { return null; } $p = STORE_DIR . '/' . $name; return is_file($p) ? $p : null; } function list_files(string $q, int $limit): array { $terms = $q === '' ? [] : preg_split('/\s+/', $q); $out = []; if (is_dir(STORE_DIR)) { foreach (new DirectoryIterator(STORE_DIR) as $f) { $name = $f->getFilename(); if (!$f->isFile() || $name[0] === '.' || !isset(ALLOWED[strtolower($f->getExtension())])) { continue; } foreach ($terms as $t) { if (!preg_match('/' . preg_quote($t, '/') . '/iu', $name)) { continue 2; } } $out[] = ['name' => $name, 'time' => $f->getMTime(), 'size' => $f->getSize()]; } } usort($out, function ($a, $b) { return $b['time'] <=> $a['time']; }); return ['total' => count($out), 'items' => array_slice($out, 0, $limit)]; } function send_job(string $path, string $dest, int $copies, string $pages, bool $duplex, bool $mono): void { $name = basename($path); $label = '“' . cut($name, 60) . '”'; $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if (!isset(ALLOWED[$ext]) || !in_array((new finfo(FILEINFO_MIME_TYPE))->file($path), ALLOWED[$ext], true)) { flash('err', "$label isn’t a printable file."); return; } $cmd = [LP, '-d', $dest, '-n', (string)$copies, '-t', cut((string)preg_replace('/[^\w .-]/u', '_', $name), 80)]; $cmd[] = '-o'; $cmd[] = $duplex ? 'sides=two-sided-long-edge' : 'sides=one-sided'; if ($mono) { $cmd[] = '-o'; $cmd[] = 'print-color-mode=monochrome'; } if ($pages !== '') { $cmd[] = '-o'; $cmd[] = 'page-ranges=' . $pages; } if ($ext !== 'pdf' && $ext !== 'txt') { $actual = isset($_GET['admin']) && ($_POST['actual'] ?? '') === '1'; // admin only: natural size instead of fit $cmd[] = '-o'; $cmd[] = $actual ? 'print-scaling=none' : 'fit-to-page'; } foreach (ext_lp_args($ext) as $arg) { $cmd[] = $arg; } $cmd[] = $path; [$code, , $err] = run($cmd); if ($code === 0) { flash('ok', "$label sent to $dest."); } else { flash('err', "$label failed: " . ($err ?: 'unknown printing error') . '.'); } } function upload_error(int $code): string { switch ($code) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: return 'is too large'; case UPLOAD_ERR_PARTIAL: return 'was only partly received — try again'; case UPLOAD_ERR_NO_TMP_DIR: case UPLOAD_ERR_CANT_WRITE: return 'could not be saved on the server'; default: return 'could not be uploaded'; } } // ---------- Session / CSRF ---------- if (TIMEZONE !== '') { @date_default_timezone_set(TIMEZONE); } else { $tzf = trim((string)@file_get_contents('/etc/timezone')); if ($tzf !== '' && in_array($tzf, timezone_identifiers_list(), true)) { date_default_timezone_set($tzf); } } session_name('lanprint'); session_set_cookie_params(['lifetime' => 0, 'path' => '/', 'httponly' => true, 'samesite' => 'Strict']); session_start(); if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(16)); } header('X-Content-Type-Options: nosniff'); /* * Permit this print portal to load inside Organizr, * as long as both are accessed from the same scheme/host/port. header('X-Frame-Options: SAMEORIGIN'); header( "Content-Security-Policy: " . "default-src 'none'; " . "style-src 'unsafe-inline'; " . "script-src 'unsafe-inline'; " . "form-action 'self'; " . "base-uri 'none'; " . "frame-ancestors 'self'" ); header('Referrer-Policy: no-referrer'); header('Cache-Control: no-store'); */ ext_boot(); // ---------- Actions ---------- $q = cut(trim((string)($_POST['q'] ?? $_GET['q'] ?? '')), 100); $admin = isset($_GET['admin']); // Delete buttons only appear at /print/?admin if ($_SERVER['REQUEST_METHOD'] === 'POST') { $printers = printers(); $action = (string)($_POST['action'] ?? ''); if ((int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0 && !$_POST && !$_FILES) { flash('err', 'That upload was too large. The limit is ' . MAX_MB . ' MB in total.'); } elseif (!hash_equals($_SESSION['csrf'], (string)($_POST['csrf'] ?? ''))) { flash('err', 'This page expired. Please try again.'); } elseif ($action === 'cancel') { $job = (string)($_POST['job'] ?? ''); if (preg_match('/^[A-Za-z0-9_.-]+-\d+$/', $job)) { [$code, , $err] = run([CANCEL, $job]); $code === 0 ? flash('ok', 'Job cancelled.') : flash('err', 'Could not cancel the job. ' . $err); } } elseif ($action === 'delete') { $path = $admin ? stored_path((string)($_POST['file'] ?? '')) : null; if ($path !== null && @unlink($path)) { flash('ok', 'Deleted “' . cut(basename($path), 60) . '”.'); } else { flash('err', 'Could not delete that file.'); } } else { $dest = PRINTER !== '' ? PRINTER : (string)($_POST['printer'] ?? default_printer()); $copies = max(1, min(MAX_COPIES, (int)($_POST['copies'] ?? 1))); $pages = preg_replace('/\s+/', '', (string)($_POST['pages'] ?? '')); $duplex = ($_POST['duplex'] ?? '') === '1'; $mono = ($_POST['mono'] ?? '') === '1'; $reprint = (string)($_POST['reprint'] ?? ''); $names = (array)($_FILES['files']['name'] ?? []); if (!isset($printers[$dest])) { flash('err', 'No printer is available. Ask the person who runs this server.'); } elseif ($pages !== '' && !preg_match('/^\d+(-\d+)?(,\d+(-\d+)?)*$/', $pages)) { flash('err', 'Pages should look like 1-3,5 — leave empty to print everything.'); } elseif ($reprint !== '') { $path = stored_path($reprint); if ($path === null) { flash('err', 'That file is no longer in the folder.'); } else { @touch($path); send_job($path, $dest, $copies, $pages, $duplex, $mono); } } elseif (!$names || (string)$names[0] === '') { flash('err', 'Choose at least one file first.'); } elseif (count($names) > MAX_FILES) { flash('err', 'Please send at most ' . MAX_FILES . ' files at a time.'); } elseif (!storage_ready()) { flash('err', 'The save folder is missing or not writable: ' . STORE_DIR); } else { $finfo = new finfo(FILEINFO_MIME_TYPE); foreach ($names as $i => $orig) { $orig = (string)$orig; $label = '“' . cut($orig, 60) . '”'; $tmp = (string)$_FILES['files']['tmp_name'][$i]; $errc = (int)$_FILES['files']['error'][$i]; if ($errc !== UPLOAD_ERR_OK) { flash('err', "$label " . upload_error($errc) . '.'); continue; } $conv = ext_convert($tmp, $orig); // Office files become PDFs (see extensions.php) if ($conv !== null) { if (isset($conv['error'])) { flash('err', "$label " . $conv['error']); continue; } $tmp = $conv['path']; $orig = $conv['name']; $label = '“' . cut($orig, 60) . '”'; } $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); if (!isset(ALLOWED[$ext])) { $hint = in_array($ext, ['heic', 'heif'], true) ? ' Photos in HEIC format must be shared as JPEG.' : ''; flash('err', "$label can’t be printed here (accepted: PDF, TXT, JPG, PNG, GIF)." . $hint); continue; } if (!in_array($finfo->file($tmp), ALLOWED[$ext], true)) { flash('err', "$label doesn’t look like a real .$ext file."); continue; } $path = unique_path(safe_name($orig, $ext)); if (!($conv !== null ? @rename($tmp, $path) : move_uploaded_file($tmp, $path))) { flash('err', "$label could not be saved on the server."); continue; } @chmod($path, 0664); send_job($path, $dest, $copies, $pages, $duplex, $mono); } } } $qs = []; if ($q !== '') { $qs[] = 'q=' . rawurlencode($q); } if ($admin) { $qs[] = 'admin'; } $loc = (strtok($_SERVER['REQUEST_URI'], '?') ?: '/') . ($qs ? '?' . implode('&', $qs) : ''); header('Location: ' . $loc, true, 303); exit; } // ---------- Page data ---------- $printers = printers(); $default = PRINTER !== '' ? PRINTER : default_printer(); if (!isset($printers[$default])) { $default = (string)(array_key_first($printers) ?? ''); } $jobs = SHOW_QUEUE ? jobs() : []; $flashes = $_SESSION['flash'] ?? []; unset($_SESSION['flash']); $state = $printers[$default] ?? 'Offline'; $files = list_files($q, $q === '' ? RECENT_COUNT : 50); ?> <?= h(SITE_TITLE) ?>

·

No printer found

The server can’t see any printer. Check that CUPS is running and a printer is added.

1): ?>

Print uses the copies and options above.

·
count($files['items'])): ?>

Showing of

Waiting to print


· KB

Printed files are saved on the server so you can print them again.