Replace legacy Bash check and rebuild script with a new Python-based validator; add comprehensive validation checks and improve documentation for usage and features.

This commit is contained in:
2026-01-24 00:05:18 +01:00
parent ee7a36ef41
commit b6cf03c4cf
5 changed files with 858 additions and 225 deletions

View File

@@ -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!

View File

@@ -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

View File

@@ -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

View File

@@ -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()