diff --git a/AGENTS.md b/AGENTS.md
index 942302a..73aa20f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -133,7 +133,7 @@ Noch nicht umgesetzt — bitte nicht als vorhanden dokumentieren, bevor es wirkl
- [ ] Automatischer Server-Neustart (`systemd` oder `pm2`).
- [ ] SSL/HTTPS für Produktionsbetrieb.
- [ ] Docker-Containerisierung.
-- [ ] Path-Traversal-Schutz und Auth für den PUT-Endpoint.
+- [ ] Auth für den PUT-Endpoint.
---
diff --git a/README.md b/README.md
index 28e0f09..5aa35f9 100644
--- a/README.md
+++ b/README.md
@@ -119,7 +119,7 @@ python scripts/validate.py pfad/zum/template.json
python scripts/validate.py --all
# Nur JSON-Templates
-python scripts/validate.py --type-json
+python scripts/validate.py --all --type-json
```
---
diff --git a/docs/INDEX.md b/docs/INDEX.md
index 3b399ce..5ae1c02 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -25,4 +25,4 @@ Dieser Ordner enthält alle technischen und prozessbezogenen Dokumentationen fü
---
-*Letzte Aktualisierung: `$(date +%Y-%m-%d)`*
+*Letzte Aktualisierung: 2026-05-03*
diff --git a/web/file_ops.py b/web/file_ops.py
index 594661a..e50f8aa 100644
--- a/web/file_ops.py
+++ b/web/file_ops.py
@@ -10,25 +10,6 @@ from pathlib import Path
from typing import Optional
-def read_file(filepath: str) -> Optional[str]:
- """
- Liest eine Datei und gibt den Inhalt als String zurück.
-
- Args:
- filepath: Absoluter Pfad zur Datei
-
- Returns:
- Dateiinhalt als UTF-8 String, oder None bei Fehler
- """
- try:
- with open(filepath, "r", encoding="utf-8") as f:
- return f.read()
- except FileNotFoundError:
- return None
- except Exception:
- return None
-
-
def read_file_binary(filepath: str) -> Optional[bytes]:
"""
Liest eine Datei im Binärmodus.
@@ -71,11 +52,6 @@ def write_file(filepath: str, content: bytes) -> bool:
return False
-def file_exists(filepath: str) -> bool:
- """Prüft, ob eine Datei existiert."""
- return os.path.isfile(filepath)
-
-
def directory_exists(dirpath: str) -> bool:
"""Prüft, ob ein Verzeichnis existiert."""
return os.path.isdir(dirpath)
diff --git a/web/handler.py b/web/handler.py
index 547426b..3f98d84 100644
--- a/web/handler.py
+++ b/web/handler.py
@@ -35,6 +35,26 @@ class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, directory=None, **kwargs):
super().__init__(*args, directory=directory or self.directory, **kwargs)
+ def send_response(self, code, message=None):
+ if self.request_version != 'HTTP/0.9':
+ if message is None:
+ if code in self.responses:
+ message = self.responses[code][0]
+ else:
+ message = ''
+ if not hasattr(self, '_headers_buffer'):
+ self._headers_buffer = []
+ self._headers_buffer.append(("%s %d %s\r\n" %
+ (self.protocol_version, code, message)).encode('latin-1', 'strict'))
+ # Write Server and Date headers (mimicking base class behavior)
+ self.send_header('Server', self.version_string())
+ self.send_header('Date', self.date_time_string())
+ # Security headers
+ self.send_header('X-Content-Type-Options', 'nosniff')
+ self.send_header('X-Frame-Options', 'DENY')
+ self.send_header('Content-Security-Policy',
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'")
+
def do_PUT(self):
"""Speichert eine Template-Datei."""
file_path = self.validator.resolve_template_path(urlparse(self.path).path)
@@ -77,7 +97,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
self.wfile.write(b"File saved successfully")
except Exception as e:
logger.error("Failed to save file: %s", e)
- self.send_error(500, f"Failed to save file: {e}")
+ self.send_error(500, "Failed to save file")
def do_GET(self):
"""Liefert Dateien aus."""
diff --git a/web/index.html b/web/index.html
index 848e20e..073415d 100644
--- a/web/index.html
+++ b/web/index.html
@@ -47,7 +47,7 @@
-
+
@@ -90,7 +90,7 @@
-
+
diff --git a/web/js/api.js b/web/js/api.js
index 0f407e4..6288c5a 100644
--- a/web/js/api.js
+++ b/web/js/api.js
@@ -90,7 +90,7 @@ async function scanTemplates() {
format: 'json'
});
}
- } catch (e) {}
+ } catch (e) { console.error('scanTemplates error:', e); }
}
const userFiles = ['email_draft', 'brainstorming'];
@@ -110,7 +110,7 @@ async function scanTemplates() {
format: 'md'
});
}
- } catch (e) {}
+ } catch (e) { console.error('scanTemplates error:', e); }
}
return templates;
diff --git a/web/js/editor.js b/web/js/editor.js
index 6ac25ca..dac3147 100644
--- a/web/js/editor.js
+++ b/web/js/editor.js
@@ -5,7 +5,6 @@
* die Werte zurück in ein JSON-Objekt.
*/
-let editContainerRef = null;
let currentIndent = 2;
/**
@@ -81,6 +80,9 @@ function createJsonEditUI(container, jsonData, editPathHint = '') {
if (!container) return;
Object.keys(data).forEach(key => {
+ if (key.includes('__proto__') || key.includes('constructor') || key.includes('prototype')) {
+ return;
+ }
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = data[key];
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -264,30 +266,39 @@ function extractJsonFromForm(formDiv) {
// Sort by key to ensure stable parent-first construction
inputEntries.sort((a,b) => a.key.localeCompare(b.key));
- const result = {};
+ const result = Object.create(null);
inputEntries.forEach(({ key, value }) => {
const parts = key.split('.');
let cur = result;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLast = i === parts.length - 1;
- const m = part.match(/^([^\[\]]+)\[(\d+)\]$/);
- if (m && !isLast) {
+ const lastBracket = part.lastIndexOf('[');
+ const lastCloseBracket = part.lastIndexOf(']');
+ let m = null;
+ if (lastBracket !== -1 && lastCloseBracket !== -1 && lastCloseBracket > lastBracket) {
+ const beforeBracket = part.substring(0, lastBracket);
+ const between = part.substring(lastBracket + 1, lastCloseBracket);
+ const afterBracket = part.substring(lastCloseBracket + 1);
+ if (afterBracket === '' && /^\d+$/.test(between)) {
+ m = [part, beforeBracket, between];
+ }
+ }
+ if (m && isLast) {
const base = m[1];
const idx = Number(m[2]);
- if (!cur[base]) cur[base] = [];
- if (!cur[base][idx]) cur[base][idx] = {};
- cur = cur[base][idx];
- } else if (m && isLast) {
- // Array leaf
- const base = m[1];
- const idx = Number(m[2]);
- if (!cur[base]) cur[base] = [];
+ if (!cur[base]) cur[base] = Object.create(null);
cur[base][idx] = value;
+ } else if (m && !isLast) {
+ const base = m[1];
+ const idx = Number(m[2]);
+ if (!cur[base]) cur[base] = [];
+ if (!cur[base][idx]) cur[base][idx] = Object.create(null);
+ cur = cur[base][idx];
} else if (isLast) {
cur[part] = value;
} else {
- if (!cur[part]) cur[part] = {};
+ if (!cur[part]) cur[part] = Object.create(null);
cur = cur[part];
}
}
@@ -520,7 +531,13 @@ async function applyTunedContent() {
* Schließe Tune-Modal
*/
function closeTuneModal() {
- document.getElementById('tune-modal').classList.remove('active');
+ window._lastFocusedElement = document.activeElement;
+ var modal = document.getElementById('tune-modal');
+ modal.classList.remove('active');
+ _setAriaHidden('tune-modal', true);
+ if (window._lastFocusedElement && window._lastFocusedElement.focus) {
+ window._lastFocusedElement.focus();
+ }
window._tuneState = { path: '', rawPath: '', template: '' };
}
diff --git a/web/js/main.js b/web/js/main.js
index d757ac0..febc8e8 100644
--- a/web/js/main.js
+++ b/web/js/main.js
@@ -14,10 +14,26 @@ document.getElementById('search').addEventListener('input', (e) => {
document.getElementById('modal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
+document.getElementById('edit-modal').addEventListener('click', (e) => {
+ if (e.target === e.currentTarget) closeEditModal();
+});
+document.getElementById('tune-modal').addEventListener('click', (e) => {
+ if (e.target === e.currentTarget) closeTuneModal();
+});
+
+// Focus trap for edit-modal
+document.getElementById('edit-modal').addEventListener('keydown', function(e) {
+ if (e.key === 'Tab') { _trapFocus(e, document.getElementById('edit-modal')); }
+});
+
+// Focus trap for tune-modal
+document.getElementById('tune-modal').addEventListener('keydown', function(e) {
+ if (e.key === 'Tab') { _trapFocus(e, document.getElementById('tune-modal')); }
+});
// Escape key closes modal
document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') { closeEditModal(); closeModal(); }
+ if (e.key === 'Escape') { closeEditModal(); closeModal(); closeTuneModal(); }
});
// Hash -> type filter
@@ -38,6 +54,11 @@ document.querySelectorAll('.nav a').forEach(link => {
});
});
+// Set initial aria-hidden on all modals
+_setAriaHidden('modal', true);
+_setAriaHidden('edit-modal', true);
+_setAriaHidden('tune-modal', true);
+
// Initial load
loadTemplates().then(t => {
allTemplates = t;
diff --git a/web/js/modal.js b/web/js/modal.js
index 805efea..391d321 100644
--- a/web/js/modal.js
+++ b/web/js/modal.js
@@ -2,29 +2,87 @@
* Modal-Management für View und Edit Modals.
*/
+var _lastFocusedElement = null;
+
+function _getFocusableElements(container) {
+ var selectors = 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])';
+ return Array.from(container.querySelectorAll(selectors)).filter(function(el) {
+ return !el.closest('.hidden');
+ });
+}
+
+function _trapFocus(e, container) {
+ if (e.key !== 'Tab') return;
+ var focusable = _getFocusableElements(container);
+ if (focusable.length === 0) return;
+ var first = focusable[0];
+ var last = focusable[focusable.length - 1];
+ if (e.shiftKey) {
+ if (document.activeElement === first) {
+ e.preventDefault();
+ last.focus();
+ }
+ } else {
+ if (document.activeElement === last) {
+ e.preventDefault();
+ first.focus();
+ }
+ }
+}
+
+function _setAriaHidden(id, hidden) {
+ var el = document.getElementById(id);
+ if (el) {
+ if (hidden) {
+ el.setAttribute('aria-hidden', 'true');
+ } else {
+ el.removeAttribute('aria-hidden');
+ }
+ }
+}
+
/**
* Zeige View-Modal mit Titel und Inhalt
* @param {string} title - Titel des Modals
* @param {string} content - Inhalt als Text
*/
function showModal(title, content) {
+ _lastFocusedElement = document.activeElement;
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-content').textContent = content;
- document.getElementById('modal').classList.add('active');
+ var modal = document.getElementById('modal');
+ modal.classList.add('active');
+ _setAriaHidden('modal', false);
+ var focusable = _getFocusableElements(modal);
+ if (focusable.length > 0) {
+ focusable[0].focus();
+ }
+ modal.addEventListener('keydown', function(e) { _trapFocus(e, modal); });
}
/**
* Schließe View-Modal
*/
function closeModal() {
- document.getElementById('modal').classList.remove('active');
+ var modal = document.getElementById('modal');
+ modal.classList.remove('active');
+ _setAriaHidden('modal', true);
+ if (_lastFocusedElement && _lastFocusedElement.focus) {
+ _lastFocusedElement.focus();
+ }
}
/**
* Schließe Edit-Modal
*/
function closeEditModal() {
- document.getElementById('edit-modal').classList.remove('active');
+ _lastFocusedElement = document.activeElement;
+ var modal = document.getElementById('edit-modal');
+ modal.classList.remove('active');
+ _setAriaHidden('edit-modal', true);
+ if (_lastFocusedElement && _lastFocusedElement.focus) {
+ _lastFocusedElement.focus();
+ }
}
/**
@@ -35,11 +93,4 @@ function wasViewModalOpen() {
return window._wasViewModalOpen || false;
}
-/**
- * Merke dass View-Modal offen war
- */
-function rememberViewModalOpen() {
- window._wasViewModalOpen = true;
-}
-
// exported functions are global (loaded as