mirror of
https://github.com/ThunixdotNet/www.git
synced 2026-01-23 23:10:17 +00:00
added terminal interface as default. old website as no-js fallback
This commit is contained in:
27
terminal/api/_bootstrap.php
Normal file
27
terminal/api/_bootstrap.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
$rootDir = dirname(__DIR__, 2);
|
||||
if (!is_dir($rootDir)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Invalid root directory'], JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
function json_out(array $data, int $status = 200): void
|
||||
{
|
||||
http_response_code($status);
|
||||
try {
|
||||
echo json_encode(
|
||||
$data,
|
||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo '{"error":"JSON encoding failed"}';
|
||||
}
|
||||
exit;
|
||||
}
|
||||
58
terminal/api/menu.php
Normal file
58
terminal/api/menu.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/terminal/_bootstrap.php';
|
||||
|
||||
$file = $rootDir . '/includes/sidebar.md';
|
||||
if (!is_file($file)) {
|
||||
json_out(['sections' => []]);
|
||||
}
|
||||
|
||||
$md = file_get_contents($file);
|
||||
if ($md === false) {
|
||||
json_out(['sections' => []]);
|
||||
}
|
||||
|
||||
$lines = preg_split('/\r\n|\n|\r/', $md) ?: [];
|
||||
$sections = [];
|
||||
|
||||
$current = null;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$raw = rtrim($line);
|
||||
|
||||
// Section header: "- Title"
|
||||
if (preg_match('/^\-\s{2,}(.+)$/', $raw, $m)) {
|
||||
if ($current !== null) {
|
||||
$sections[] = $current;
|
||||
}
|
||||
$current = [
|
||||
'title' => trim($m[1]),
|
||||
'items' => [],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Menu item: " - [Text](Href)"
|
||||
if ($current !== null && preg_match('/^\s+\-\s{2,}\[(.+?)\]\((.+?)\)\s*$/', $raw, $m)) {
|
||||
$text = trim($m[1]);
|
||||
$href = trim($m[2]);
|
||||
|
||||
$internal = str_starts_with($href, '/');
|
||||
$slug = $internal ? ltrim(parse_url($href, PHP_URL_PATH) ?? '', '/') : '';
|
||||
|
||||
$current['items'][] = [
|
||||
'text' => $text,
|
||||
'href' => $href,
|
||||
'internal' => $internal,
|
||||
'slug' => $slug,
|
||||
];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($current !== null) {
|
||||
$sections[] = $current;
|
||||
}
|
||||
|
||||
json_out(['sections' => $sections]);
|
||||
53
terminal/api/page.php
Normal file
53
terminal/api/page.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$p = $_GET['p'] ?? '';
|
||||
if (!is_string($p)) {
|
||||
json_out(['error' => 'Invalid page'], 400);
|
||||
}
|
||||
|
||||
$slug = trim($p);
|
||||
$slug = ltrim($slug, '/');
|
||||
$slug = preg_replace('/\?.*$/', '', $slug) ?? $slug;
|
||||
|
||||
if ($slug === '' || !preg_match('/^[A-Za-z0-9][A-Za-z0-9_-]*$/', $slug)) {
|
||||
json_out(['error' => 'Invalid page'], 400);
|
||||
}
|
||||
|
||||
if (preg_match('/^success\d+$/', $slug) === 1) {
|
||||
json_out(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$file = $rootDir . '/articles/' . $slug . '.md';
|
||||
if (!is_file($file)) {
|
||||
json_out(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$md = file_get_contents($file);
|
||||
if ($md === false) {
|
||||
json_out(['error' => 'Failed to read page'], 500);
|
||||
}
|
||||
|
||||
$parsedownPath = $rootDir . '/parsedown-1.7.3/Parsedown.php';
|
||||
$extraPath = $rootDir . '/parsedown-extra-0.7.1/ParsedownExtra.php';
|
||||
|
||||
if (is_file($parsedownPath)) {
|
||||
require_once $parsedownPath;
|
||||
}
|
||||
if (is_file($extraPath)) {
|
||||
require_once $extraPath;
|
||||
}
|
||||
|
||||
$html = '';
|
||||
if (class_exists('ParsedownExtra')) {
|
||||
$pd = new ParsedownExtra();
|
||||
$html = $pd->text($md);
|
||||
}
|
||||
|
||||
json_out([
|
||||
'slug' => $slug,
|
||||
'markdown' => $md,
|
||||
'html' => $html,
|
||||
]);
|
||||
51
terminal/api/pages.php
Normal file
51
terminal/api/pages.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$articlesDir = $rootDir . '/articles';
|
||||
if (!is_dir($articlesDir)) {
|
||||
json_out(['pages' => []]);
|
||||
}
|
||||
|
||||
$pages = [];
|
||||
$files = glob($articlesDir . '/*.md') ?: [];
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$slug = basename($file, '.md');
|
||||
|
||||
if (preg_match('/^success\d+$/', $slug) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = $slug;
|
||||
|
||||
$fh = fopen($file, 'rb');
|
||||
if ($fh !== false) {
|
||||
for ($i = 0; $i < 20; $i++) {
|
||||
$line = fgets($fh);
|
||||
if ($line === false) {
|
||||
break;
|
||||
}
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^#\s+(.+)$/', $line, $m)) {
|
||||
$title = trim($m[1]);
|
||||
break;
|
||||
}
|
||||
$title = mb_substr($line, 0, 80);
|
||||
break;
|
||||
}
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
$pages[] = [
|
||||
'slug' => $slug,
|
||||
'title' => $title,
|
||||
];
|
||||
}
|
||||
|
||||
json_out(['pages' => $pages]);
|
||||
49
terminal/api/server.php
Normal file
49
terminal/api/server.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$candidates = [
|
||||
$rootDir . '/report',
|
||||
$rootDir . '/includes/report',
|
||||
];
|
||||
|
||||
$report = null;
|
||||
foreach ($candidates as $cand) {
|
||||
if (is_file($cand)) {
|
||||
$report = $cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($report === null) {
|
||||
json_out(['rows' => [], 'lastUpdated' => null]);
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$fh = fopen($report, 'rb');
|
||||
if ($fh === false) {
|
||||
json_out(['rows' => [], 'lastUpdated' => null]);
|
||||
}
|
||||
|
||||
while (($line = fgets($fh)) !== false) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = str_getcsv($line);
|
||||
if (count($parts) < 3) {
|
||||
continue;
|
||||
}
|
||||
$rows[] = [
|
||||
'host' => (string)$parts[0],
|
||||
'check' => (string)$parts[1],
|
||||
'status' => (string)$parts[2],
|
||||
];
|
||||
}
|
||||
fclose($fh);
|
||||
|
||||
$mtime = @filemtime($report);
|
||||
$last = $mtime ? gmdate('c', (int)$mtime) : null;
|
||||
|
||||
json_out(['rows' => $rows, 'lastUpdated' => $last]);
|
||||
42
terminal/api/users.php
Normal file
42
terminal/api/users.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$siteRoot = '//' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||
|
||||
$skelIndex = '/etc/skel/public_html/index.html';
|
||||
$skelIndexCksum = is_file($skelIndex) ? @md5_file($skelIndex) : null;
|
||||
|
||||
$users = [];
|
||||
$homes = glob('/home/*', GLOB_ONLYDIR) ?: [];
|
||||
|
||||
foreach ($homes as $homeDir) {
|
||||
$user = basename($homeDir);
|
||||
if ($user === '' || $user === 'lost+found') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userIndex = $homeDir . '/public_html/index.html';
|
||||
$userPub = $homeDir . '/public_html';
|
||||
|
||||
if (!is_dir($userPub)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasCustomIndex = false;
|
||||
if (is_file($userIndex) && $skelIndexCksum !== null) {
|
||||
$userCksum = @md5_file($userIndex);
|
||||
$hasCustomIndex = ($userCksum !== false && $userCksum !== $skelIndexCksum);
|
||||
} elseif (is_file($userIndex)) {
|
||||
$hasCustomIndex = true;
|
||||
}
|
||||
|
||||
$users[] = [
|
||||
'username' => $user,
|
||||
'url' => $siteRoot . '/~' . rawurlencode($user) . '/',
|
||||
'hasContent' => $hasCustomIndex,
|
||||
];
|
||||
}
|
||||
|
||||
json_out(['users' => $users]);
|
||||
159
terminal/index.php
Normal file
159
terminal/index.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
||||
|
||||
?><!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>thunix terminal</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
|
||||
<noscript><meta http-equiv="refresh" content="0;url=/main"></noscript>
|
||||
<!-- xterm core styles -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.css">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--mono: "Departure Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bg: #111;
|
||||
--amber: #edb200;
|
||||
--amber-txt: #ffc828;
|
||||
--panel: rgba(255, 255, 255, 0.03);
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
*{ box-sizing:border-box; }
|
||||
|
||||
html, body{
|
||||
margin:0;
|
||||
padding:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
overflow:hidden;
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
body::before{
|
||||
content:'';
|
||||
position:fixed;
|
||||
inset:0;
|
||||
pointer-events:none;
|
||||
z-index:2;
|
||||
background: linear-gradient(to bottom, transparent 50%, rgba(0,0,0,0.32) 51%);
|
||||
background-size: 100% 4px;
|
||||
}
|
||||
|
||||
#wrap{
|
||||
position:relative;
|
||||
z-index:1;
|
||||
height:100%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
padding: 6vh 0 6vh 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#head{
|
||||
width: min(92vw, 1400px);
|
||||
margin: 0 auto;
|
||||
font-size: clamp(18px, 3vw, 44px);
|
||||
text-shadow: 0 0 1.75rem rgba(237,178,0,0.55);
|
||||
line-height:1.1;
|
||||
}
|
||||
#head small{
|
||||
display:block;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.85;
|
||||
margin-top: 8px;
|
||||
text-shadow:none;
|
||||
}
|
||||
|
||||
#main{
|
||||
width: min(92vw, 1400px);
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 1.25fr;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#terminalHost,
|
||||
#contentHost{
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 24px rgba(237,178,0,0.09);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#terminal{ height: 100%; width: 100%; }
|
||||
|
||||
#contentFrame{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
@media (max-width: 1050px){
|
||||
#main{ grid-template-columns: 1fr; grid-template-rows: 48vh 1fr; }
|
||||
}
|
||||
|
||||
#terminalHost .xterm-rows a,
|
||||
#terminalHost .xterm-screen a{
|
||||
color: var(--amber-txt) !important;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 3px;
|
||||
text-shadow: 0 0 0.85rem rgba(237,178,0,0.30);
|
||||
}
|
||||
#terminalHost .xterm-rows a:hover,
|
||||
#terminalHost .xterm-screen a:hover{
|
||||
filter: brightness(1.08);
|
||||
text-shadow: 0 0 1.25rem rgba(237,178,0,0.55);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<noscript>
|
||||
<div style="max-width: 900px; margin: 2rem auto; padding: 1.25rem 1.5rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 14px; background: rgba(0,0,0,0.55); color: #e6e6e6; font-family: system-ui, -apple-system, Segoe UI, sans-serif;">
|
||||
<h1 style="margin: 0 0 0.6rem 0; font-size: 1.35rem; font-weight: 700;">Terminal mode needs JavaScript</h1>
|
||||
<p style="margin: 0; opacity: 0.9; line-height: 1.4;">JavaScript is disabled, so the terminal UI can’t run. Redirecting you to the classic site… If you’re not redirected, use <a href="/main" style="color: #6ec5ff; text-decoration: underline;">the classic site</a>.</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="wrap">
|
||||
<div id="head">
|
||||
🌻 thunix
|
||||
<small>Type <strong>help</strong>. Click inside the terminal to focus.</small>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="terminalHost">
|
||||
<div id="terminal" aria-label="Terminal"></div>
|
||||
</div>
|
||||
|
||||
<div id="contentHost" aria-label="Content">
|
||||
<iframe
|
||||
id="contentFrame"
|
||||
src="/terminal/view.php?page=main"
|
||||
title="thunix content"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="eager"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/terminal/terminal.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
545
terminal/terminal.js
Normal file
545
terminal/terminal.js
Normal file
@@ -0,0 +1,545 @@
|
||||
// thunix terminal UI powered by xterm.js (ESM via jsDelivr).
|
||||
|
||||
import { Terminal } from "https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/+esm";
|
||||
import { FitAddon } from "https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.11.0/+esm";
|
||||
import { WebLinksAddon } from "https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.12.0/+esm";
|
||||
|
||||
const MODULE_BASE = new URL(".", import.meta.url);
|
||||
const urlFromBase = (path) => new URL(path, MODULE_BASE).toString();
|
||||
|
||||
const DEFAULT_WEBMAIL_PATH = "/webmail/";
|
||||
|
||||
const ESC = "\x1b";
|
||||
const CSI = `${ESC}[`;
|
||||
const OSC = `${ESC}]`;
|
||||
|
||||
const ANSI = {
|
||||
reset: `${CSI}0m`,
|
||||
bold: `${CSI}1m`,
|
||||
dim: `${CSI}2m`,
|
||||
underline: `${CSI}4m`,
|
||||
noUnderline: `${CSI}24m`,
|
||||
clrLine: `${CSI}2K`,
|
||||
fgBrightYellow: `${CSI}93m`,
|
||||
fgWhite: `${CSI}37m`,
|
||||
};
|
||||
|
||||
function osc8(url, text) {
|
||||
const BEL = "\x07";
|
||||
return `${OSC}8;;${url}${BEL}${text}${OSC}8;;${BEL}`;
|
||||
}
|
||||
|
||||
function absolutizeUrl(href) {
|
||||
if (!href) return href;
|
||||
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(href)) return href;
|
||||
if (href.startsWith("//")) return `${location.protocol}${href}`;
|
||||
if (href.startsWith("/")) return `${location.origin}${href}`;
|
||||
return new URL(href, location.href).toString();
|
||||
}
|
||||
|
||||
function renderInlineLinks(line) {
|
||||
return line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, href) => {
|
||||
const url = absolutizeUrl(String(href).trim());
|
||||
const label = `${ANSI.underline}${ANSI.fgBrightYellow}${text}${ANSI.reset}`;
|
||||
return osc8(url, label);
|
||||
});
|
||||
}
|
||||
|
||||
function stripHtmlToText(html) {
|
||||
let s = String(html || "");
|
||||
|
||||
s = s.replace(/<\s*br\s*\/?\s*>/gi, "\n");
|
||||
s = s.replace(/<\s*\/(p|div|tr|li|table|form|h1|h2|h3|ul|ol)\s*>/gi, "\n");
|
||||
s = s.replace(/<\s*(p|div|tr|li|table|form|h1|h2|h3|ul|ol)(\s[^>]*)?>/gi, "\n");
|
||||
|
||||
s = s.replace(/<a\s+[^>]*href=['"]([^'"]+)['"][^>]*>(.*?)<\/a>/gi, (_m, href, text) => {
|
||||
const cleanText = String(text).replace(/<[^>]+>/g, "").trim() || href;
|
||||
return `[${cleanText}](${href})`;
|
||||
});
|
||||
|
||||
s = s.replace(/<[^>]+>/g, "");
|
||||
|
||||
s = s.replace(/ /g, " ");
|
||||
s = s.replace(/&/g, "&");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/"/g, '"');
|
||||
s = s.replace(/'/g, "'");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function renderMarkdown(md) {
|
||||
const input = stripHtmlToText(md);
|
||||
const out = [];
|
||||
const lines = input.replace(/\r\n/g, "\n").split("\n");
|
||||
|
||||
let inCode = false;
|
||||
|
||||
for (const raw of lines) {
|
||||
let line = raw;
|
||||
|
||||
if (/^\s*```/.test(line)) {
|
||||
inCode = !inCode;
|
||||
out.push(inCode ? `${ANSI.dim}--- code ---${ANSI.reset}` : `${ANSI.dim}--- end ---${ANSI.reset}`);
|
||||
continue;
|
||||
}
|
||||
if (inCode) {
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\s*#\s+/.test(line)) {
|
||||
const title = line.replace(/^\s*#\s+/, "").trim();
|
||||
out.push(`${ANSI.bold}${title}${ANSI.reset}`);
|
||||
out.push(`${ANSI.dim}${"=".repeat(Math.min(78, title.length || 1))}${ANSI.reset}`);
|
||||
continue;
|
||||
}
|
||||
if (/^\s*##\s+/.test(line)) {
|
||||
const title = line.replace(/^\s*##\s+/, "").trim();
|
||||
out.push(`${ANSI.bold}${title}${ANSI.reset}`);
|
||||
out.push(`${ANSI.dim}${"-".repeat(Math.min(78, title.length || 1))}${ANSI.reset}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mBullet = line.match(/^\s*[-*]\s+(.*)$/);
|
||||
if (mBullet) {
|
||||
line = ` - ${mBullet[1]}`;
|
||||
}
|
||||
|
||||
line = line.replace(/\s+$/g, "");
|
||||
line = renderInlineLinks(line);
|
||||
out.push(line);
|
||||
}
|
||||
|
||||
return out.join("\r\n");
|
||||
}
|
||||
|
||||
class ThunixTerminal {
|
||||
constructor(hostEl, frameEl) {
|
||||
this.el = hostEl;
|
||||
this.frame = frameEl;
|
||||
|
||||
this.term = null;
|
||||
this.fit = new FitAddon();
|
||||
|
||||
this.buffer = "";
|
||||
this.history = [];
|
||||
this.historyIdx = -1;
|
||||
|
||||
this.pages = new Map();
|
||||
this.menu = null;
|
||||
|
||||
this.webLinks = new WebLinksAddon((ev, uri) => {
|
||||
try {
|
||||
ev?.preventDefault?.();
|
||||
} catch {
|
||||
}
|
||||
this.handleLinkActivate(uri);
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.term = new Terminal({
|
||||
cursorBlink: true,
|
||||
convertEol: true,
|
||||
scrollback: 4000,
|
||||
fontFamily:
|
||||
'"Departure Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: 18,
|
||||
theme: {
|
||||
background: "#111111",
|
||||
foreground: "#edb200",
|
||||
cursor: "#ffc828",
|
||||
selection: "rgba(255, 200, 40, 0.25)",
|
||||
black: "#000000",
|
||||
brightYellow: "#ffc828",
|
||||
},
|
||||
});
|
||||
|
||||
this.term.loadAddon(this.fit);
|
||||
this.term.loadAddon(this.webLinks);
|
||||
|
||||
this.term.open(this.el);
|
||||
this.fit.fit();
|
||||
|
||||
window.addEventListener("resize", () => this.fit.fit());
|
||||
|
||||
this.el.addEventListener("mousedown", () => this.term.focus());
|
||||
this.term.focus();
|
||||
|
||||
this.term.onKey((e) => this.onKey(e));
|
||||
|
||||
this.boot();
|
||||
}
|
||||
|
||||
writeln(s = "") {
|
||||
this.term.writeln(s);
|
||||
}
|
||||
|
||||
write(s = "") {
|
||||
this.term.write(s);
|
||||
}
|
||||
|
||||
prompt() {
|
||||
this.write(`\r\n${ANSI.bold}guest@thunix${ANSI.reset}:${ANSI.fgWhite}~${ANSI.reset}$ `);
|
||||
}
|
||||
|
||||
redrawInput(newValue) {
|
||||
this.write(`\r${ANSI.clrLine}${ANSI.bold}guest@thunix${ANSI.reset}:${ANSI.fgWhite}~${ANSI.reset}$ ${newValue}`);
|
||||
}
|
||||
|
||||
async boot() {
|
||||
this.writeln(`${ANSI.bold}thunix terminal${ANSI.reset}`);
|
||||
this.writeln(`${ANSI.dim}Terminal commands + real HTML panel so forms actually work.${ANSI.reset}`);
|
||||
this.writeln("");
|
||||
|
||||
await Promise.allSettled([this.loadPages(), this.loadMenu()]);
|
||||
|
||||
const hash = (location.hash || "").replace(/^#/, "").trim();
|
||||
const initial = hash && this.normalizeSlug(hash);
|
||||
if (initial) {
|
||||
this.openInPanel(initial);
|
||||
} else {
|
||||
this.openInPanel("main");
|
||||
}
|
||||
|
||||
this.writeln(`${ANSI.dim}Type ${ANSI.reset}${ANSI.bold}help${ANSI.reset}${ANSI.dim} for commands.${ANSI.reset}`);
|
||||
this.prompt();
|
||||
}
|
||||
|
||||
async loadPages() {
|
||||
const res = await fetch(urlFromBase("api/pages.php"), { cache: "no-store" });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data?.pages) return;
|
||||
for (const p of data.pages) {
|
||||
this.pages.set(p.slug, p.title || p.slug);
|
||||
}
|
||||
}
|
||||
|
||||
async loadMenu() {
|
||||
const res = await fetch(urlFromBase("api/menu.php"), { cache: "no-store" });
|
||||
if (!res.ok) return;
|
||||
this.menu = await res.json();
|
||||
}
|
||||
|
||||
handleLinkActivate(uri) {
|
||||
const u = (() => {
|
||||
try {
|
||||
return new URL(uri, location.origin);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!u) {
|
||||
window.open(uri, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
|
||||
if (u.pathname.endsWith("/terminal/view.php")) {
|
||||
const p = u.searchParams.get("page") || "";
|
||||
if (p) {
|
||||
this.openInPanel(p);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (u.origin === location.origin && u.pathname.startsWith("/")) {
|
||||
const slug = this.normalizeSlug(u.pathname.replace(/^\/+/, ""));
|
||||
if (slug && this.pages.has(slug)) {
|
||||
this.openInPanel(slug);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
window.open(u.toString(), "_blank", "noopener");
|
||||
}
|
||||
|
||||
onKey({ key, domEvent }) {
|
||||
const ev = domEvent;
|
||||
|
||||
if (ev.ctrlKey && ev.key.toLowerCase() === "l") {
|
||||
ev.preventDefault();
|
||||
this.cmdClear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.ctrlKey && ev.key.toLowerCase() === "c") {
|
||||
ev.preventDefault();
|
||||
this.write("^C");
|
||||
this.buffer = "";
|
||||
this.historyIdx = -1;
|
||||
this.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Enter") {
|
||||
const line = this.buffer.trim();
|
||||
this.buffer = "";
|
||||
this.historyIdx = -1;
|
||||
this.write("\r\n");
|
||||
if (line) this.history.unshift(line);
|
||||
this.run(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Backspace") {
|
||||
if (this.buffer.length > 0) {
|
||||
this.buffer = this.buffer.slice(0, -1);
|
||||
this.write("\b \b");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "ArrowUp") {
|
||||
if (this.history.length === 0) return;
|
||||
if (this.historyIdx + 1 < this.history.length) this.historyIdx++;
|
||||
const next = this.history[this.historyIdx] ?? "";
|
||||
this.buffer = next;
|
||||
this.redrawInput(this.buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "ArrowDown") {
|
||||
if (this.history.length === 0) return;
|
||||
if (this.historyIdx > 0) this.historyIdx--;
|
||||
else this.historyIdx = -1;
|
||||
const next = this.historyIdx >= 0 ? (this.history[this.historyIdx] ?? "") : "";
|
||||
this.buffer = next;
|
||||
this.redrawInput(this.buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.altKey && !ev.ctrlKey && !ev.metaKey && key && key.length === 1) {
|
||||
this.buffer += key;
|
||||
this.write(key);
|
||||
}
|
||||
}
|
||||
|
||||
async run(line) {
|
||||
if (!line) {
|
||||
this.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
const [cmdRaw, ...args] = line.split(/\s+/);
|
||||
const cmd = cmdRaw.toLowerCase();
|
||||
|
||||
switch (cmd) {
|
||||
case "help":
|
||||
this.cmdHelp();
|
||||
break;
|
||||
case "clear":
|
||||
this.cmdClear();
|
||||
break;
|
||||
case "menu":
|
||||
await this.cmdMenu();
|
||||
break;
|
||||
case "pages":
|
||||
case "ls":
|
||||
await this.cmdPages();
|
||||
break;
|
||||
case "open":
|
||||
await this.cmdOpen(args.join(" "));
|
||||
break;
|
||||
case "cat":
|
||||
await this.cmdCat(args.join(" "));
|
||||
break;
|
||||
case "web":
|
||||
this.cmdWeb(args.join(" "));
|
||||
break;
|
||||
case "webmail":
|
||||
case "mail":
|
||||
this.cmdWebmail(args.join(" "));
|
||||
break;
|
||||
case "users":
|
||||
await this.cmdOpen("users");
|
||||
break;
|
||||
case "server":
|
||||
await this.cmdOpen("server");
|
||||
break;
|
||||
case "news":
|
||||
await this.cmdOpen("news");
|
||||
break;
|
||||
case "main":
|
||||
case "home":
|
||||
await this.cmdOpen("main");
|
||||
break;
|
||||
default:
|
||||
this.writeln(`${ANSI.dim}Unknown command:${ANSI.reset} ${cmdRaw}`);
|
||||
this.writeln(`${ANSI.dim}Try:${ANSI.reset} ${ANSI.bold}help${ANSI.reset}`);
|
||||
break;
|
||||
}
|
||||
|
||||
this.prompt();
|
||||
}
|
||||
|
||||
cmdHelp() {
|
||||
this.writeln(`${ANSI.bold}Commands${ANSI.reset}`);
|
||||
this.writeln(`${ANSI.dim}help${ANSI.reset} Show this help`);
|
||||
this.writeln(`${ANSI.dim}pages | ls${ANSI.reset} List available content pages`);
|
||||
this.writeln(`${ANSI.dim}open <page>${ANSI.reset} Load a page in the panel`);
|
||||
this.writeln(`${ANSI.dim}web <page>${ANSI.reset} Print web URLs for a page`);
|
||||
this.writeln(`${ANSI.dim}webmail [url]${ANSI.reset} Open webmail in a new tab (alias: mail)`);
|
||||
this.writeln(`${ANSI.dim}clear${ANSI.reset} Clear the terminal (Ctrl+L)`);
|
||||
this.writeln("");
|
||||
this.writeln(`${ANSI.dim}Aliases:${ANSI.reset} home, main, users, server, news, mail`);
|
||||
}
|
||||
|
||||
cmdClear() {
|
||||
this.term.clear();
|
||||
this.term.reset();
|
||||
this.writeln(`${ANSI.bold}thunix terminal${ANSI.reset}`);
|
||||
}
|
||||
|
||||
async cmdMenu() {
|
||||
if (!this.menu) await this.loadMenu();
|
||||
const menu = this.menu;
|
||||
|
||||
if (!menu?.sections?.length) {
|
||||
this.writeln(`${ANSI.dim}Menu not available.${ANSI.reset}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const section of menu.sections) {
|
||||
this.writeln("");
|
||||
this.writeln(`${ANSI.bold}${section.title}${ANSI.reset}`);
|
||||
this.writeln(`${ANSI.dim}${"-".repeat(Math.min(78, section.title.length || 1))}${ANSI.reset}`);
|
||||
for (const item of section.items) {
|
||||
const href = item.internal ? `${location.origin}/${item.slug}` : absolutizeUrl(item.href);
|
||||
const label = `${ANSI.underline}${ANSI.fgBrightYellow}${item.text}${ANSI.reset}`;
|
||||
const link = osc8(href, label);
|
||||
const hint = item.internal ? `${ANSI.dim} (open ${item.slug})${ANSI.reset}` : "";
|
||||
this.writeln(` • ${link}${hint}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cmdPages() {
|
||||
if (this.pages.size === 0) await this.loadPages();
|
||||
if (this.pages.size === 0) {
|
||||
this.writeln(`${ANSI.dim}No pages found.${ANSI.reset}`);
|
||||
return;
|
||||
}
|
||||
this.writeln(`${ANSI.bold}Pages${ANSI.reset}`);
|
||||
const slugs = [...this.pages.keys()].sort();
|
||||
this.writeln(slugs.map((s) => ` - ${s}`).join("\r\n"));
|
||||
}
|
||||
|
||||
cmdWeb(arg) {
|
||||
const slug = this.normalizeSlug(arg);
|
||||
if (!slug) {
|
||||
this.writeln(`${ANSI.dim}Usage:${ANSI.reset} web <page>`);
|
||||
return;
|
||||
}
|
||||
const classic = `${location.origin}/${slug}`;
|
||||
const panelUrl = new URL("view.php", MODULE_BASE);
|
||||
panelUrl.searchParams.set("page", slug);
|
||||
const panel = panelUrl.toString();
|
||||
this.writeln(`${ANSI.dim}Classic:${ANSI.reset} ${osc8(classic, `${ANSI.underline}${ANSI.fgBrightYellow}${classic}${ANSI.reset}`)}`);
|
||||
this.writeln(`${ANSI.dim}Panel:${ANSI.reset} ${osc8(panel, `${ANSI.underline}${ANSI.fgBrightYellow}${panel}${ANSI.reset}`)}`);
|
||||
}
|
||||
|
||||
cmdWebmail(arg) {
|
||||
const raw = String(arg || "").trim();
|
||||
|
||||
const configured = (() => {
|
||||
try {
|
||||
const v = window.THUNIX_WEBMAIL_URL;
|
||||
return typeof v === "string" ? v.trim() : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
|
||||
const target = raw || configured || DEFAULT_WEBMAIL_PATH;
|
||||
const url = absolutizeUrl(target);
|
||||
|
||||
window.open(url, "_blank", "noopener");
|
||||
|
||||
const label = `${ANSI.underline}${ANSI.fgBrightYellow}${url}${ANSI.reset}`;
|
||||
this.writeln(`${ANSI.dim}Opened webmail:${ANSI.reset} ${osc8(url, label)}`);
|
||||
|
||||
if (!raw && !configured && DEFAULT_WEBMAIL_PATH !== "/webmail/") {
|
||||
this.writeln(`${ANSI.dim}Hint:${ANSI.reset} set window.THUNIX_WEBMAIL_URL if your webmail lives elsewhere.`);
|
||||
} else if (!raw && !configured) {
|
||||
this.writeln(
|
||||
`${ANSI.dim}Hint:${ANSI.reset} if your webmail isn't at ${ANSI.bold}${DEFAULT_WEBMAIL_PATH}${ANSI.reset}${ANSI.dim}, set window.THUNIX_WEBMAIL_URL (or run: webmail <url>).${ANSI.reset}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
normalizeSlug(arg) {
|
||||
if (!arg) return "";
|
||||
let s = String(arg).trim();
|
||||
if (s.startsWith("/")) s = s.replace(/^\/+/, "");
|
||||
if (s.includes("?")) s = s.split("?")[0];
|
||||
if (s === "") return "";
|
||||
return s;
|
||||
}
|
||||
|
||||
openInPanel(slug) {
|
||||
if (!this.frame) return;
|
||||
const clean = this.normalizeSlug(slug);
|
||||
this.frame.src = `${urlFromBase("view.php")}?page=${encodeURIComponent(clean)}`;
|
||||
|
||||
try {
|
||||
history.replaceState(null, "", `#${encodeURIComponent(clean)}`);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
async cmdOpen(arg) {
|
||||
const slug = this.normalizeSlug(arg);
|
||||
if (!slug) {
|
||||
this.writeln(`${ANSI.dim}Usage:${ANSI.reset} open <page>`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(slug) || slug.startsWith("//")) {
|
||||
const url = absolutizeUrl(slug);
|
||||
window.open(url, "_blank", "noopener");
|
||||
this.writeln(`${ANSI.dim}Opened externally:${ANSI.reset} ${osc8(url, `${ANSI.underline}${ANSI.fgBrightYellow}${url}${ANSI.reset}`)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.pages.size === 0) await this.loadPages();
|
||||
if (this.pages.size && !this.pages.has(slug) && !/^success\d+$/.test(slug)) {
|
||||
this.writeln(`${ANSI.dim}No such page:${ANSI.reset} ${slug}`);
|
||||
this.writeln(`${ANSI.dim}Try:${ANSI.reset} pages`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.openInPanel(slug);
|
||||
this.writeln(`${ANSI.dim}Loaded in panel:${ANSI.reset} ${ANSI.bold}${slug}${ANSI.reset}`);
|
||||
}
|
||||
|
||||
async cmdCat(arg) {
|
||||
const slug = this.normalizeSlug(arg);
|
||||
if (!slug) {
|
||||
this.writeln(`${ANSI.dim}Usage:${ANSI.reset} cat <page>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`${urlFromBase("api/page.php")}?p=${encodeURIComponent(slug)}`, { cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
this.writeln(`${ANSI.dim}No such page:${ANSI.reset} ${slug}`);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const md = String(data?.markdown ?? "");
|
||||
this.writeln("");
|
||||
this.writeln(renderMarkdown(md));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const host = document.getElementById("terminal");
|
||||
const frame = document.getElementById("contentFrame");
|
||||
if (!host) return;
|
||||
new ThunixTerminal(host, frame);
|
||||
});
|
||||
234
terminal/view.php
Normal file
234
terminal/view.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Render wiki content using the *same* parser stack as wiki.php,
|
||||
|
||||
require __DIR__ . '/../config.php';
|
||||
require __DIR__ . '/../parsedown-1.7.3/Parsedown.php';
|
||||
require __DIR__ . '/../parsedown-extra-0.7.1/ParsedownExtra.php';
|
||||
|
||||
$page = isset($_GET['page']) ? (string) $_GET['page'] : 'main';
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/', $page)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "Bad page name.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$contentPath = $doc_root . '/articles/' . $page . '.md';
|
||||
if (!is_file($contentPath)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "Not found.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$ParsedownExtra = new ParsedownExtra();
|
||||
|
||||
$md = file_get_contents($contentPath);
|
||||
if ($md === false) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "Failed to read page.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$html = $ParsedownExtra->text($md);
|
||||
|
||||
if ($page === 'users' || $page === 'server') {
|
||||
$inc = $doc_root . '/includes/' . $page . '.php';
|
||||
if (is_file($inc)) {
|
||||
ob_start();
|
||||
require $inc;
|
||||
$html .= (string) ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
$knownPages = [];
|
||||
foreach (glob($doc_root . '/articles/*.md') ?: [] as $file) {
|
||||
$slug = basename($file, '.md');
|
||||
$knownPages[$slug] = true;
|
||||
}
|
||||
|
||||
$rewriteFormActions = function (string $action): string {
|
||||
$path = parse_url($action, PHP_URL_PATH);
|
||||
if (!is_string($path) || $path === '') {
|
||||
$path = $action;
|
||||
}
|
||||
|
||||
$pathLower = strtolower($path);
|
||||
if (!preg_match('~(^|/)(includes/(contact|signup)\.php)$~', $pathLower)) {
|
||||
return $action;
|
||||
}
|
||||
|
||||
$parts = parse_url($action);
|
||||
if ($parts === false) {
|
||||
if (strpos($action, 'terminal=1') !== false) {
|
||||
return $action;
|
||||
}
|
||||
return (strpos($action, '?') !== false) ? ($action . '&terminal=1') : ($action . '?terminal=1');
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if (isset($parts['query'])) {
|
||||
parse_str((string) $parts['query'], $query);
|
||||
}
|
||||
$query['terminal'] = '1';
|
||||
$queryString = http_build_query($query);
|
||||
|
||||
$rebuilt = '';
|
||||
if (isset($parts['scheme'])) {
|
||||
$rebuilt .= $parts['scheme'] . '://';
|
||||
} elseif (str_starts_with($action, '//')) {
|
||||
$rebuilt .= '//';
|
||||
}
|
||||
|
||||
if (isset($parts['user'])) {
|
||||
$rebuilt .= $parts['user'];
|
||||
if (isset($parts['pass'])) {
|
||||
$rebuilt .= ':' . $parts['pass'];
|
||||
}
|
||||
$rebuilt .= '@';
|
||||
}
|
||||
|
||||
if (isset($parts['host'])) {
|
||||
$rebuilt .= $parts['host'];
|
||||
}
|
||||
if (isset($parts['port'])) {
|
||||
$rebuilt .= ':' . $parts['port'];
|
||||
}
|
||||
|
||||
$rebuilt .= $parts['path'] ?? '';
|
||||
if ($queryString !== '') {
|
||||
$rebuilt .= '?' . $queryString;
|
||||
}
|
||||
if (isset($parts['fragment'])) {
|
||||
$rebuilt .= '#' . $parts['fragment'];
|
||||
}
|
||||
|
||||
return $rebuilt;
|
||||
};
|
||||
|
||||
|
||||
$isTerminalSuccessFormTarget = function (string $action): bool {
|
||||
$path = parse_url($action, PHP_URL_PATH);
|
||||
if (!is_string($path) || $path === '') {
|
||||
$path = $action;
|
||||
}
|
||||
|
||||
$pathLower = strtolower($path);
|
||||
return preg_match('~(^|/)(includes/(contact|signup)\.php)$~', $pathLower) === 1;
|
||||
};
|
||||
|
||||
|
||||
$html = preg_replace_callback(
|
||||
'~(<form\b[^>]*\baction\s*=\s*)(["\'])([^"\']+)(\2)~i',
|
||||
function (array $m) use ($rewriteFormActions): string {
|
||||
$new = $rewriteFormActions((string) $m[3]);
|
||||
return $m[1] . $m[2] . $new . $m[4];
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
$html = preg_replace_callback(
|
||||
'~(<form\b[^>]*>)~i',
|
||||
function (array $m) use ($rewriteFormActions, $isTerminalSuccessFormTarget): string {
|
||||
$tag = $m[1];
|
||||
|
||||
if (preg_match('~\baction\s*=\s*(["\'])([^"\']+)\1~i', $tag, $am) !== 1) {
|
||||
return $tag;
|
||||
}
|
||||
|
||||
$action = (string) $am[2];
|
||||
if ($isTerminalSuccessFormTarget($action) === false) {
|
||||
return $tag;
|
||||
}
|
||||
|
||||
$newAction = $rewriteFormActions($action);
|
||||
$tag = preg_replace(
|
||||
'~\baction\s*=\s*(["\'])([^"\']+)\1~i',
|
||||
'action=' . $am[1] . $newAction . $am[1],
|
||||
$tag,
|
||||
1
|
||||
);
|
||||
|
||||
return $tag . "\n" . '<input type="hidden" name="terminal" value="1">';
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
$dom->loadHTML(
|
||||
'<!doctype html><html><head><meta charset="utf-8"></head><body><div id="content">' . $html . '</div></body></html>',
|
||||
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
|
||||
);
|
||||
|
||||
foreach ($dom->getElementsByTagName('a') as $a) {
|
||||
$href = $a->getAttribute('href');
|
||||
if ($href === '' || str_starts_with($href, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*:/', $href) === 1) {
|
||||
$a->setAttribute('target', '_blank');
|
||||
$a->setAttribute('rel', 'noopener');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($href, '/')) {
|
||||
$path = parse_url($href, PHP_URL_PATH) ?? '';
|
||||
$slug = ltrim($path, '/');
|
||||
|
||||
if ($slug !== '' && isset($knownPages[$slug])) {
|
||||
$a->setAttribute('href', './view.php?page=' . rawurlencode($slug));
|
||||
continue;
|
||||
}
|
||||
|
||||
$a->setAttribute('target', '_blank');
|
||||
$a->setAttribute('rel', 'noopener');
|
||||
continue;
|
||||
}
|
||||
|
||||
$a->setAttribute('target', '_blank');
|
||||
$a->setAttribute('rel', 'noopener');
|
||||
}
|
||||
|
||||
foreach ($dom->getElementsByTagName('form') as $form) {
|
||||
$action = $form->getAttribute('action');
|
||||
if ($action !== '') {
|
||||
$form->setAttribute('action', $rewriteFormActions($action));
|
||||
}
|
||||
}
|
||||
|
||||
$finalHtml = $dom->saveHTML();
|
||||
libxml_clear_errors();
|
||||
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
|
||||
?><!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?php echo htmlspecialchars($site_name . ' - ' . $page, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
<link rel="stylesheet" type="text/css" href="<?php echo htmlspecialchars($site_root . '/includes/terminal.css', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
$dom2 = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom2->loadHTML($finalHtml);
|
||||
libxml_clear_errors();
|
||||
$content = $dom2->getElementById('content');
|
||||
if ($content === null) {
|
||||
echo $finalHtml;
|
||||
} else {
|
||||
echo $dom2->saveHTML($content);
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user