公開済みのソース
ソースの ZIP をダウンロード
CH-J Tools
特定のアプリケーションリリースのディレクトリとファイルを閲覧できます。
CH-J / Server Manager
1704 ファイル
0.1.2-beta
追加日時: · ビルド日時:
1
"use strict";3
// Everything that changes the server. Each change is shown first, needs a confirmation, keeps a backup and passes the4
// configuration test, or the previous state comes back. A reload is always a step of its own.5
const writeState = { plan: null, pendingEdit: null, file: null, lastChange: [], seenDiffs: new Set(), missingModules: [] };6
const WIZARD_FIELDS = {7
static: ["wizardRootField", "wizardCreateRootField"],8
php: ["wizardRootField", "wizardCreateRootField", "wizardPhpField"],9
wordpress: ["wizardRootField", "wizardCreateRootField", "wizardPhpField"],10
proxy: ["wizardBackendField"],11
websocket: ["wizardBackendField"],12
redirect: ["wizardRedirectTargetField", "wizardRedirectCodeField"],13
custom: ["wizardTemplateField", "wizardRootField", "wizardCreateRootField", "wizardBackendField", "wizardPhpField"]14
};15
const TEXT_FIXES = new Set(["open-proxy", "directory-listing", "no-listen"]);17
async function writeOperation(startMessage, work) {18
const requestGeneration = generation;19
setBusy(true); setStatus(startMessage);20
try { return await work(requestGeneration); }21
catch (error) {22
if (requestGeneration !== generation || error?.message === "CHJ_SESSION_CHANGED") return undefined;23
elements.commandOutput.textContent = errorText(error); elements.commandOutput.classList.add("error");24
setStatus(errorText(error), true);25
return undefined;26
} finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false); } }27
}29
function confirmOn(question) { return window.confirm(`${api.sessions.displayName(selectedSession())}\n\n${question}`); }30
function showOutput(text) { elements.commandOutput.textContent = text || "—"; elements.commandOutput.classList.remove("error"); }31
function readStored(key, fallback) { try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; } }32
function writeStored(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; } }34
// --- diff -----------------------------------------------------------------------------------------36
function lineDiff(before, after) {37
const a = before.split("\n"), b = after.split("\n");38
let start = 0;39
while (start < a.length && start < b.length && a[start] === b[start]) start += 1;40
let endA = a.length, endB = b.length;41
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA -= 1; endB -= 1; }42
const lines = [];43
for (let at = Math.max(0, start - 3); at < start; at += 1) lines.push(["", a[at]]);44
const middleA = a.slice(start, endA), middleB = b.slice(start, endB);45
if (middleA.length * middleB.length > 4_000_000) {46
middleA.forEach((line) => lines.push(["del", line]));47
middleB.forEach((line) => lines.push(["add", line]));48
} else {49
const table = Array.from({ length: middleA.length + 1 }, () => new Uint32Array(middleB.length + 1));50
for (let i = middleA.length - 1; i >= 0; i -= 1) for (let j = middleB.length - 1; j >= 0; j -= 1) table[i][j] = middleA[i] === middleB[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);51
let i = 0, j = 0;52
while (i < middleA.length || j < middleB.length) {53
if (i < middleA.length && j < middleB.length && middleA[i] === middleB[j]) { lines.push(["", middleA[i]]); i += 1; j += 1; }54
else if (i < middleA.length && (j === middleB.length || table[i + 1][j] >= table[i][j + 1])) { lines.push(["del", middleA[i]]); i += 1; }55
else { lines.push(["add", middleB[j]]); j += 1; }56
}57
}58
for (let at = endA; at < Math.min(a.length, endA + 3); at += 1) lines.push(["", a[at]]);59
return lines;60
}62
function renderDiff(target, before, after) {63
const lines = lineDiff(before, after);64
if (!lines.some(([kind]) => kind)) { target.textContent = t("Bez rozdílů."); return; }65
target.replaceChildren(...lines.map(([kind, line]) => node("span", kind, `${kind === "add" ? "+" : kind === "del" ? "-" : " "} ${line}`)));66
}68
function rememberChange(items) { writeState.lastChange = items; byId("undoButton").hidden = !items.length; }70
async function refreshAfterChange(requestGeneration, message) {71
await sessionResult(loadInventory(true), requestGeneration);72
setStatus(message);73
}75
// --- wizard ---------------------------------------------------------------------------------------77
function syncWizard() {78
const type = byId("wizardType").value;79
for (const id of new Set(Object.values(WIZARD_FIELDS).flat())) byId(id).hidden = !WIZARD_FIELDS[type].includes(id);80
byId("wizardTlsField").hidden = type === "custom";81
byId("wizardHeadersField").hidden = type === "custom";82
const tls = type === "custom" ? "none" : byId("wizardTls").value;83
for (const id of ["wizardCertificateField", "wizardKeyField", "wizardRedirectHttpField"]) byId(id).hidden = tls !== "existing";84
const root = byId("wizardRoot");85
const domain = byId("wizardDomain").value.trim().toLowerCase().replace(/^\*\./, "");86
if (!root.dataset.edited) root.value = domain ? `/var/www/${domain}` : "";87
const sockets = inventory?.phpSockets || [];88
byId("wizardPhpList").replaceChildren(...sockets.map((socket) => { const option = node("option"); option.value = socket; return option; }));89
if (sockets.length && !byId("wizardPhp").value) byId("wizardPhp").value = sockets[0];90
invalidatePlan();91
}93
function invalidatePlan() {94
writeState.plan = null; writeState.missingModules = [];95
byId("wizardDeploy").disabled = true;96
byId("wizardResult").hidden = true;97
}99
function wizardRequest() {100
const custom = byId("wizardType").value === "custom";101
const https = !custom && byId("wizardTls").value === "existing";102
return {103
type: byId("wizardType").value, serverNames: [byId("wizardDomain").value.trim(), ...byId("wizardAliases").value.split(/[\s,]+/)].filter(Boolean), port: Number(byId("wizardPort").value) || 80,104
root: byId("wizardRoot").value.trim(), createRoot: byId("wizardCreateRoot").checked, phpBackend: byId("wizardPhp").value.trim(), backend: byId("wizardBackend").value.trim(),105
redirectTarget: byId("wizardRedirectTarget").value.trim(), redirectCode: Number(byId("wizardRedirectCode").value),106
https, certificate: byId("wizardCertificate").value.trim(), certificateKey: byId("wizardKey").value.trim(), redirectHttp: https && byId("wizardRedirectHttp").checked,107
securityHeaders: byId("wizardHeaders").checked, template: custom ? byId("wizardTemplateText").value : ""108
};109
}111
async function previewSite() {112
await writeOperation(t("Sestavuji konfiguraci…"), async (requestGeneration) => {113
const site = wizardRequest();114
const plan = await sessionResult(api.apache.planSite(elements.sessionSelect.value, { site, ...auth() }), requestGeneration);115
writeState.plan = site; writeState.missingModules = plan.missingModules || [];116
byId("wizardPath").textContent = `${plan.path} (${plan.link ? t("vznikne vypnutý, zapnutí vytvoří {link}", { link: plan.link }) : t("vznikne vypnutý")})`;117
byId("wizardText").textContent = plan.text;118
const notes = plan.warnings.map((item) => t("Na portu {port} Apache nenaslouchá; web bude po zapnutí nedostupný, dokud nepřidáte Listen {port}.", { port: item.port }));119
if (writeState.missingModules.length) notes.push(t("Web potřebuje moduly, které nejsou načtené: {modules}.", { modules: writeState.missingModules.join(", ") }));120
byId("wizardWarnings").textContent = notes.join(" ");121
byId("wizardWarnings").hidden = !notes.length;122
byId("wizardEnableModules").hidden = !(writeState.missingModules.length && inventory?.canManageModules);123
byId("wizardResult").hidden = false;124
byId("wizardDeploy").disabled = writeState.missingModules.length > 0;125
byId("wizardDeploy").scrollIntoView({ block: "start" });126
setStatus(t("Náhled je připravený, na serveru se zatím nic nezměnilo."));127
});128
}130
async function enableMissingModules() {131
const modules = [...writeState.missingModules];132
if (!modules.length || !confirmOn(t("Zapnout moduly {modules} a spustit test konfigurace? Projeví se po plynulém načtení.", { modules: modules.join(", ") }))) return;133
await writeOperation(t("Zapínám moduly…"), async (requestGeneration) => {134
for (const name of modules) await sessionResult(api.apache.setModule(elements.sessionSelect.value, { name, enabled: true, confirm: true, ...auth() }), requestGeneration);135
await sessionResult(api.apache.reload(elements.sessionSelect.value, { confirm: true, ...auth() }), requestGeneration);136
await refreshAfterChange(requestGeneration, t("Moduly jsou zapnuté a načtené. Můžete znovu zobrazit generovanou konfiguraci."));137
invalidatePlan();138
});139
}141
async function deploySite() {142
const site = writeState.plan;143
if (!site || !confirmOn(t("Vytvořit vypnutý web {name} a spustit test konfigurace? Při neúspěšném testu se nový soubor zase odstraní. Existující konfigurace se nepřepisuje.", { name: site.serverNames[0] }))) return;144
await writeOperation(t("Vytvářím web a ověřuji konfiguraci…"), async (requestGeneration) => {145
const result = await sessionResult(api.apache.createSite(elements.sessionSelect.value, { site, confirm: true, ...auth() }), requestGeneration);146
showOutput(`${result.testOutput}\n\n${t("Soubor: {path}", { path: result.path })}`);147
byId("wizard").hidden = true;148
invalidatePlan();149
await sessionResult(loadInventory(true), requestGeneration);150
const created = inventory?.sites.find((item) => item.domain === site.serverNames[0].toLowerCase() && item.role !== "companion");151
if (created) openDetail(created);152
setStatus(t("Web byl vytvořen vypnutý a test konfigurace uspěl. Zapnete ho tlačítkem Aktivovat, projeví se po plynulém načtení konfigurace."));153
});154
}156
const TEMPLATES_KEY = "chj.apache.templates";157
function storedTemplates() { const value = readStored(TEMPLATES_KEY, {}); return value && typeof value === "object" ? value : {}; }158
function renderTemplates() {159
const select = byId("wizardTemplateSelect");160
const previous = select.value;161
const names = Object.keys(storedTemplates()).sort();162
const none = node("option", "", t("Nová předloha")); none.value = "";163
select.replaceChildren(none, ...names.map((name) => { const option = node("option", "", name); option.value = name; return option; }));164
if (names.includes(previous)) select.value = previous;165
}166
function loadTemplate() {167
const name = byId("wizardTemplateSelect").value;168
byId("wizardTemplateName").value = name;169
byId("wizardTemplateText").value = storedTemplates()[name] || "";170
invalidatePlan();171
}172
function saveTemplate() {173
const name = byId("wizardTemplateName").value.trim();174
const text = byId("wizardTemplateText").value;175
if (!name || !text.trim()) { setStatus(t("Zadejte název a text předlohy."), true); return; }176
if (!writeStored(TEMPLATES_KEY, { ...storedTemplates(), [name]: text })) { setStatus(t("Předlohu se nepodařilo uložit."), true); return; }177
renderTemplates(); byId("wizardTemplateSelect").value = name;178
setStatus(t("Předloha {name} byla uložena.", { name }));179
}180
function deleteTemplate() {181
const name = byId("wizardTemplateSelect").value;182
if (!name || !window.confirm(t("Smazat předlohu {name}?", { name }))) return;183
const remaining = storedTemplates(); delete remaining[name];184
writeStored(TEMPLATES_KEY, remaining);185
byId("wizardTemplateSelect").value = ""; renderTemplates(); loadTemplate();186
}188
// --- site detail ----------------------------------------------------------------------------------190
function fillDetailForm(site) {191
byId("detailName").value = site.domain || "";192
byId("detailAliases").value = (site.aliases || []).join(" ");193
byId("detailRoot").value = site.rootInherited ? "" : site.documentRoot || "";194
byId("detailBackend").value = (site.proxies || []).find((rule) => rule.path === "/" && !rule.excluded)?.target || "";195
byId("detailRedirect").value = site.redirectTarget || "";196
byId("detailCertificate").value = ""; byId("detailKey").value = "";197
byId("diffPanel").hidden = true;198
}200
function renderWrite() {201
const site = selected;202
byId("detailEdit").hidden = !site;203
const toggle = byId("detailToggle");204
if (site) {205
toggle.textContent = site.enabled ? t("Deaktivovat") : t("Aktivovat");206
toggle.hidden = !site.canToggle;207
const note = byId("detailEditNote");208
note.textContent = site.editable ? "" : site.fromMacro ? t("Web vzniká z makra, upravte ho v konfiguračním editoru.") : t("Web je uvnitř podmínky nebo mimo upravitelné soubory, upravte ho v konfiguračním editoru.");209
note.hidden = site.editable;210
for (const id of ["detailName", "detailAliases", "detailRoot", "detailBackend", "detailRedirect", "detailCertificate", "detailKey"]) byId(id).disabled = !site.editable;211
byId("detailPlan").disabled = !site.editable;212
byId("detailBackendField").hidden = !(site.proxies || []).some((rule) => rule.path === "/" && !rule.excluded);213
byId("detailRedirectField").hidden = site.type !== "redirect" || !/^https?:\/\/[^%$]+$/.test(site.redirectTarget || "");214
byId("detailRootField").hidden = site.type === "redirect" || site.type === "proxy";215
const hasTls = (inventory?.sites || []).some((other) => other.sslEnabled && other.enabled === site.enabled && other.domain === site.domain);216
byId("detailHttps").hidden = !site.editable || site.sslEnabled || hasTls || !site.domain;217
}218
renderProposals(); renderAcmeSites();219
}221
function detailChanges() {222
const site = selected, changes = {};223
const value = (id) => byId(id).value.trim();224
if (value("detailName") !== (site.domain || "")) changes.serverName = value("detailName");225
if (value("detailAliases") !== (site.aliases || []).join(" ")) changes.aliases = value("detailAliases");226
if (!byId("detailRootField").hidden && value("detailRoot") && value("detailRoot") !== (site.rootInherited ? "" : site.documentRoot || "")) changes.documentRoot = value("detailRoot");227
const backend = (site.proxies || []).find((rule) => rule.path === "/" && !rule.excluded)?.target || "";228
if (!byId("detailBackendField").hidden && value("detailBackend") !== backend) changes.backend = value("detailBackend");229
if (!byId("detailRedirectField").hidden && value("detailRedirect") !== (site.redirectTarget || "")) changes.redirect = value("detailRedirect");230
if (!byId("detailHttps").hidden && value("detailCertificate")) changes.https = { certificate: value("detailCertificate"), certificateKey: value("detailKey"), redirectHttp: byId("detailRedirectHttp").checked };231
return changes;232
}234
function showPlan(plan) {235
writeState.pendingEdit = plan;236
byId("diffTitle").textContent = plan.path;237
renderDiff(byId("diffOutput"), plan.before, plan.after);238
byId("diffPanel").hidden = false;239
switchTab("sites");240
byId("diffPanel").scrollIntoView({ block: "nearest" });241
setStatus(t("Náhled je připravený, na serveru se zatím nic nezměnilo."));242
}244
async function planDetail() {245
const site = selected;246
const changes = detailChanges();247
if (!Object.keys(changes).length) { setStatus(t("Nebyla provedena žádná změna.")); return; }248
await writeOperation(t("Připravuji náhled změn…"), async (requestGeneration) => {249
showPlan(await sessionResult(api.apache.planEdit(elements.sessionSelect.value, { path: site.editPath, line: site.line, changes, ...auth() }), requestGeneration));250
});251
}253
async function applyEdit() {254
const plan = writeState.pendingEdit;255
if (!plan || !confirmOn(t("Uložit {path}, vytvořit zálohu a spustit test konfigurace? Neplatná konfigurace se automaticky vrátí.", { path: plan.path }))) return;256
await writeOperation(t("Ukládám zálohu a ověřuji novou konfiguraci…"), async (requestGeneration) => {257
const result = await sessionResult(api.apache.saveFile(elements.sessionSelect.value, { path: plan.path, text: plan.after, expectedSha256: plan.sha256, confirm: true, ...auth() }), requestGeneration);258
writeState.pendingEdit = null; byId("diffPanel").hidden = true;259
rememberChange([{ path: plan.path, backup: result.backupPath, sha256: result.sha256 }]);260
showOutput(`${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}`);261
await refreshAfterChange(requestGeneration, t("Konfigurace byla uložena a test uspěl. Projeví se po plynulém načtení konfigurace."));262
});263
}265
async function toggleSite() {266
const site = selected;267
if (!site) return;268
const enabled = !site.enabled;269
const name = site.domain || site.file;270
if (!confirmOn(enabled ? t("Aktivovat web {name} a spustit test konfigurace?", { name }) : t("Deaktivovat web {name} a spustit test konfigurace? Soubor webu zůstane na serveru.", { name }))) return;271
await writeOperation(t("Měním stav webu a ověřuji konfiguraci…"), async (requestGeneration) => {272
const result = await sessionResult(api.apache.setSiteEnabled(elements.sessionSelect.value, { path: site.linkPath ? site.editPath : site.file, linkPath: site.linkPath || "", enabled, confirm: true, ...auth() }), requestGeneration);273
showOutput(result.testOutput);274
selected = null;275
await refreshAfterChange(requestGeneration, t("Stav webu byl změněn a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));276
});277
}279
async function reloadApache() {280
if (!confirmOn(t("Provést plynulé načtení konfigurace Apache? Před ním se znovu spustí test konfigurace."))) return;281
await writeOperation(t("Ověřuji konfiguraci a provádím plynulé načtení…"), async (requestGeneration) => {282
const result = await sessionResult(api.apache.reload(elements.sessionSelect.value, { confirm: true, ...auth() }), requestGeneration);283
showOutput(result.output);284
await sessionResult(loadInventory(true), requestGeneration);285
if (result.missingPorts?.length) setStatus(t("Načtení proběhlo, ale Apache nenaslouchá na portech {ports}. Chybí Listen, nebo port obsadilo něco jiného.", { ports: result.missingPorts.join(", ") }), true);286
else setStatus(t("Apache byl plynule načten."));287
});288
}290
async function undoLastChange() {291
const items = writeState.lastChange;292
if (!items.length || !confirmOn(t("Vrátit {files} do stavu před poslední změnou a spustit test konfigurace?", { files: items.map((item) => item.path).join(", ") }))) return;293
await writeOperation(t("Obnovuji zálohu a ověřuji konfiguraci…"), async (requestGeneration) => {294
for (const item of items) await sessionResult(api.apache.restoreBackup(elements.sessionSelect.value, { path: item.path, backup: item.backup, expectedSha256: item.sha256, confirm: true, ...auth() }), requestGeneration);295
rememberChange([]);296
await refreshAfterChange(requestGeneration, t("Změna byla vrácena a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));297
});298
}300
// --- fixes ----------------------------------------------------------------------------------------302
function addFindingFix(element, item) {303
const holder = element.querySelector(".badges");304
const button = (label, handler) => { const control = node("button", "mini", label); control.type = "button"; control.addEventListener("click", (event) => { event.stopPropagation(); handler(); }); holder.append(control); };305
if (TEXT_FIXES.has(item.code) && item.file) button(t("Navrhnout opravu"), () => void planFix(item));306
if (item.code === "no-listen") {307
holder.lastChild?.remove();308
const anchor = (inventory?.server?.listen || []).at(-1);309
if (anchor) button(t("Navrhnout opravu"), () => void planFix({ ...item, file: anchor.file, line: anchor.line }));310
}311
}313
async function planFix(item) {314
await writeOperation(t("Připravuji náhled změn…"), async (requestGeneration) => {315
const link = (inventory?.files || []).find((file) => file.path === item.file)?.linkTarget;316
showPlan(await sessionResult(api.apache.planFix(elements.sessionSelect.value, { path: link || item.file, code: item.code, line: item.line, params: item.params, ...auth() }), requestGeneration));317
});318
}320
function proposalText(item) {321
if (item.kind === "broken-link") return t("Symlink vede na neexistující soubor {target}. Oprava ho odstraní.", { target: item.target });322
const older = !item.older ? "" : item.identical ? ` ${t("Stejná kopie v sites-available se použije.")}` : ` ${t("V sites-available je jiná verze. Nejdřív si prohlédněte rozdíl: převod by ji nahradil načteným souborem a starou uložil do zálohy.")}`;323
return `${t("Skutečný soubor v sites-enabled funguje a lze ho vypínat i takto. Sjednocení by ho přesunulo do {path} a vytvořilo symlink.", { path: item.available })}${older}`;324
}325
function proposalDiffers(item) { return Boolean(item.older && !item.identical); }326
function keptKey() { return `chj.apache.kept.${selectedSession()?.host || ""}`; }327
function visibleProposals() { const kept = readStored(keptKey(), []); return (inventory?.proposals || []).filter((item) => !kept.includes(item.path)); }329
function renderProposals() {330
const items = visibleProposals();331
byId("proposalsBlock").hidden = !items.length;332
byId("proposalsList").replaceChildren(...items.map((item) => {333
const element = row(item.path, proposalText(item));334
const holder = element.querySelector(".badges");335
const add = (label, handler, disabled = false, title = "") => { const control = node("button", "mini", label); control.type = "button"; control.disabled = disabled; control.title = title; control.addEventListener("click", handler); holder.append(control); };336
if (proposalDiffers(item)) add(t("Rozdíl"), () => void showProposalDiff(item));337
const locked = proposalDiffers(item) && !writeState.seenDiffs.has(item.path);338
add(item.kind === "broken-link" ? t("Opravit") : t("Sjednotit"), () => void fixProposal(item), locked, locked ? t("Nejdřív si prohlédněte rozdíl.") : "");339
if (item.kind !== "broken-link") add(t("Ponechat takto"), () => { writeStored(keptKey(), [...readStored(keptKey(), []), item.path]); renderProposals(); });340
return element;341
}));342
}344
async function showProposalDiff(item) {345
await writeOperation(t("Načítám oba soubory…"), async (requestGeneration) => {346
const session = elements.sessionSelect.value;347
const older = await sessionResult(api.apache.readFile(session, { path: item.older, ...auth() }), requestGeneration);348
const loaded = await sessionResult(api.apache.readFile(session, { path: item.path, ...auth() }), requestGeneration);349
renderDiff(byId("proposalDiff"), older.text, loaded.text);350
byId("proposalDiff").hidden = false;351
writeState.seenDiffs.add(item.path);352
renderProposals();353
setStatus(t("Rozdíl: {older} (−) a načtený {loaded} (+).", { older: item.older, loaded: item.path }));354
});355
}357
async function fixProposal(item) {358
const question = item.kind === "broken-link" ? t("Odstranit symlink {path}? Jeho cíl {target} neexistuje, takže Apache tento web nenačítá.", { path: item.path, target: item.target }) : `${t("Přesunout {path} do sites-available a nahradit ho symlinkem? Apache načte stejnou konfiguraci jako dosud, oba soubory se zálohují.", { path: item.path })}${proposalDiffers(item) ? ` ${t("{path} je jiná verze. Nahradí ji načtený soubor, stará verze zůstane v záloze.", { path: item.older })}` : ""}`;359
if (!confirmOn(question)) return;360
await writeOperation(t("Opravuji a ověřuji konfiguraci…"), async (requestGeneration) => {361
const session = elements.sessionSelect.value;362
const result = await sessionResult(item.kind === "broken-link" ? api.apache.dropBrokenLink(session, { path: item.path, confirm: true, ...auth() }) : api.apache.adoptSite(session, { path: item.path, confirm: true, ...auth() }), requestGeneration);363
showOutput(result.backupPath ? `${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}` : result.testOutput);364
byId("proposalDiff").hidden = true;365
await refreshAfterChange(requestGeneration, t("Změna proběhla a test konfigurace uspěl."));366
});367
}369
// --- modules and certificates ---------------------------------------------------------------------371
function moduleAction(name, kind, loaded) {372
if (!inventory?.canManageModules || kind === "static") return [];373
const control = node("button", "mini", loaded ? t("Vypnout") : t("Zapnout"));374
control.type = "button";375
control.addEventListener("click", () => void setModule(name, !loaded));376
return [control];377
}379
async function setModule(name, enabled) {380
if (!confirmOn(enabled ? t("Zapnout modul {name} a spustit test konfigurace?", { name }) : t("Vypnout modul {name} a spustit test konfigurace? Modul, který potřebuje některý aktivní web, aplikace nevypne.", { name }))) return;381
await writeOperation(t("Měním modul a ověřuji konfiguraci…"), async (requestGeneration) => {382
const result = await sessionResult(api.apache.setModule(elements.sessionSelect.value, { name, enabled, confirm: true, ...auth() }), requestGeneration);383
showOutput(result.testOutput);384
setStatus(t("Modul byl změněn a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));385
});386
}388
function acmeCandidates() { return (inventory?.sites || []).filter((site) => site.role === "site" && site.domain && site.documentRoot && !site.rootInherited && !site.domain.startsWith("*")); }390
function renderAcmeSites() {391
const select = byId("acmeSite");392
const previous = select.value;393
select.replaceChildren(...acmeCandidates().map((site) => { const option = node("option", "", site.domain); option.value = `${site.file}:${site.line}`; return option; }));394
if ([...select.options].some((option) => option.value === previous)) select.value = previous;395
}397
async function issueCertificate() {398
const site = acmeCandidates().find((item) => `${item.file}:${item.line}` === byId("acmeSite").value);399
if (!site) { setStatus(t("Vyberte web."), true); return; }400
if (!site.enabled) { setStatus(t("Web je vypnutý. Před vystavením certifikátu ho zapněte a proveďte plynulé načtení konfigurace."), true); return; }401
if (!byId("acmeAgree").checked) { setStatus(t("Nejprve potvrďte souhlas s podmínkami Let's Encrypt."), true); return; }402
const domains = [site.domain, ...(site.aliases || [])];403
if (!confirmOn(t("Požádat Let's Encrypt o certifikát pro {names}?", { names: domains.join(", ") }))) return;404
const fields = auth();405
if (await requestCertificate(site, domains, fields) !== "missing") return;406
if (!confirmOn(`${t("Certbot na tomto serveru chybí.")} ${certbotQuestion(false)}`)) { setStatus(t("Certbot na tomto serveru není nainstalovaný."), true); return; }407
if (await installCertbot(fields, false, true)) await requestCertificate(site, domains, fields);408
}410
async function requestCertificate(site, domains, fields) {411
let missing = false;412
await writeOperation(t("Certbot žádá o certifikát, může to trvat několik minut…"), async (requestGeneration) => {413
let result;414
try {415
result = await sessionResult(api.apache.issueCertificate(elements.sessionSelect.value, { email: byId("acmeEmail").value.trim(), root: site.documentRoot, domains, agreeTos: true, confirm: true, ...fields }), requestGeneration);416
} catch (error) {417
if (!CERTBOT_MISSING.test(errorText(error))) throw error;418
missing = true;419
return;420
}421
showOutput(result.output);422
switchTab("sites"); openDetail(site);423
byId("detailCertificate").value = result.certificate; byId("detailKey").value = result.certificateKey;424
setStatus(t("Certifikát byl vystaven. Teď ho zapněte v detailu webu."));425
});426
return missing ? "missing" : "done";427
}429
function certbotQuestion(allowEpel) {430
return allowEpel431
? t("Certbot není v zapnutých repozitářích tohoto serveru. Přidat repozitář EPEL (balíček epel-release) a nainstalovat certbot?")432
: t("Nainstalovat certbot přes balíčkový správce serveru? Stáhne jeho balíčky a závislosti z repozitářů serveru a zapne automatickou obnovu certifikátů, pokud ji balíček obsahuje.");433
}434
const CERTBOT_MISSING = /certbot is not installed/i;435
const CERTBOT_NEEDS_EPEL = /needs the EPEL repository/i;437
async function installCertbot(fields = auth(), allowEpel = false, asked = false) {438
if (!asked && !confirmOn(certbotQuestion(allowEpel))) return false;439
let needsEpel = false;440
let installed = false;441
await writeOperation(t("Instaluji certbot, může to trvat několik minut…"), async (requestGeneration) => {442
let result;443
try {444
result = await sessionResult(api.apache.installCertbot(elements.sessionSelect.value, { confirm: true, allowEpel, ...fields }), requestGeneration);445
} catch (error) {446
if (!CERTBOT_NEEDS_EPEL.test(errorText(error))) throw error;447
needsEpel = true;448
return;449
}450
elements.sudoPassword.value = fields.sudoPassword;451
await sessionResult(loadInventory(true), requestGeneration);452
showOutput(result.output);453
setStatus(result.renewal === "enabled" ? t("Certbot byl nainstalován a automatická obnova je zapnutá.") : t("Certbot byl nainstalován, ale automatická obnova nebyla zjištěna. Obnovu certifikátů nastavte na serveru ručně."));454
installed = true;455
});456
return needsEpel ? installCertbot(fields, true) : installed;457
}459
// --- configuration editor -------------------------------------------------------------------------461
function isDirty() { return Boolean(writeState.file) && byId("fileText").value !== writeState.file.text; }462
window.__CHJ_HAS_UNSAVED_CHANGES__ = isDirty;464
function syncWriteControls() {465
byId("newSiteButton").disabled = busy || !inventory;466
byId("fileSave").disabled = busy || !writeState.file || !isDirty();467
byId("fileRevert").disabled = busy || !writeState.file;468
byId("fileHistory").disabled = busy || !writeState.file;469
byId("fileText").readOnly = !writeState.file;470
}472
/// Shows a file. It can be edited once its exact bytes and hash came from the server, not from the overview.473
async function openFile(path, line) {474
if (isDirty() && !window.confirm(t("Zahodit neuložené změny aktuální konfigurace?"))) return;475
switchTab("config");476
const area = byId("fileText");477
const known = (inventory?.files || []).find((item) => item.path === path);478
byId("historyBlock").hidden = true;479
writeState.file = null;480
area.value = known ? known.text : t("Soubor nebyl načten: {path}", { path });481
byId("fileMeta").textContent = path;482
const requestGeneration = generation;483
setBusy(true); setStatus(t("Načítám {path}…", { path }));484
try {485
const read = await sessionResult(api.apache.readFile(elements.sessionSelect.value, { path, ...auth() }), requestGeneration);486
writeState.file = { path: read.path, text: read.text, sha256: read.sha256 };487
area.value = read.text;488
setStatus(line ? `${path}:${line}` : path);489
} catch (error) {490
if (requestGeneration !== generation || error?.message === "CHJ_SESSION_CHANGED") return;491
setStatus(needsPassword(error) ? t("Soubor je jen pro čtení. Pro úpravy zadejte heslo pro sudo a otevřete ho znovu.") : errorText(error), !needsPassword(error));492
} finally { if (requestGeneration === generation) setBusy(false); }493
if (line) {494
const before = area.value.split("\n").slice(0, line - 1).join("\n").length + (line > 1 ? 1 : 0);495
area.focus(); area.setSelectionRange(before, before + (area.value.slice(before).indexOf("\n") + 1 || 0));496
area.scrollTop = Math.max(0, (line - 4) * 15);497
} else area.scrollTop = 0;498
}500
async function saveFile() {501
const file = writeState.file;502
if (!file || !isDirty() || !confirmOn(t("Uložit {path}, vytvořit zálohu a spustit test konfigurace? Neplatná konfigurace se automaticky vrátí.", { path: file.path }))) return;503
const text = byId("fileText").value;504
await writeOperation(t("Ukládám zálohu a ověřuji novou konfiguraci…"), async (requestGeneration) => {505
const result = await sessionResult(api.apache.saveFile(elements.sessionSelect.value, { path: file.path, text, expectedSha256: file.sha256, confirm: true, ...auth() }), requestGeneration);506
writeState.file = { ...file, text, sha256: result.sha256 };507
rememberChange([{ path: file.path, backup: result.backupPath, sha256: result.sha256 }]);508
showOutput(`${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}`);509
await refreshAfterChange(requestGeneration, t("Konfigurace byla uložena a test uspěl. Projeví se po plynulém načtení konfigurace."));510
});511
}513
async function openHistory() {514
const file = writeState.file;515
if (!file) return;516
await writeOperation(t("Načítám zálohy…"), async (requestGeneration) => {517
const result = await sessionResult(api.apache.backups(elements.sessionSelect.value, { path: file.path, ...auth() }), requestGeneration);518
byId("historyDiff").hidden = true;519
fill(byId("historyList"), result.backups.map((backup) => {520
const element = row(new Date(backup.modified * 1000).toLocaleString(i18n.locale()), t("{size} B", { size: backup.size }));521
const holder = element.querySelector(".badges");522
const show = node("button", "mini", t("Rozdíl")); show.type = "button"; show.addEventListener("click", () => void showBackup(file, backup.path));523
const restore = node("button", "mini", t("Obnovit")); restore.type = "button"; restore.addEventListener("click", () => void restoreBackup(file, backup.path));524
holder.append(show, restore);525
return element;526
}), t("Tento soubor zatím nemá žádnou zálohu."));527
byId("historyBlock").hidden = false;528
setStatus(t("Nalezeno záloh: {count}.", { count: result.backups.length }));529
});530
}532
async function showBackup(file, backup) {533
await writeOperation(t("Načítám zálohu…"), async (requestGeneration) => {534
const result = await sessionResult(api.apache.readBackup(elements.sessionSelect.value, { path: file.path, backup, ...auth() }), requestGeneration);535
renderDiff(byId("historyDiff"), result.text, file.text);536
byId("historyDiff").hidden = false;537
setStatus(t("Rozdíl mezi zálohou (−) a načteným souborem (+)."));538
});539
}541
async function restoreBackup(file, backup) {542
if (isDirty()) { setStatus(t("Nejprve uložte nebo zahoďte neuložené změny."), true); return; }543
if (!confirmOn(t("Obnovit {path} ze zálohy a spustit test konfigurace? Současný stav se také zálohuje.", { path: file.path }))) return;544
await writeOperation(t("Obnovuji zálohu a ověřuji konfiguraci…"), async (requestGeneration) => {545
const result = await sessionResult(api.apache.restoreBackup(elements.sessionSelect.value, { path: file.path, backup, expectedSha256: file.sha256, confirm: true, ...auth() }), requestGeneration);546
showOutput(`${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}`);547
writeState.file = null;548
await sessionResult(loadInventory(true), requestGeneration);549
setStatus(t("Záloha byla obnovena a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));550
byId("historyBlock").hidden = true;551
await openFile(file.path);552
});553
}555
function resetWrite() {556
Object.assign(writeState, { plan: null, pendingEdit: null, file: null, missingModules: [], seenDiffs: new Set() });557
rememberChange([]);558
for (const id of ["wizard", "diffPanel", "proposalDiff", "historyBlock"]) byId(id).hidden = true;559
byId("fileText").value = ""; byId("fileMeta").textContent = "";560
}562
function initWrite() {563
byId("newSiteButton").addEventListener("click", () => { selected = null; renderDetail(); renderWrite(); byId("wizard").hidden = false; syncWizard(); byId("wizard").scrollIntoView({ block: "start" }); byId("wizardDomain").focus(); });564
byId("wizardClose").addEventListener("click", () => { byId("wizard").hidden = true; });565
byId("wizard").addEventListener("input", (event) => { if (event.target.id === "wizardRoot") event.target.dataset.edited = "1"; syncWizard(); });566
byId("wizardPreview").addEventListener("click", previewSite);567
byId("wizardDeploy").addEventListener("click", deploySite);568
byId("wizardEnableModules").addEventListener("click", enableMissingModules);569
byId("wizardTemplateSelect").addEventListener("change", loadTemplate);570
byId("wizardTemplateSave").addEventListener("click", saveTemplate);571
byId("wizardTemplateDelete").addEventListener("click", deleteTemplate);572
byId("detailToggle").addEventListener("click", toggleSite);573
byId("detailPlan").addEventListener("click", planDetail);574
byId("diffCancel").addEventListener("click", () => { writeState.pendingEdit = null; byId("diffPanel").hidden = true; });575
byId("diffApply").addEventListener("click", applyEdit);576
byId("undoButton").addEventListener("click", undoLastChange);577
byId("acmeIssue").addEventListener("click", issueCertificate);578
byId("certbotInstallButton").addEventListener("click", () => void installCertbot());579
byId("fileText").addEventListener("input", syncWriteControls);580
byId("fileSave").addEventListener("click", saveFile);581
byId("fileRevert").addEventListener("click", () => { if (writeState.file) { const { path } = writeState.file; byId("fileText").value = writeState.file.text; void openFile(path); } });582
byId("fileHistory").addEventListener("click", openHistory);583
renderTemplates(); syncWizard();584
window.addEventListener("beforeunload", (event) => { if (isDirty()) { event.preventDefault(); event.returnValue = ""; } });585
}SHA-256
043c017f04adbb7df4fb5123cf0c7c0a6bbc5e14d6b2a987e534bf370e25b9b9CH-J Proprietary Software License 1.14 · ソースは CH-J Proprietary Software License 1.14 に基づいて提供されています。公開されていても、ライセンス条項が変わったり、追加の権利が付与されたりすることはありません。
アーカイブの SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f