#!/usr/bin/env node import fs from "node:fs"; import vm from "node:vm"; class ClassList { constructor() { this.values = new Set(); } toggle(name, active) { active ? this.values.add(name) : this.values.delete(name); } } class Element { constructor(id = "") { this.id = id; this.value = ""; this.checked = false; this.disabled = false; this.hidden = false; this.open = false; this.dataset = {}; this.children = []; this.listeners = new Map(); this.classList = new ClassList(); this.attributes = new Map(); this.tabIndex = 0; this.textContent = ""; this.type = ""; this.readOnly = false; this.elements = []; } addEventListener(name, callback) { this.listeners.set(name, callback); } dispatch(name) { return this.listeners.get(name)?.({ target: this, preventDefault() {} }); } setAttribute(name, value) { this.attributes.set(name, String(value)); } append(...items) { this.children.push(...items); } replaceChildren(...items) { this.children = [...items]; } showModal() { this.open = true; } close() { this.open = false; } reset() {} } const ids = [ "htaccess-help-close","htaccess-help-dialog","php-help-close","php-help-dialog","web-service-cache-control","web-service-cache-rules", "web-service-close","web-service-default-mime","web-service-dialog","web-service-download-extensions", "web-service-editing","web-service-form","web-service-hardening-attacks","web-service-hardening-headers", "web-service-htaccess-enabled","web-service-htaccess-filename","web-service-htaccess-help","web-service-htaccess-recursive", "web-service-htaccess-strict","web-service-id","web-service-index","web-service-list","web-service-listing", "web-service-mime-types","web-service-name","web-service-new","web-service-php-concurrency", "web-service-php-connect-timeout","web-service-php-enabled","web-service-php-endpoint","web-service-php-extensions", "web-service-php-request-timeout","web-service-php-pass-authorization","web-service-php-body-limit","web-service-php-header-limit", "web-service-php-stderr-limit","web-service-php-custom-params","web-service-php-values","web-service-php-help", "web-service-precompressed","web-service-precompressed-br","web-service-precompressed-gzip", "web-service-preset","web-service-preset-apply","web-service-preset-summary","web-service-reset", "web-service-response-headers","web-service-rewrites","web-service-root","web-service-search", "web-service-symlink-policy","web-service-tabs","web-service-try-files" ]; const elements = new Map(ids.map(id => [id, new Element(id)])); const pages = ["general","routing","htaccess","mime","cache","delivery","php","security"].map(name => { const page = new Element(); page.dataset.webServicePage = name; return page; }); elements.get("web-service-php-endpoint").value = "unix:/run/php/php-fpm.sock"; elements.get("web-service-php-connect-timeout").value = "1000"; elements.get("web-service-php-request-timeout").value = "30000"; elements.get("web-service-php-concurrency").value = "64"; elements.get("web-service-php-pass-authorization").checked = true; elements.get("web-service-php-body-limit").value = "0"; elements.get("web-service-php-header-limit").value = "65536"; elements.get("web-service-php-stderr-limit").value = "4096"; const document = { querySelectorAll(selector) { if (selector === "#web-service-tabs button") return elements.get("web-service-tabs").children; if (selector === "[data-web-service-page]") return pages; return []; } }; let savedSection = null; const context = { console, structuredClone, document, window: {}, envelope: null, busy: false, $: id => { const value = elements.get(id); if (!value) throw new Error(`missing fake element ${id}`); return value; }, t: key => key, el: (_tag, text = "", className = "") => { const node = new Element(); node.textContent = text ?? ""; if (className) node.classList.values.add(className); return node; }, toast: () => {}, saveSection: async (name, value) => { savedSection = { name, value: structuredClone(value) }; return true; }, appConfirm: async () => true, }; vm.createContext(context); vm.runInContext(fs.readFileSync("src/admin/web/web-server.js", "utf8"), context, { filename: "web-server.js" }); const preset = elements.get("web-service-preset"); preset.value = "wordpress"; preset.dispatch("change"); const expectedIndex = "index.php\nindex.html\nindex.htm"; const expectedTry = "$uri\n$uri/\n/index.php?$query_string"; if (elements.get("web-service-index").value !== expectedIndex) throw new Error("WordPress preset did not populate index files"); if (elements.get("web-service-try-files").value !== expectedTry) throw new Error("WordPress preset did not populate try_files"); if (!elements.get("web-service-php-enabled").checked) throw new Error("WordPress preset did not enable PHP-FPM"); if (!elements.get("web-service-precompressed").checked) throw new Error("WordPress preset did not enable precompressed sidecars"); if (!elements.get("web-service-cache-rules").value.includes("ext:css = public, max-age=604800")) throw new Error("WordPress preset did not populate cache rules"); preset.value = "downloads"; preset.dispatch("change"); if (elements.get("web-service-index").value !== "") throw new Error("Downloads preset should clear index files"); if (!elements.get("web-service-listing").checked) throw new Error("Downloads preset should enable directory listing"); if (elements.get("web-service-php-enabled").checked) throw new Error("Downloads preset should disable PHP-FPM"); if (elements.get("web-service-cache-control").value !== "public, max-age=3600") throw new Error("Downloads preset cache policy missing"); // The save path must serialize the values materialized by preset selection, not // the previous/custom values that were present when the dialog opened. context.envelope = { config: { web_services: [] } }; elements.get("web-service-id").value = "web1"; elements.get("web-service-name").value = "web1"; elements.get("web-service-root").value = "/srv/web1"; elements.get("web-service-php-pass-authorization").checked = false; elements.get("web-service-php-body-limit").value = "1048576"; elements.get("web-service-php-header-limit").value = "131072"; elements.get("web-service-php-stderr-limit").value = "8192"; elements.get("web-service-php-custom-params").value = "APP_ENV = production"; elements.get("web-service-php-values").value = "memory_limit = 256M"; preset.value = "wordpress"; preset.dispatch("change"); await elements.get("web-service-form").dispatch("submit"); if (savedSection?.name !== "web-services" || savedSection.value.length !== 1) throw new Error("WordPress preset save did not persist a Web Service"); const saved = savedSection.value[0]; if (saved.index.join("\n") !== expectedIndex) throw new Error("Saved WordPress preset lost index files"); if (saved.try_files.join("\n") !== expectedTry) throw new Error("Saved WordPress preset lost try_files"); if (!saved.php.enabled) throw new Error("Saved WordPress preset lost PHP-FPM enablement"); if (!saved.precompressed.enabled) throw new Error("Saved WordPress preset lost precompressed enablement"); if (saved.php.pass_authorization !== false) throw new Error("Saved WordPress preset lost Authorization forwarding choice"); if (saved.php.max_request_body_bytes !== 1048576 || saved.php.max_response_header_bytes !== 131072 || saved.php.stderr_log_bytes !== 8192) throw new Error("Saved WordPress preset lost advanced FastCGI limits"); if (saved.php.custom_params.APP_ENV !== "production") throw new Error("Saved WordPress preset lost custom FastCGI parameters"); if (saved.php.php_values.memory_limit !== "256M") throw new Error("Saved WordPress preset lost PHP_VALUE settings"); if (!saved.cache_rules.some(rule => rule.matcher === "ext:css" && rule.cache_control.includes("604800"))) throw new Error("Saved WordPress preset lost cache rules"); console.log("PASS: Web Service preset selection materializes and persists CMS/download settings.");