pre_rules_htmx.py
- Pfad:
/var/www/tools/ki-protokoll/claude-hook/quality/pre_rules_htmx.py - Namespace: claude-hook.quality
- Zeilen: 133 | Größe: 4,386 Bytes
- Geändert: 2025-12-27 11:51:03 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 100
- Dependencies: 100 (25%)
- LOC: 100 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 3
- use re
- use typing.Optional
- use rule_base.block
Funktionen 7
-
is_view_file()Zeile 22 -
_check_csrf_on_method()Zeile 31 -
htmx_c1_csrf_on_post()Zeile 69 -
htmx_c2_csrf_on_delete()Zeile 74 -
htmx_c3_csrf_on_patch()Zeile 79 -
htmx_c5_csrf_on_put()Zeile 84 -
htmx_c4_delete_requires_confirm()Zeile 89
Code
#!/usr/bin/env python3
"""
Pre-Hook HTMX Regeln (BLOCK) - Contract #14: htmx-patterns.
Kritische Regeln aus dem HTMX-Contract:
- HTMX-C1: CSRF auf hx-post
- HTMX-C2: CSRF auf hx-delete
- HTMX-C3: CSRF auf hx-patch
- HTMX-C4: Confirm auf hx-delete
- HTMX-C5: CSRF auf hx-put
"""
import re
from typing import Optional
from .rule_base import block
# =============================================================================
# VIEW FILE CHECK
# =============================================================================
def is_view_file(file_path: str) -> bool:
"""Prüft ob es sich um eine View-Datei handelt."""
return "/View/" in file_path and file_path.endswith(".php")
# =============================================================================
# HELPER: Generic CSRF Check
# =============================================================================
def _check_csrf_on_method(
file_path: str,
content: str,
method: str,
rule_id: str
) -> Optional[dict]:
"""Generische CSRF-Prüfung für hx-{method} Attribute."""
if not is_view_file(file_path):
return None
pattern = rf'hx-{method}\s*=\s*["\'][^"\']+["\']'
matches = list(re.finditer(pattern, content))
if not matches:
return None
for match in matches:
# Suche im umgebenden Kontext (150 Zeichen vor und nach)
start = max(0, match.start() - 150)
end = min(len(content), match.end() + 150)
context = content[start:end]
# Muss hx-headers mit X-CSRF-TOKEN haben
if 'hx-headers' not in context or 'CSRF' not in context.upper():
line_num = content[:match.start()].count('\n') + 1
return block(
rule_id,
f"hx-{method} at line {line_num} missing CSRF token. "
"Add: hx-headers='{\"X-CSRF-TOKEN\": \"<?= $csrfToken ?>\"}'"
)
return None
# =============================================================================
# HTMX CONTRACT RULES (Critical)
# =============================================================================
def htmx_c1_csrf_on_post(file_path: str, content: str) -> Optional[dict]:
"""HTMX-C1: hx-post MUSS X-CSRF-TOKEN in hx-headers haben."""
return _check_csrf_on_method(file_path, content, "post", "HTMX-C1")
def htmx_c2_csrf_on_delete(file_path: str, content: str) -> Optional[dict]:
"""HTMX-C2: hx-delete MUSS X-CSRF-TOKEN in hx-headers haben."""
return _check_csrf_on_method(file_path, content, "delete", "HTMX-C2")
def htmx_c3_csrf_on_patch(file_path: str, content: str) -> Optional[dict]:
"""HTMX-C3: hx-patch MUSS X-CSRF-TOKEN in hx-headers haben."""
return _check_csrf_on_method(file_path, content, "patch", "HTMX-C3")
def htmx_c5_csrf_on_put(file_path: str, content: str) -> Optional[dict]:
"""HTMX-C5: hx-put MUSS X-CSRF-TOKEN in hx-headers haben."""
return _check_csrf_on_method(file_path, content, "put", "HTMX-C5")
def htmx_c4_delete_requires_confirm(file_path: str, content: str) -> Optional[dict]:
"""HTMX-C4: hx-delete MUSS hx-confirm Attribut haben."""
if not is_view_file(file_path):
return None
hx_delete_pattern = r'hx-delete\s*=\s*["\'][^"\']+["\']'
hx_deletes = list(re.finditer(hx_delete_pattern, content))
if not hx_deletes:
return None
for match in hx_deletes:
# Suche im Element-Kontext (vom < bis zum nächsten >)
# Gehe zurück bis zum < und vorwärts bis zum >
element_start = content.rfind('<', 0, match.start())
element_end = content.find('>', match.end())
if element_start == -1 or element_end == -1:
continue
element = content[element_start:element_end + 1]
if 'hx-confirm' not in element:
line_num = content[:match.start()].count('\n') + 1
return block(
"HTMX-C4",
f"hx-delete at line {line_num} missing confirmation. "
"Add: hx-confirm=\"Wirklich loeschen?\""
)
return None
# =============================================================================
# RULE COLLECTION
# =============================================================================
RULES = [
htmx_c1_csrf_on_post,
htmx_c2_csrf_on_delete,
htmx_c3_csrf_on_patch,
htmx_c4_delete_requires_confirm,
htmx_c5_csrf_on_put,
]