pre_rules_htmx.py

Code Hygiene Score: 100

Keine Issues gefunden.

Dependencies 3

Funktionen 7

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,
]
← Übersicht