From b6cf03c4cf9d6bd887fac8da3eee5ea9050106b6 Mon Sep 17 00:00:00 2001 From: bsiggel Date: Sat, 24 Jan 2026 00:05:18 +0100 Subject: [PATCH] Replace legacy Bash check and rebuild script with a new Python-based validator; add comprehensive validation checks and improve documentation for usage and features. --- README.md | 27 +- custom/scripts/QUICKSTART.md | 120 ++++++ custom/scripts/VALIDATOR_README.md | 182 +++++++++ custom/scripts/check_and_rebuild.sh | 218 ---------- custom/scripts/validate_and_rebuild.py | 536 +++++++++++++++++++++++++ 5 files changed, 858 insertions(+), 225 deletions(-) create mode 100644 custom/scripts/QUICKSTART.md create mode 100644 custom/scripts/VALIDATOR_README.md delete mode 100755 custom/scripts/check_and_rebuild.sh create mode 100755 custom/scripts/validate_and_rebuild.py diff --git a/README.md b/README.md index c512b1e1..71aee8ef 100644 --- a/README.md +++ b/README.md @@ -44,22 +44,28 @@ Keine integrierte KI-Schnittstelle existiert, aber mit Dateizugriff können auto **WICHTIG:** Nach jeder Änderung an Custom-Dateien muss ein Rebuild durchgeführt werden! -### Check & Rebuild Script (Empfohlen) +### Validate & Rebuild Script (Empfohlen) -**Zentrales Tool:** `custom/scripts/check_and_rebuild.sh` +**Zentrales Tool:** `custom/scripts/validate_and_rebuild.py` + +**NEU ab Januar 2026:** Erweitertes Python-basiertes Validierungs-Tool mit automatischen Checks! Dieses Script sollte **IMMER** verwendet werden (nicht manueller Rebuild). Es führt automatisch aus: ✅ **Validierungen:** - JSON-Syntax-Prüfung aller `.json` Dateien im `custom/` Verzeichnis +- **Relationship-Konsistenz-Prüfung** (bidirektionale Links, foreign-Definitionen) +- **Formula-Script Platzierung** (korrekt in `/formula/` statt `/entityDefs/`) +- **i18n-Vollständigkeit** (fehlende Übersetzungen für Links) +- Layout-Struktur-Prüfung - Dateirechte-Prüfung (`www-data:www-data` Owner) -- System-Checks (Cache-Verzeichnis, Logs-Verzeichnis) ✅ **Automatische Korrekturen:** - Setzt fehlerhafte Dateirechte auf `www-data:www-data` - Korrigiert Verzeichnis-Permissions (775) und Datei-Permissions (664) ✅ **Rebuild:** +- **Nur wenn keine kritischen Fehler gefunden werden!** - Merged alle Custom-Metadata mit Core-Definitionen - Aktualisiert Datenbank-Schema (neue Felder, Tabellen, Indizes) - Leert Cache-Verzeichnis @@ -68,18 +74,25 @@ Dieses Script sollte **IMMER** verwendet werden (nicht manueller Rebuild). Es f **Verwendung:** ```bash # Im EspoCRM-Root-Verzeichnis ausführen -./custom/scripts/check_and_rebuild.sh +python3 custom/scripts/validate_and_rebuild.py + +# Nur Validierung ohne Rebuild +python3 custom/scripts/validate_and_rebuild.py --dry-run ``` **Ausgabe:** - ✓ Grün: Alles in Ordnung, Rebuild erfolgreich -- ⚠ Gelb: Warnungen, Rebuild wird trotzdem ausgeführt -- ✗ Rot: Fehler (z.B. ungültiges JSON), Rebuild wird NICHT ausgeführt +- ⚠ Gelb: Warnungen (z.B. fehlende i18n), Rebuild wird trotzdem ausgeführt +- ✗ Rot: Kritische Fehler (z.B. ungültiges JSON, fehlende Relationships), Rebuild wird NICHT ausgeführt **Bei Fehlern:** - JSON-Syntax-Fehler werden mit Datei und Zeilennummer angezeigt +- Relationship-Fehler zeigen fehlende Links zwischen Entities +- Formula-Platzierungsfehler werden erkannt und gemeldet +- i18n-Probleme werden als Warnungen angezeigt (kein Abbruch) - Dateirechte-Probleme werden automatisch korrigiert -- System-Fehler (fehlende Verzeichnisse) müssen manuell behoben werden + +**Detaillierte Dokumentation:** Siehe `custom/scripts/VALIDATOR_README.md` ### Wann Rebuild erforderlich? diff --git a/custom/scripts/QUICKSTART.md b/custom/scripts/QUICKSTART.md new file mode 100644 index 00000000..d60459ac --- /dev/null +++ b/custom/scripts/QUICKSTART.md @@ -0,0 +1,120 @@ +# 🚀 EspoCRM Validator - Quick Start + +## Installation bereits abgeschlossen ✅ + +Das Validierungs-Tool ist bereits installiert und einsatzbereit! + +## Verwendung + +```bash +# Im EspoCRM-Root-Verzeichnis +python3 custom/scripts/validate_and_rebuild.py +``` + +Das war's! Das Script führt automatisch alle Checks durch und startet den Rebuild. + +**Tipp:** Für nur Validierung ohne Rebuild: `python3 custom/scripts/validate_and_rebuild.py --dry-run` + +## Was wird geprüft? + +1. ✅ **JSON-Syntax** - Alle Custom-Dateien +2. ✅ **Relationships** - Bidirektionale Link-Konsistenz +3. ✅ **Formula-Scripts** - Korrekte Platzierung +4. ✅ **i18n** - Vollständigkeit der Übersetzungen +5. ✅ **Layouts** - Struktur-Validierung +6. ✅ **Dateirechte** - Owner & Permissions +7. ✅ **Rebuild** - Nur bei Fehlerfreiheit + +## Output verstehen + +| Symbol | Bedeutung | Aktion | +|--------|-----------|--------| +| ✓ Grün | OK | Keine | +| ⚠ Gelb | Warnung | Optional beheben | +| ✗ Rot | Fehler | **Muss behoben werden!** | + +## Beispiel + +```bash +$ python3 custom/scripts/validate_and_rebuild.py + +EspoCRM Custom Entity Validator & Rebuild Tool +Arbeitsverzeichnis: /var/lib/docker/volumes/vmh-espocrm_espocrm/_data + +====================================================================== + 1. JSON-SYNTAX VALIDIERUNG +====================================================================== + +✓ Alle 547 JSON-Dateien sind syntaktisch korrekt + +====================================================================== + 2. RELATIONSHIP-KONSISTENZ +====================================================================== + +✗ 4 Relationship-Fehler gefunden: + • CMietobjekt.vmhRumungsklages → CVmhRumungsklage: + Foreign link 'mietobjekte' fehlt in CVmhRumungsklage +... + +✗ REBUILD ABGEBROCHEN: Kritische Fehler müssen behoben werden! +``` + +## Häufige Fehler beheben + +### Relationship-Fehler +```json +// In CVmhRumungsklage.json HINZUFÜGEN: +{ + "links": { + "mietobjekte": { + "type": "hasMany", + "entity": "CMietobjekt", + "foreign": "vmhRumungsklages" + } + } +} +``` + +### JSON-Syntax-Fehler +- Mit einem JSON-Validator prüfen (z.B. `python3 -m json.tool datei.json`) +- Auf fehlende Kommata, geschweifte Klammern achten + +### i18n-Warnung (optional) +```json +// In i18n/de_DE/Entity.json ERGÄNZEN: +{ + "links": { + "meinLink": "Mein Link" + } +} +``` + +## Mehr Infos + +- **Ausführliche Doku:** `custom/scripts/VALIDATOR_README.md` +- **EspoCRM-Doku:** `README.md` +- **Custom-Struktur:** `custom/CUSTOM_DIRECTORY.md` + +## Troubleshooting + +**Python3 nicht gefunden?** +```bash +sudo apt-get install python3 # Ubuntu/Debian +sudo yum install python3 # CentOS/RHEL +``` + +**Keine Berechtigung?** +```bash +chmod +x custom/scripts/validate_and_rebuild.py +``` + +**Script findet rebuild.php nicht?** +```bash +# Ins richtige Verzeichnis wechseln +cd /var/lib/docker/volumes/vmh-espocrm_espocrm/_data +python3 custom/scripts/validate_and_rebuild.py +``` + +--- + +**💡 Tipp:** Führe das Script nach **jeder** Änderung an Custom-Dateien aus! diff --git a/custom/scripts/VALIDATOR_README.md b/custom/scripts/VALIDATOR_README.md new file mode 100644 index 00000000..8305712f --- /dev/null +++ b/custom/scripts/VALIDATOR_README.md @@ -0,0 +1,182 @@ +# EspoCRM Validator & Rebuild Tool + +## Übersicht + +Das neue Python-basierte Validierungs-Tool `validate_and_rebuild.py` ersetzt das bisherige Bash-Script und bietet erweiterte Prüfungen für EspoCRM Custom-Entities. + +## Features + +### ✅ Automatische Validierungen + +1. **JSON-Syntax-Prüfung** + - Validiert alle `.json` Dateien im `custom/` Verzeichnis + - Findet Syntax-Fehler mit Dateiname und Zeilennummer + +2. **Relationship-Konsistenz** + - Prüft bidirektionale Relationships (hasMany/hasOne) + - Validiert `foreign`-Links zwischen Entities + - Überprüft `relationName`-Konsistenz + - Erkennt fehlende Gegenseiten-Definitionen + +3. **Formula-Script Platzierung** + - Prüft ob Formula-Scripts in `/formula/` statt `/entityDefs/` liegen + - Warnt vor leeren Formula-Definitionen + +4. **i18n-Vollständigkeit** + - Findet fehlende Übersetzungen (de_DE / en_US) + - Prüft Link-Labels für alle Custom-Relationships + - Warnt bei komplett fehlenden i18n-Dateien + +5. **Layout-Struktur** + - Validiert clientDefs und bottomPanels + - Findet unnötige `false`-Elemente + +6. **Dateirechte** + - Prüft Owner (www-data:www-data) + - Korrigiert Permissions automatisch + +7. **Rebuild-Ausführung** + - Führt `rebuild.php` nur aus, wenn keine kritischen Fehler vorliegen + +## Verwendung + +### Direkt ausführen +```bash +cd /path/to/espocrm +python3 custom/scripts/validate_and_rebuild.py +``` + + + +## Exit Codes + +- `0` - Erfolg: Alle Validierungen bestanden, Rebuild erfolgreich +- `1` - Fehler: Kritische Fehler gefunden oder Rebuild fehlgeschlagen + +## Output-Format + +Das Script verwendet farbcodierte Ausgaben: + +- 🟢 **Grün (✓)**: Erfolgreich / OK +- 🟡 **Gelb (⚠)**: Warnungen (nicht kritisch) +- 🔴 **Rot (✗)**: Fehler (kritisch, Rebuild wird abgebrochen) +- 🔵 **Blau (ℹ)**: Informationen + +## Fehlertypen + +### Kritische Fehler (Rebuild-Abbruch) +- JSON-Syntax-Fehler +- Fehlende Relationship-Definitionen +- Falsch platzierte Formula-Scripts + +### Warnungen (kein Abbruch) +- Fehlende i18n-Übersetzungen +- Layout-Strukturprobleme +- Dateirechte-Probleme + +## Beispiel-Output + +``` +EspoCRM Custom Entity Validator & Rebuild Tool +Arbeitsverzeichnis: /var/lib/docker/volumes/vmh-espocrm_espocrm/_data + +====================================================================== + 1. JSON-SYNTAX VALIDIERUNG +====================================================================== + +✓ Alle 547 JSON-Dateien sind syntaktisch korrekt + +====================================================================== + 2. RELATIONSHIP-KONSISTENZ +====================================================================== + +✗ 4 Relationship-Fehler gefunden: + • Contact.cBankverbindungenContact: Ziel-Entity 'CBankverbindung' existiert nicht + • CMietobjekt.vmhRumungsklages → CVmhRumungsklage: Foreign link 'mietobjekte' fehlt + +====================================================================== + ZUSAMMENFASSUNG +====================================================================== + +FEHLER: 4 + ✗ Contact.cBankverbindungenContact: Ziel-Entity 'CBankverbindung' existiert nicht + ... + +REBUILD ABGEBROCHEN: Kritische Fehler müssen behoben werden! +``` + +## Erweiterung + +Das Script ist modular aufgebaut. Neue Validierungen können einfach hinzugefügt werden: + +```python +def validate_custom_check(self) -> bool: + """Eigene Validierung.""" + print_header("X. CUSTOM CHECK") + + # Prüflogik hier + if error_found: + self.errors.append("Fehlerbeschreibung") + return False + + print_success("Check erfolgreich") + return True +``` + +Dann in `validate_all()` hinzufügen: + +```python +if not self.validate_custom_check(): + all_valid = False +``` + +## Anforderungen + +- Python 3.6+ +- Keine zusätzlichen Packages erforderlich (nur Standard-Library) +- Optionale sudo-Rechte für Dateirechte-Korrektur + +## Integration in Workflow + +Das Script kann in automatisierte Workflows integriert werden: + +```bash +# In CI/CD Pipeline +python3 custom/scripts/validate_and_rebuild.py || exit 1 + +# Als Git Pre-Commit Hook +#!/bin/bash +python3 custom/scripts/validate_and_rebuild.py --dry-run +``` + +## Troubleshooting + +### Script findet rebuild.php nicht +```bash +# Stelle sicher, dass du im EspoCRM-Root bist +cd /var/lib/docker/volumes/vmh-espocrm_espocrm/_data +python3 custom/scripts/validate_and_rebuild.py +``` + +### Dateirechte können nicht korrigiert werden +```bash +# Manuell mit sudo korrigieren +sudo chown -R www-data:www-data custom/ +sudo find custom/ -type f -exec chmod 664 {} \; +sudo find custom/ -type d -exec chmod 775 {} \; +``` + +### Python3 nicht verfügbar +```bash +# Ubuntu/Debian +sudo apt-get install python3 + +# CentOS/RHEL +sudo yum install python3 +``` + +## Siehe auch + +- [README.md](../../README.md) - Hauptdokumentation +- [CUSTOM_DIRECTORY.md](../CUSTOM_DIRECTORY.md) - Custom-Verzeichnis-Struktur +- [workflow_manager.php](workflow_manager.php) - Workflow-Management-Tool diff --git a/custom/scripts/check_and_rebuild.sh b/custom/scripts/check_and_rebuild.sh deleted file mode 100755 index a46c0610..00000000 --- a/custom/scripts/check_and_rebuild.sh +++ /dev/null @@ -1,218 +0,0 @@ -#!/bin/bash - -# EspoCRM Check & Rebuild Script -# Prüft auf häufige Fehler und führt bei Fehlerfreiheit einen Rebuild durch - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -CUSTOM_DIR="$SCRIPT_DIR/custom" -ERRORS=0 -WARNINGS=0 - -echo "==========================================" -echo "EspoCRM Check & Rebuild Script" -echo "==========================================" -echo "" - -# Farben für Output -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# 1. JSON-Syntax prüfen -echo -e "${BLUE}[1/3] Prüfe JSON-Syntax...${NC}" -echo "---" - -JSON_FILES=$(find "$CUSTOM_DIR" -type f -name "*.json" 2>/dev/null) -JSON_COUNT=$(echo "$JSON_FILES" | grep -c . || echo 0) - -if [ "$JSON_COUNT" -eq 0 ]; then - echo -e "${YELLOW}⚠ Warnung: Keine JSON-Dateien im custom/ Verzeichnis gefunden${NC}" - WARNINGS=$((WARNINGS + 1)) -else - echo "Gefundene JSON-Dateien: $JSON_COUNT" - - INVALID_JSON=0 - while IFS= read -r file; do - if [ -n "$file" ]; then - if ! jq empty "$file" 2>/dev/null; then - echo -e "${RED}✗ Fehler: Ungültiges JSON in $file${NC}" - ERRORS=$((ERRORS + 1)) - INVALID_JSON=$((INVALID_JSON + 1)) - fi - fi - done <<< "$JSON_FILES" - - if [ "$INVALID_JSON" -eq 0 ]; then - echo -e "${GREEN}✓ Alle JSON-Dateien sind gültig${NC}" - else - echo -e "${RED}✗ $INVALID_JSON JSON-Dateien mit Syntaxfehlern gefunden${NC}" - fi -fi - -echo "" - -# 2. Dateirechte prüfen und korrigieren -echo -e "${BLUE}[2/3] Prüfe Dateirechte...${NC}" -echo "---" - -WRONG_OWNER=0 -FIXED_FILES=0 -CUSTOM_FILES=$(find "$CUSTOM_DIR" -type f 2>/dev/null || echo "") -CUSTOM_DIRS=$(find "$CUSTOM_DIR" -type d 2>/dev/null || echo "") - -if [ -z "$CUSTOM_FILES" ]; then - echo -e "${YELLOW}⚠ Warnung: Keine Dateien im custom/ Verzeichnis gefunden${NC}" - WARNINGS=$((WARNINGS + 1)) -else - # Prüfe und korrigiere Dateien einzeln - while IFS= read -r file; do - if [ -n "$file" ]; then - OWNER=$(stat -c '%U:%G' "$file" 2>/dev/null || echo "unknown:unknown") - if [ "$OWNER" != "www-data:www-data" ]; then - WRONG_OWNER=$((WRONG_OWNER + 1)) - # Korrigiere direkt nur diese Datei - if sudo chown www-data:www-data "$file" 2>/dev/null && sudo chmod 664 "$file" 2>/dev/null; then - FIXED_FILES=$((FIXED_FILES + 1)) - fi - fi - fi - done <<< "$CUSTOM_FILES" - - # Prüfe und korrigiere Verzeichnisse einzeln - while IFS= read -r dir; do - if [ -n "$dir" ]; then - OWNER=$(stat -c '%U:%G' "$dir" 2>/dev/null || echo "unknown:unknown") - if [ "$OWNER" != "www-data:www-data" ]; then - sudo chown www-data:www-data "$dir" 2>/dev/null && sudo chmod 775 "$dir" 2>/dev/null - fi - fi - done <<< "$CUSTOM_DIRS" - - if [ "$WRONG_OWNER" -eq 0 ]; then - echo -e "${GREEN}✓ Alle Dateien haben korrekte Berechtigungen (www-data:www-data)${NC}" - else - if [ "$FIXED_FILES" -eq "$WRONG_OWNER" ]; then - echo -e "${GREEN}✓ $FIXED_FILES Dateien korrigiert${NC}" - else - echo -e "${RED}✗ Fehler: Konnte nicht alle Berechtigungen korrigieren${NC}" - ERRORS=$((ERRORS + 1)) - fi - fi -fi - -echo "" - -# 3. Cache-Verzeichnis prüfen -echo -e "${BLUE}[3/3] Prüfe System...${NC}" -echo "---" - -if [ ! -d "$SCRIPT_DIR/data/cache" ]; then - echo -e "${RED}✗ Fehler: Cache-Verzeichnis existiert nicht${NC}" - ERRORS=$((ERRORS + 1)) -else - echo -e "${GREEN}✓ Cache-Verzeichnis existiert${NC}" -fi - -if [ ! -d "$SCRIPT_DIR/data/logs" ]; then - echo -e "${RED}✗ Fehler: Logs-Verzeichnis existiert nicht${NC}" - ERRORS=$((ERRORS + 1)) -else - echo -e "${GREEN}✓ Logs-Verzeichnis existiert${NC}" -fi - -echo "" -echo "==========================================" -echo "Zusammenfassung:" -echo "---" - -if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then - echo -e "${GREEN}✓ Keine Fehler oder Warnungen gefunden${NC}" -elif [ "$ERRORS" -eq 0 ]; then - echo -e "${YELLOW}⚠ $WARNINGS Warnung(en) gefunden (Rebuild wird trotzdem ausgeführt)${NC}" -else - echo -e "${RED}✗ $ERRORS Fehler und $WARNINGS Warnung(en) gefunden${NC}" -fi - -echo "==========================================" -echo "" - -# Entscheidung: Rebuild durchführen oder nicht -if [ "$ERRORS" -gt 0 ]; then - echo -e "${RED}REBUILD WIRD NICHT DURCHGEFÜHRT${NC}" - echo "Bitte behebe die oben genannten Fehler und führe das Script erneut aus." - exit 1 -else - echo -e "${GREEN}Starte Rebuild und Cache-Bereinigung...${NC}" - echo "" - - # Rebuild und Cache-Bereinigung durchführen - if command -v docker &> /dev/null; then - echo -e "${BLUE}[1/2] Führe Clear Cache aus...${NC}" - if docker exec espocrm php /var/www/html/command.php ClearCache 2>&1; then - echo -e "${GREEN}✓ Cache erfolgreich gelöscht${NC}" - echo "" - - echo -e "${BLUE}[2/2] Führe Rebuild aus...${NC}" - if docker exec espocrm php /var/www/html/command.php rebuild 2>&1; then - echo "" - echo -e "${GREEN}==========================================" - echo "✓ REBUILD ERFOLGREICH ABGESCHLOSSEN" - echo "==========================================${NC}" - exit 0 - else - echo "" - echo -e "${RED}==========================================" - echo "✗ REBUILD FEHLGESCHLAGEN" - echo "==========================================${NC}" - echo "" - echo -e "${YELLOW}Letzte Log-Einträge:${NC}" - echo "---" - # Zeige die letzten 30 Zeilen der neuesten Log-Datei - LATEST_LOG=$(ls -t "$SCRIPT_DIR/data/logs/"*.log 2>/dev/null | head -1) - if [ -n "$LATEST_LOG" ]; then - echo -e "${BLUE}Aus: $(basename "$LATEST_LOG")${NC}" - tail -n 30 "$LATEST_LOG" - else - echo -e "${YELLOW}Keine Log-Dateien gefunden in data/logs/${NC}" - fi - exit 1 - fi - else - echo -e "${RED}✗ Clear Cache fehlgeschlagen${NC}" - echo "Fahre mit Rebuild fort..." - echo "" - - if docker exec espocrm php /var/www/html/command.php rebuild 2>&1; then - echo "" - echo -e "${GREEN}==========================================" - echo "✓ REBUILD ERFOLGREICH ABGESCHLOSSEN" - echo "==========================================${NC}" - exit 0 - else - echo "" - echo -e "${RED}==========================================" - echo "✗ REBUILD FEHLGESCHLAGEN" - echo "==========================================${NC}" - echo "" - echo -e "${YELLOW}Letzte Log-Einträge:${NC}" - echo "---" - # Zeige die letzten 30 Zeilen der neuesten Log-Datei - LATEST_LOG=$(ls -t "$SCRIPT_DIR/data/logs/"*.log 2>/dev/null | head -1) - if [ -n "$LATEST_LOG" ]; then - echo -e "${BLUE}Aus: $(basename "$LATEST_LOG")${NC}" - tail -n 30 "$LATEST_LOG" - else - echo -e "${YELLOW}Keine Log-Dateien gefunden in data/logs/${NC}" - fi - exit 1 - fi - fi - else - echo -e "${RED}✗ Docker nicht gefunden. Rebuild kann nicht durchgeführt werden.${NC}" - exit 1 - fi -fi diff --git a/custom/scripts/validate_and_rebuild.py b/custom/scripts/validate_and_rebuild.py new file mode 100755 index 00000000..830f90c7 --- /dev/null +++ b/custom/scripts/validate_and_rebuild.py @@ -0,0 +1,536 @@ +#!/usr/bin/env python3 +""" +EspoCRM Custom Entity Validator & Rebuild Tool +Führt umfassende Validierungen durch bevor der Rebuild ausgeführt wird. +""" + +import json +import os +import sys +import subprocess +import re +from pathlib import Path +from typing import Dict, List, Tuple, Set +from collections import defaultdict + +# ANSI Color Codes +class Colors: + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BLUE = '\033[94m' + BOLD = '\033[1m' + END = '\033[0m' + +def print_header(text: str): + print(f"\n{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}") + print(f"{Colors.BOLD}{Colors.BLUE}{text.center(70)}{Colors.END}") + print(f"{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}\n") + +def print_success(text: str): + print(f"{Colors.GREEN}✓{Colors.END} {text}") + +def print_warning(text: str): + print(f"{Colors.YELLOW}⚠{Colors.END} {text}") + +def print_error(text: str): + print(f"{Colors.RED}✗{Colors.END} {text}") + +def print_info(text: str): + print(f"{Colors.BLUE}ℹ{Colors.END} {text}") + +class EntityValidator: + def __init__(self, base_path: str): + self.base_path = Path(base_path) + self.custom_path = self.base_path / "custom" / "Espo" / "Custom" / "Resources" + self.metadata_path = self.custom_path / "metadata" + self.i18n_path = self.custom_path / "i18n" + self.errors = [] + self.warnings = [] + self.entity_defs = {} + self.relationships = defaultdict(list) + + def validate_json_syntax(self) -> bool: + """Validiere JSON-Syntax aller Dateien im custom-Verzeichnis.""" + print_header("1. JSON-SYNTAX VALIDIERUNG") + + json_files = list(self.custom_path.rglob("*.json")) + if not json_files: + print_warning("Keine JSON-Dateien gefunden") + return True + + invalid_files = [] + for json_file in json_files: + try: + with open(json_file, 'r', encoding='utf-8') as f: + json.load(f) + except json.JSONDecodeError as e: + self.errors.append(f"JSON-Fehler in {json_file.relative_to(self.base_path)}: {e}") + invalid_files.append(str(json_file.relative_to(self.base_path))) + + if invalid_files: + print_error(f"{len(invalid_files)} Datei(en) mit JSON-Fehlern gefunden:") + for f in invalid_files: + print(f" {Colors.RED}•{Colors.END} {f}") + return False + else: + print_success(f"Alle {len(json_files)} JSON-Dateien sind syntaktisch korrekt") + return True + + def load_entity_defs(self): + """Lade alle entityDefs für weitere Analysen.""" + entity_defs_path = self.metadata_path / "entityDefs" + if not entity_defs_path.exists(): + return + + for json_file in entity_defs_path.glob("*.json"): + entity_name = json_file.stem + try: + with open(json_file, 'r', encoding='utf-8') as f: + self.entity_defs[entity_name] = json.load(f) + except Exception as e: + # Fehler wird bereits in JSON-Validierung gemeldet + pass + + def validate_relationships(self) -> bool: + """Validiere Relationship-Definitionen zwischen Entities.""" + print_header("2. RELATIONSHIP-KONSISTENZ") + + if not self.entity_defs: + print_warning("Keine entityDefs geladen") + return True + + relationship_errors = [] + checked_pairs = set() + + # Links die nicht geprüft werden (Standard-EspoCRM parent-Relationships) + skip_foreign_links = {'parent', 'parents'} + + for entity_name, entity_def in self.entity_defs.items(): + links = entity_def.get('links', {}) + + for link_name, link_def in links.items(): + link_type = link_def.get('type') + target_entity = link_def.get('entity') + foreign = link_def.get('foreign') + relation_name = link_def.get('relationName') + + # Überspringe parent-Links (Standard Activities-Relationship) + if foreign in skip_foreign_links: + continue + + # Nur hasMany und hasOne prüfen (nicht belongsTo, da das die Gegenseite ist) + if link_type in ['hasMany', 'hasOne'] and target_entity and foreign: + pair_key = tuple(sorted([f"{entity_name}.{link_name}", f"{target_entity}.{foreign}"])) + if pair_key in checked_pairs: + continue + checked_pairs.add(pair_key) + + # Prüfe ob Ziel-Entity existiert + if target_entity not in self.entity_defs: + relationship_errors.append( + f"{entity_name}.{link_name}: Ziel-Entity '{target_entity}' existiert nicht" + ) + continue + + target_links = self.entity_defs[target_entity].get('links', {}) + + # Prüfe ob foreign Link existiert + if foreign not in target_links: + relationship_errors.append( + f"{entity_name}.{link_name} → {target_entity}: " + f"Foreign link '{foreign}' fehlt in {target_entity}" + ) + continue + + foreign_def = target_links[foreign] + foreign_foreign = foreign_def.get('foreign') + foreign_relation_name = foreign_def.get('relationName') + + # Prüfe ob foreign.foreign zurück zeigt + if foreign_foreign != link_name: + relationship_errors.append( + f"{entity_name}.{link_name} ↔ {target_entity}.{foreign}: " + f"Foreign zeigt auf '{foreign_foreign}' statt auf '{link_name}'" + ) + + # Prüfe ob relationName übereinstimmt (falls beide definiert) + if relation_name and foreign_relation_name and relation_name != foreign_relation_name: + relationship_errors.append( + f"{entity_name}.{link_name} ↔ {target_entity}.{foreign}: " + f"relationName unterschiedlich ('{relation_name}' vs '{foreign_relation_name}')" + ) + + if relationship_errors: + print_error(f"{len(relationship_errors)} Relationship-Fehler gefunden:") + for err in relationship_errors: + print(f" {Colors.RED}•{Colors.END} {err}") + self.errors.extend(relationship_errors) + return False + else: + print_success(f"{len(checked_pairs)} Relationships geprüft - alle konsistent") + return True + + def validate_formula_placement(self) -> bool: + """Prüfe ob Formula-Scripts korrekt in /formula/ statt /entityDefs/ platziert sind.""" + print_header("3. FORMULA-SCRIPT PLATZIERUNG") + + misplaced_formulas = [] + + # Prüfe entityDefs auf formula-Definitionen (sollte nicht da sein) + for entity_name, entity_def in self.entity_defs.items(): + if 'formula' in entity_def: + misplaced_formulas.append( + f"entityDefs/{entity_name}.json enthält 'formula' - " + f"sollte in formula/{entity_name}.json sein" + ) + + # Prüfe ob formula-Dateien existieren und valide sind + formula_path = self.metadata_path / "formula" + formula_count = 0 + if formula_path.exists(): + for formula_file in formula_path.glob("*.json"): + formula_count += 1 + try: + with open(formula_file, 'r', encoding='utf-8') as f: + formula_def = json.load(f) + # Prüfe auf leere oder null Scripts + for key, value in formula_def.items(): + if value == "" or value is None: + self.warnings.append( + f"formula/{formula_file.name}: '{key}' ist leer oder null" + ) + except Exception: + pass # JSON-Fehler bereits gemeldet + + if misplaced_formulas: + print_error(f"{len(misplaced_formulas)} Formula-Platzierungsfehler:") + for err in misplaced_formulas: + print(f" {Colors.RED}•{Colors.END} {err}") + self.errors.extend(misplaced_formulas) + return False + else: + print_success(f"{formula_count} Formula-Definitionen korrekt platziert") + return True + + def validate_i18n_completeness(self) -> bool: + """Prüfe i18n-Definitionen auf Vollständigkeit.""" + print_header("4. i18n-VOLLSTÄNDIGKEIT") + + if not self.entity_defs: + print_warning("Keine entityDefs zum Prüfen") + return True + + missing_i18n = [] + incomplete_i18n = [] + + languages = ['de_DE', 'en_US'] + custom_entities = [name for name in self.entity_defs.keys() + if name.startswith('C') or name.startswith('CVmh')] + + for entity_name in custom_entities: + entity_def = self.entity_defs[entity_name] + links = entity_def.get('links', {}) + + # Finde alle hasMany/hasOne Links die übersetzt werden sollten + custom_links = [] + for link_name, link_def in links.items(): + link_type = link_def.get('type') + if link_type in ['hasMany', 'hasOne']: + # Überspringe System-Links + if link_name not in ['createdBy', 'modifiedBy', 'assignedUser', 'teams']: + custom_links.append(link_name) + + if not custom_links: + continue + + for lang in languages: + i18n_file = self.i18n_path / lang / f"{entity_name}.json" + + if not i18n_file.exists(): + missing_i18n.append(f"{entity_name}: {lang} fehlt komplett") + continue + + try: + with open(i18n_file, 'r', encoding='utf-8') as f: + i18n_def = json.load(f) + links_i18n = i18n_def.get('links', {}) + + # Prüfe ob alle custom Links übersetzt sind + for link_name in custom_links: + if link_name not in links_i18n: + incomplete_i18n.append( + f"{entity_name} ({lang}): Link '{link_name}' fehlt in i18n" + ) + except Exception: + pass # JSON-Fehler bereits gemeldet + + total_issues = len(missing_i18n) + len(incomplete_i18n) + + if missing_i18n: + print_error(f"{len(missing_i18n)} komplett fehlende i18n-Dateien:") + for err in missing_i18n[:10]: # Max 10 anzeigen + print(f" {Colors.RED}•{Colors.END} {err}") + if len(missing_i18n) > 10: + print(f" {Colors.RED}...{Colors.END} und {len(missing_i18n) - 10} weitere") + + if incomplete_i18n: + print_warning(f"{len(incomplete_i18n)} unvollständige i18n-Definitionen:") + for err in incomplete_i18n[:10]: # Max 10 anzeigen + print(f" {Colors.YELLOW}•{Colors.END} {err}") + if len(incomplete_i18n) > 10: + print(f" {Colors.YELLOW}...{Colors.END} und {len(incomplete_i18n) - 10} weitere") + + if not missing_i18n and not incomplete_i18n: + print_success(f"i18n für {len(custom_entities)} Custom-Entities vollständig") + + # i18n-Fehler sind nur Warnungen, kein Abbruch + self.warnings.extend(missing_i18n + incomplete_i18n) + return True + + def validate_layout_structure(self) -> bool: + """Prüfe Layout-Dateien auf häufige Fehler.""" + print_header("5. LAYOUT-STRUKTUR VALIDIERUNG") + + layouts_path = self.metadata_path / "clientDefs" + if not layouts_path.exists(): + print_warning("Keine clientDefs gefunden") + return True + + layout_errors = [] + checked_layouts = 0 + + for client_def_file in layouts_path.glob("*.json"): + try: + with open(client_def_file, 'r', encoding='utf-8') as f: + client_def = json.load(f) + + # Prüfe auf häufige Layout-Fehler in bottomPanels + bottom_panels = client_def.get('bottomPanelsDetail', {}) + for panel_key, panel_def in bottom_panels.items(): + checked_layouts += 1 + + # Prüfe auf unnötige false-Elemente + if isinstance(panel_def, dict): + for key, value in panel_def.items(): + if value is False and key not in ['disabled', 'sticked']: + layout_errors.append( + f"{client_def_file.stem}: bottomPanelsDetail.{panel_key}.{key} " + f"sollte nicht 'false' sein" + ) + except Exception: + pass # JSON-Fehler bereits gemeldet + + if layout_errors: + print_warning(f"{len(layout_errors)} Layout-Strukturprobleme:") + for err in layout_errors[:5]: + print(f" {Colors.YELLOW}•{Colors.END} {err}") + if len(layout_errors) > 5: + print(f" {Colors.YELLOW}...{Colors.END} und {len(layout_errors) - 5} weitere") + self.warnings.extend(layout_errors) + else: + print_success(f"{checked_layouts} Layout-Definitionen geprüft") + + return True + + def check_file_permissions(self) -> bool: + """Prüfe Dateirechte im custom-Verzeichnis.""" + print_header("6. DATEIRECHTE-PRÜFUNG") + + try: + # Prüfe ob Dateien von www-data gehören + result = subprocess.run( + ['find', str(self.custom_path), '!', '-user', 'www-data', '-o', '!', '-group', 'www-data'], + capture_output=True, + text=True + ) + + wrong_owner_files = [line for line in result.stdout.strip().split('\n') if line] + + if wrong_owner_files: + print_warning(f"{len(wrong_owner_files)} Dateien mit falschen Rechten gefunden") + print_info("Versuche automatische Korrektur...") + + # Versuche Rechte zu korrigieren + try: + subprocess.run( + ['sudo', 'chown', '-R', 'www-data:www-data', str(self.custom_path)], + check=True, + capture_output=True + ) + subprocess.run( + ['sudo', 'find', str(self.custom_path), '-type', 'f', '-exec', 'chmod', '664', '{}', ';'], + check=True, + capture_output=True + ) + subprocess.run( + ['sudo', 'find', str(self.custom_path), '-type', 'd', '-exec', 'chmod', '775', '{}', ';'], + check=True, + capture_output=True + ) + print_success("Dateirechte korrigiert") + except subprocess.CalledProcessError: + print_warning("Konnte Dateirechte nicht automatisch korrigieren (sudo erforderlich)") + else: + print_success("Alle Dateirechte korrekt (www-data:www-data)") + + return True + except Exception as e: + print_warning(f"Konnte Dateirechte nicht prüfen: {e}") + return True + + def run_rebuild(self) -> bool: + """Führe den EspoCRM Rebuild aus.""" + print_header("7. ESPOCRM REBUILD") + + rebuild_script = self.base_path / "rebuild.php" + if not rebuild_script.exists(): + print_error(f"rebuild.php nicht gefunden in {self.base_path}") + return False + + try: + print_info("Starte Rebuild (kann 10-30 Sekunden dauern)...") + result = subprocess.run( + ['php', str(rebuild_script)], + cwd=str(self.base_path), + capture_output=True, + text=True, + timeout=60 + ) + + if result.returncode == 0: + print_success("Rebuild erfolgreich abgeschlossen") + return True + else: + print_error("Rebuild fehlgeschlagen:") + if result.stderr: + print(f"\n{result.stderr}") + return False + except subprocess.TimeoutExpired: + print_error("Rebuild-Timeout (>60 Sekunden)") + return False + except Exception as e: + print_error(f"Rebuild-Fehler: {e}") + return False + + def print_summary(self): + """Drucke Zusammenfassung aller Ergebnisse.""" + print_header("ZUSAMMENFASSUNG") + + if self.errors: + print(f"\n{Colors.RED}{Colors.BOLD}FEHLER: {len(self.errors)}{Colors.END}") + for err in self.errors: + print(f" {Colors.RED}✗{Colors.END} {err}") + + if self.warnings: + print(f"\n{Colors.YELLOW}{Colors.BOLD}WARNUNGEN: {len(self.warnings)}{Colors.END}") + for warn in self.warnings[:10]: + print(f" {Colors.YELLOW}⚠{Colors.END} {warn}") + if len(self.warnings) > 10: + print(f" {Colors.YELLOW}...{Colors.END} und {len(self.warnings) - 10} weitere Warnungen") + + if not self.errors and not self.warnings: + print(f"\n{Colors.GREEN}{Colors.BOLD}✓ ALLE PRÜFUNGEN BESTANDEN{Colors.END}") + + print() + + def validate_all(self) -> bool: + """Führe alle Validierungen durch.""" + all_valid = True + + # 1. JSON-Syntax (kritisch) + if not self.validate_json_syntax(): + all_valid = False + print_error("\nAbbruch: JSON-Syntax-Fehler müssen behoben werden!\n") + return False + + # Lade entityDefs für weitere Checks + self.load_entity_defs() + + # 2. Relationships (kritisch) + if not self.validate_relationships(): + all_valid = False + + # 3. Formula-Platzierung (kritisch) + if not self.validate_formula_placement(): + all_valid = False + + # 4. i18n-Vollständigkeit (nur Warnung) + self.validate_i18n_completeness() + + # 5. Layout-Struktur (nur Warnung) + self.validate_layout_structure() + + # 6. Dateirechte (nicht kritisch für Rebuild) + self.check_file_permissions() + + return all_valid + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description='EspoCRM Custom Entity Validator & Rebuild Tool' + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Nur Validierungen durchführen, kein Rebuild' + ) + parser.add_argument( + '--no-rebuild', + action='store_true', + help='Synonym für --dry-run' + ) + args = parser.parse_args() + + dry_run = args.dry_run or args.no_rebuild + + # Finde EspoCRM Root-Verzeichnis + script_dir = Path(__file__).parent.parent.parent + + if not (script_dir / "rebuild.php").exists(): + print_error("Fehler: Nicht im EspoCRM-Root-Verzeichnis!") + print_info(f"Aktueller Pfad: {script_dir}") + sys.exit(1) + + print(f"{Colors.BOLD}EspoCRM Custom Entity Validator & Rebuild Tool{Colors.END}") + print(f"Arbeitsverzeichnis: {script_dir}") + if dry_run: + print(f"{Colors.YELLOW}Modus: DRY-RUN (kein Rebuild){Colors.END}") + print() + + validator = EntityValidator(str(script_dir)) + + # Validierungen durchführen + all_valid = validator.validate_all() + + # Zusammenfassung drucken + validator.print_summary() + + # Entscheidung über Rebuild + if not all_valid: + print_error("REBUILD ABGEBROCHEN: Kritische Fehler müssen behoben werden!") + sys.exit(1) + + if dry_run: + print_info("Dry-Run Modus: Rebuild übersprungen") + print(f"\n{Colors.GREEN}{Colors.BOLD}✓ VALIDIERUNGEN ABGESCHLOSSEN{Colors.END}\n") + sys.exit(0) + + if validator.warnings: + print_warning( + f"Es gibt {len(validator.warnings)} Warnungen, aber keine kritischen Fehler." + ) + print_info("Rebuild wird trotzdem durchgeführt...\n") + + # Rebuild ausführen + if validator.run_rebuild(): + print(f"\n{Colors.GREEN}{Colors.BOLD}✓ ERFOLGREICH ABGESCHLOSSEN{Colors.END}\n") + sys.exit(0) + else: + print(f"\n{Colors.RED}{Colors.BOLD}✗ REBUILD FEHLGESCHLAGEN{Colors.END}\n") + sys.exit(1) + +if __name__ == "__main__": + main()