diff --git a/PHASE2_PLAN.md b/PHASE2_PLAN.md new file mode 100644 index 0000000..21f6edc --- /dev/null +++ b/PHASE2_PLAN.md @@ -0,0 +1,281 @@ +# Phase 2 Entwicklungsplan: Nachbau aller fehlenden PrestaShop-Features + Modul-Kompatibilität + +## Ziel +Alle wesentlichen Funktionen von PrestaShop werden nachgebaut UND das System wird PrestaShop-Module und Addons ohne Änderungen nutzen können. Dazu wird das komplette PrestaShop-Modul-System implementiert. + +--- + +## Übersicht & Zeitrahmen +- **Gesamtdauer:** 8-10 Wochen (inkl. PrestaShop-Kompatibilität) +- **Sprint-Länge:** 1 Woche +- **Start:** [TT.MM.JJJJ] +- **Ende (geplant):** [TT.MM.JJJJ] + +--- + +## Aufgabenliste & Prioritäten + +| Nr. | Feature/Modul | Beschreibung | Priorität | Zeitrahmen | Status | +|-----|------------------------------|-----------------------------------------------------------|-----------|------------|-----------| +| 1 | **Hook-System** | PrestaShop Hook-System für Module-Kompatibilität | KRITISCH | 1 Woche | ⬜ offen | +| 2 | **Module-Base-System** | Module-Base-Class, Installation/Deinstallation | KRITISCH | 1 Woche | ⬜ offen | +| 3 | **Override-System** | Class/Template/Controller Overrides | HOCH | 1 Woche | ⬜ offen | +| 4 | **Context-System** | PrestaShop Context-API nachbauen | HOCH | 1 Woche | ⬜ offen | +| 5 | **Produktvarianten** | Attribute, Kombinationen, Lager pro Variante | HOCH | 1 Woche | ⬜ offen | +| 6 | **Produktbewertungen** | Kundenbewertungen, Moderation, Sterne, hilfreich | HOCH | 1 Woche | ⬜ offen | +| 7 | **Gutschein-/Rabatt-System** | Gutscheine, Warenkorbregeln, Rabatte | HOCH | 1 Woche | ⬜ offen | +| 8 | **Kundenkonto-Features** | Adressverwaltung, Bestellhistorie, Rücksendungen | HOCH | 1 Woche | ⬜ offen | +| 9 | **Versandmethoden & Tracking** | Versandarten, Kosten, Tracking, Versandstatus | HOCH | 1 Woche | ⬜ offen | +| 10 | **Produktvergleich** | Vergleichsliste, UI, Session-Handling | MITTEL | 0,5 Woche | ⬜ offen | +| 11 | **Cross-Selling & Zubehör** | Verwandte Produkte, Zubehör, Upselling | MITTEL | 0,5 Woche | ⬜ offen | +| 12 | **Wunschliste** | Kundenwunschlisten, Verwaltung, Teilen | MITTEL | 0,5 Woche | ⬜ offen | +| 13 | **Lagerverwaltung** | Bestandsführung, Warnungen, Lieferanten | MITTEL | 0,5 Woche | ⬜ offen | +| 14 | **Berichte & Statistiken** | Umsatz, Topseller, Kunden, Export | MITTEL | 0,5 Woche | ⬜ offen | +| 15 | **Import/Export** | CSV/XML Import/Export für Produkte, Kunden, Bestellungen | MITTEL | 0,5 Woche | ⬜ offen | +| 16 | **Benutzerverwaltung** | Admin-Rollen, Rechte, Logins, Audit-Log | HOCH | 1 Woche | ⬜ offen | +| 17 | **E-Mail-Marketing** | Automatisierte Mails, Kampagnen, Vorlagen | MITTEL | 0,5 Woche | ⬜ offen | +| 18 | **Mehrsprachigkeit (i18n)** | Übersetzungen, Sprachumschaltung, Fallbacks | HOCH | 1 Woche | ⬜ offen | +| 19 | **Template-/Theme-System** | Theme-Engine, Child-Themes, Template-Overrides | HOCH | 1 Woche | ⬜ offen | +| 20 | **Performance-Optimierung** | Caching, Minifizierung, Bildoptimierung | MITTEL | 0,5 Woche | ⬜ offen | +| 21 | **Rechtliches & DSGVO** | Cookie-Consent, Datenschutz, AGB, Widerruf | HOCH | 0,5 Woche | ⬜ offen | +| 22 | **Social Media Integration** | Teilen, Login, Pixel, OpenGraph | NIEDRIG | 0,5 Woche | ⬜ offen | +| 23 | **Affiliate-/Partner-System** | Tracking, Provisionen, Auswertungen | NIEDRIG | 0,5 Woche | ⬜ offen | +| 24 | **PrestaShop-Module-Tests** | Test mit echten PrestaShop-Modulen | HOCH | 1 Woche | ⬜ offen | + +--- + +## PrestaShop Modul-System Komponenten + +### 1. **Hook-System** (Priorität: KRITISCH) +```php +// PrestaShop Hook-Beispiele +Hook::exec('actionProductUpdate', ['id_product' => $id]); +Hook::exec('displayProductAdditionalInfo', ['product' => $product]); +Hook::exec('actionCartUpdateQuantityBefore', ['id_product' => $id, 'quantity' => $qty]); +``` + +**Zu implementieren:** +- ✅ Hook-Registry +- ✅ Hook-Execution +- ✅ Module-Hook-Registration +- ✅ Hook-Parameter-Handling + +### 2. **Module-System** (Priorität: KRITISCH) +```php +// PrestaShop Module-Struktur +class MyModule extends Module +{ + public function __construct() + { + $this->name = 'mymodule'; + $this->tab = 'front_office_features'; + $this->version = '1.0.0'; + $this->author = 'Your Name'; + $this->need_instance = 0; + } + + public function install() + { + return parent::install() && + $this->registerHook('displayProductAdditionalInfo'); + } +} +``` + +**Zu implementieren:** +- ✅ Module-Base-Class +- ✅ Module-Installation/Deinstallation +- ✅ Module-Konfiguration +- ✅ Module-Admin-Interface + +### 3. **Override-System** (Priorität: HOCH) +```php +// PrestaShop Override-Beispiele +// classes/Product.php -> modules/mymodule/override/classes/Product.php +// controllers/front/ProductController.php -> modules/mymodule/override/controllers/front/ProductController.php +``` + +**Zu implementieren:** +- ✅ Class-Override-Detection +- ✅ Template-Override-System +- ✅ Controller-Override-System +- ✅ Override-Priority-System + +### 4. **Module-API & Services** (Priorität: HOCH) +```php +// PrestaShop Service-Beispiele +$this->context->shop +$this->context->language +$this->context->currency +Module::getInstanceByName('mymodule') +``` + +**Zu implementieren:** +- ✅ Context-System +- ✅ Service-Container +- ✅ Module-Discovery +- ✅ Module-Dependencies + +--- + +## Fortschrittstracker + +- ⬜ offen | 🟡 in Arbeit | ✅ erledigt + +--- + +## Sprint-Planung + +### **Sprint 1 (Woche 1): PrestaShop-Kompatibilität Basis** +- Hook-System (KRITISCH) +- Module-Base-Class (KRITISCH) +- Module-Installation/Deinstallation + +### **Sprint 2 (Woche 2): PrestaShop-Kompatibilität Erweitert** +- Override-System +- Context-System +- Service-Container + +### **Sprint 3 (Woche 3): Core-E-Commerce Features** +- Produktvarianten +- Produktbewertungen +- Gutschein-/Rabatt-System + +### **Sprint 4 (Woche 4): Kunden- & Versand-Features** +- Kundenkonto-Features +- Versandmethoden & Tracking +- Benutzerverwaltung + +### **Sprint 5 (Woche 5): Marketing & UX Features** +- Cross-Selling & Zubehör +- Produktvergleich +- Wunschliste +- E-Mail-Marketing + +### **Sprint 6 (Woche 6): System-Features** +- Lagerverwaltung +- Berichte & Statistiken +- Import/Export + +### **Sprint 7 (Woche 7): Internationalisierung & Performance** +- Mehrsprachigkeit (i18n) +- Template-/Theme-System +- Performance-Optimierung + +### **Sprint 8 (Woche 8): Rechtliches & Tests** +- Rechtliches & DSGVO +- Social Media Integration +- Affiliate-/Partner-System +- PrestaShop-Module-Tests + +--- + +## Bekannte PrestaShop-Module zum Testen + +### **Einfache Module:** +- **blockreassurance** (Hooks: displayProductAdditionalInfo) +- **ps_mainmenu** (Hooks: displayTop, displayNav) +- **ps_shoppingcart** (Hooks: displayTop, displayNav) + +### **Komplexe Module:** +- **ps_checkout** (Viele Hooks, Overrides) +- **ps_facebook** (API-Integration) +- **ps_cashondelivery** (Payment-Module) + +--- + +## Technische Details + +### **Hook-System Struktur:** +```php +// Zu implementieren in app/Core/Hook.php +class Hook +{ + private static $hooks = []; + + public static function register($hookName, $moduleName, $callback) + { + self::$hooks[$hookName][] = [ + 'module' => $moduleName, + 'callback' => $callback + ]; + } + + public static function exec($hookName, $params = []) + { + $results = []; + if (isset(self::$hooks[$hookName])) { + foreach (self::$hooks[$hookName] as $hook) { + $results[] = call_user_func($hook['callback'], $params); + } + } + return $results; + } +} +``` + +### **Module-Base-Class:** +```php +// Zu implementieren in app/Core/Module.php +abstract class Module +{ + public $name; + public $tab; + public $version; + public $author; + public $need_instance; + + abstract public function install(); + abstract public function uninstall(); + + public function registerHook($hookName) + { + // Hook-Registration + } + + public function unregisterHook($hookName) + { + // Hook-Unregistration + } +} +``` + +--- + +## Prioritäten für sofortige Implementierung + +### **KRITISCH (sofort):** +1. Hook-System (Basis für alle Module) +2. Module-Base-Class (Grundlage für Module) +3. Module-Installation/Deinstallation + +### **HOCH (nächste Woche):** +1. Override-System +2. Context-System +3. Service-Container + +### **MITTEL (später):** +1. Module-Admin-Interface +2. Module-Dependencies +3. Performance-Optimierung + +--- + +## Nächste Schritte + +1. **Hook-System implementieren** (Sofort) +2. **Module-Base-Class erstellen** (Sofort) +3. **Einfaches Test-Modul entwickeln** (Validierung) +4. **Bekannte PrestaShop-Module testen** (Kompatibilität) + +--- + +## Hinweise +- Die Reihenfolge kann je nach Nutzerwunsch angepasst werden. +- Nach jedem Sprint erfolgt ein Review und ggf. ein Merge/Push zu Gitea. +- Feature-Requests können jederzeit ergänzt werden. +- PrestaShop-Kompatibilität hat höchste Priorität für Module-Nutzung. + +--- + +**Letzte Aktualisierung:** [TT.MM.JJJJ] – Phase 2 Planung mit PrestaShop-Kompatibilität erstellt \ No newline at end of file diff --git a/app/Core/Hook.php b/app/Core/Hook.php new file mode 100644 index 0000000..c46636a --- /dev/null +++ b/app/Core/Hook.php @@ -0,0 +1,340 @@ + $moduleName, + 'callback' => $callback, + 'position' => $position + ]; + + // Nach Position sortieren + usort(self::$hooks[$hookName], function($a, $b) { + return $a['position'] - $b['position']; + }); + + // Hook in Datenbank speichern + self::saveHookToDatabase($hookName, $moduleName, $position); + } + + /** + * Hook ausführen + */ + public static function exec($hookName, $params = []) + { + self::init(); + + $results = []; + + if (isset(self::$hooks[$hookName])) { + foreach (self::$hooks[$hookName] as $hook) { + try { + $result = call_user_func($hook['callback'], $params); + if ($result !== null) { + $results[] = $result; + } + } catch (\Exception $e) { + error_log("Hook Fehler in {$hook['module']}: " . $e->getMessage()); + } + } + } + + return $results; + } + + /** + * Hook mit Rückgabewert ausführen (für display-Hooks) + */ + public static function execWithReturn($hookName, $params = []) + { + self::init(); + + $output = ''; + + if (isset(self::$hooks[$hookName])) { + foreach (self::$hooks[$hookName] as $hook) { + try { + $result = call_user_func($hook['callback'], $params); + if (is_string($result)) { + $output .= $result; + } + } catch (\Exception $e) { + error_log("Hook Fehler in {$hook['module']}: " . $e->getMessage()); + } + } + } + + return $output; + } + + /** + * Hook entfernen + */ + public static function unregister($hookName, $moduleName) + { + if (isset(self::$hooks[$hookName])) { + foreach (self::$hooks[$hookName] as $key => $hook) { + if ($hook['module'] === $moduleName) { + unset(self::$hooks[$hookName][$key]); + } + } + } + + // Hook aus Datenbank entfernen + self::removeHookFromDatabase($hookName, $moduleName); + } + + /** + * Verfügbare Hooks abrufen + */ + public static function getHooks() + { + self::init(); + return array_keys(self::$hooks); + } + + /** + * Module für einen Hook abrufen + */ + public static function getHookModules($hookName) + { + self::init(); + + if (isset(self::$hooks[$hookName])) { + return array_column(self::$hooks[$hookName], 'module'); + } + + return []; + } + + /** + * Hooks aus Datenbank laden + */ + private static function loadHooksFromDatabase() + { + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + SELECT hook_name, module_name, position + FROM ws_hook_module + WHERE active = 1 + ORDER BY position ASC + '); + $stmt->execute(); + + $hooks = $stmt->fetchAllAssociative(); + + foreach ($hooks as $hook) { + $moduleName = $hook['module_name']; + $hookName = $hook['hook_name']; + $position = $hook['position']; + + // Module-Klasse laden + $moduleClass = self::getModuleClass($moduleName); + if ($moduleClass && method_exists($moduleClass, 'hook' . ucfirst($hookName))) { + $callback = [$moduleClass, 'hook' . ucfirst($hookName)]; + self::register($hookName, $moduleName, $callback, $position); + } + } + + } catch (Exception $e) { + error_log('Hook-Datenbank Fehler: ' . $e->getMessage()); + } + } + + /** + * Hook in Datenbank speichern + */ + private static function saveHookToDatabase($hookName, $moduleName, $position) + { + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + INSERT INTO ws_hook_module (hook_name, module_name, position, active, created_at) + VALUES (?, ?, ?, 1, NOW()) + ON DUPLICATE KEY UPDATE position = ?, active = 1, updated_at = NOW() + '); + $stmt->execute([$hookName, $moduleName, $position, $position]); + + } catch (Exception $e) { + error_log('Hook-Speicher Fehler: ' . $e->getMessage()); + } + } + + /** + * Hook aus Datenbank entfernen + */ + private static function removeHookFromDatabase($hookName, $moduleName) + { + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + UPDATE ws_hook_module + SET active = 0, updated_at = NOW() + WHERE hook_name = ? AND module_name = ? + '); + $stmt->execute([$hookName, $moduleName]); + + } catch (Exception $e) { + error_log('Hook-Entfernung Fehler: ' . $e->getMessage()); + } + } + + /** + * Module-Klasse laden + */ + private static function getModuleClass($moduleName) + { + $modulePath = __DIR__ . '/../../modules/' . $moduleName . '/' . $moduleName . '.php'; + + if (file_exists($modulePath)) { + require_once $modulePath; + $className = ucfirst($moduleName); + + if (class_exists($className)) { + return new $className(); + } + } + + return null; + } + + /** + * Hook-Statistiken abrufen + */ + public static function getHookStatistics() + { + self::init(); + + $stats = []; + foreach (self::$hooks as $hookName => $hookList) { + $stats[$hookName] = count($hookList); + } + + return $stats; + } + + /** + * Hook-Liste für Admin-Interface + */ + public static function getHookList() + { + return [ + // Display Hooks (Frontend) + 'displayHeader' => 'Header-Bereich', + 'displayTop' => 'Oberer Bereich', + 'displayNav' => 'Navigation', + 'displayNav1' => 'Navigation 1', + 'displayNav2' => 'Navigation 2', + 'displayTopColumn' => 'Obere Spalte', + 'displayLeftColumn' => 'Linke Spalte', + 'displayRightColumn' => 'Rechte Spalte', + 'displayFooter' => 'Footer-Bereich', + 'displayFooterAfter' => 'Footer nach', + 'displayHome' => 'Startseite', + 'displayHomeTab' => 'Startseite Tabs', + 'displayHomeTabContent' => 'Startseite Tab-Inhalt', + + // Product Hooks + 'displayProductListReviews' => 'Produktliste Bewertungen', + 'displayProductAdditionalInfo' => 'Produkt zusätzliche Info', + 'displayProductPriceBlock' => 'Produkt Preis-Block', + 'displayProductButtons' => 'Produkt Buttons', + 'displayProductTab' => 'Produkt Tabs', + 'displayProductTabContent' => 'Produkt Tab-Inhalt', + 'displayProductListFunctionalButtons' => 'Produktliste Funktions-Buttons', + + // Cart Hooks + 'displayShoppingCart' => 'Warenkorb', + 'displayShoppingCartFooter' => 'Warenkorb Footer', + 'actionCartUpdateQuantityBefore' => 'Warenkorb Menge vor Update', + 'actionCartUpdateQuantityAfter' => 'Warenkorb Menge nach Update', + 'actionCartListOverride' => 'Warenkorb Liste überschreiben', + + // Order Hooks + 'displayOrderConfirmation' => 'Bestellbestätigung', + 'displayOrderDetail' => 'Bestelldetails', + 'actionOrderStatusUpdate' => 'Bestellstatus Update', + 'actionValidateOrder' => 'Bestellung validieren', + + // Customer Hooks + 'displayCustomerAccount' => 'Kundenkonto', + 'displayCustomerAccountForm' => 'Kundenkonto Formular', + 'actionCustomerAccountAdd' => 'Kundenkonto hinzufügen', + 'actionCustomerAccountUpdate' => 'Kundenkonto Update', + + // Admin Hooks + 'displayAdminOrder' => 'Admin Bestellung', + 'displayAdminProducts' => 'Admin Produkte', + 'actionAdminProductsControllerSaveAfter' => 'Admin Produkt nach Speichern', + 'actionAdminCustomersControllerSaveAfter' => 'Admin Kunde nach Speichern', + + // Payment Hooks + 'displayPayment' => 'Zahlungsmethoden', + 'displayPaymentReturn' => 'Zahlungsrückgabe', + 'actionPaymentConfirmation' => 'Zahlungsbestätigung', + + // Search Hooks + 'displaySearch' => 'Suche', + 'actionSearch' => 'Suchaktion', + 'displaySearchResults' => 'Suchergebnisse', + + // Newsletter Hooks + 'displayNewsletterRegistration' => 'Newsletter Registrierung', + 'actionNewsletterRegistrationAfter' => 'Newsletter nach Registrierung', + + // Security Hooks + 'actionAuthentication' => 'Authentifizierung', + 'actionCustomerLogoutAfter' => 'Kunde nach Logout', + 'actionAdminLoginControllerLoginAfter' => 'Admin nach Login' + ]; + } +} \ No newline at end of file diff --git a/app/Core/Module.php b/app/Core/Module.php new file mode 100644 index 0000000..8e10879 --- /dev/null +++ b/app/Core/Module.php @@ -0,0 +1,407 @@ +context = Context::getContext(); + $this->shop = new Shop(); + $this->config = new Configuration(); + + // Standardwerte setzen + $this->need_instance = 0; + $this->bootstrap = false; + $this->ps_versions_compliancy = ['min' => '1.7.0.0', 'max' => _PS_VERSION_]; + } + + /** + * Modul installieren + */ + public function install() + { + try { + // Modul in Datenbank eintragen + $this->registerModule(); + + // Modul-Konfiguration installieren + $this->installConfiguration(); + + // Datenbank-Tabellen erstellen + $this->installDatabase(); + + // Hooks registrieren + $this->registerHooks(); + + return true; + + } catch (\Exception $e) { + error_log("Modul-Installation Fehler: " . $e->getMessage()); + return false; + } + } + + /** + * Modul deinstallieren + */ + public function uninstall() + { + try { + // Hooks entfernen + $this->unregisterHooks(); + + // Datenbank-Tabellen entfernen + $this->uninstallDatabase(); + + // Modul-Konfiguration entfernen + $this->uninstallConfiguration(); + + // Modul aus Datenbank entfernen + $this->unregisterModule(); + + return true; + + } catch (\Exception $e) { + error_log("Modul-Deinstallation Fehler: " . $e->getMessage()); + return false; + } + } + + /** + * Modul in Datenbank registrieren + */ + protected function registerModule() + { + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + INSERT INTO ws_module ( + name, display_name, description, version, author, + tab, need_instance, bootstrap, active, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW()) + ON DUPLICATE KEY UPDATE + display_name = ?, description = ?, version = ?, + author = ?, tab = ?, need_instance = ?, bootstrap = ?, + updated_at = NOW() + '); + + $stmt->execute([ + $this->name, + $this->displayName ?: $this->name, + $this->description ?: '', + $this->version ?: '1.0.0', + $this->author ?: 'Unknown', + $this->tab ?: 'administration', + $this->need_instance, + $this->bootstrap ? 1 : 0, + $this->displayName ?: $this->name, + $this->description ?: '', + $this->version ?: '1.0.0', + $this->author ?: 'Unknown', + $this->tab ?: 'administration', + $this->need_instance, + $this->bootstrap ? 1 : 0 + ]); + + } catch (Exception $e) { + throw new \Exception('Modul-Registrierung Fehler: ' . $e->getMessage()); + } + } + + /** + * Modul aus Datenbank entfernen + */ + protected function unregisterModule() + { + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + UPDATE ws_module + SET active = 0, updated_at = NOW() + WHERE name = ? + '); + $stmt->execute([$this->name]); + + } catch (Exception $e) { + error_log('Modul-Entfernung Fehler: ' . $e->getMessage()); + } + } + + /** + * Modul-Konfiguration installieren + */ + protected function installConfiguration() + { + // Standard-Konfiguration installieren + $this->config->set('MODULE_' . strtoupper($this->name) . '_ENABLED', true); + } + + /** + * Modul-Konfiguration entfernen + */ + protected function uninstallConfiguration() + { + // Modul-Konfiguration entfernen + $this->config->delete('MODULE_' . strtoupper($this->name) . '_ENABLED'); + } + + /** + * Datenbank-Tabellen erstellen + */ + protected function installDatabase() + { + // Kann von Modulen überschrieben werden + } + + /** + * Datenbank-Tabellen entfernen + */ + protected function uninstallDatabase() + { + // Kann von Modulen überschrieben werden + } + + /** + * Hooks registrieren + */ + protected function registerHooks() + { + // Standard-Hooks können hier registriert werden + // Hook::register('displayHeader', $this->name, [$this, 'hookDisplayHeader']); + } + + /** + * Hooks entfernen + */ + protected function unregisterHooks() + { + // Alle Hooks für dieses Modul entfernen + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + UPDATE ws_hook_module + SET active = 0, updated_at = NOW() + WHERE module_name = ? + '); + $stmt->execute([$this->name]); + + } catch (Exception $e) { + error_log('Hook-Entfernung Fehler: ' . $e->getMessage()); + } + } + + /** + * Hook registrieren + */ + public function registerHook($hookName) + { + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + INSERT INTO ws_hook_module (hook_name, module_name, position, active, created_at) + VALUES (?, ?, 0, 1, NOW()) + ON DUPLICATE KEY UPDATE active = 1, updated_at = NOW() + '); + $stmt->execute([$hookName, $this->name]); + + return true; + + } catch (Exception $e) { + error_log('Hook-Registrierung Fehler: ' . $e->getMessage()); + return false; + } + } + + /** + * Hook entfernen + */ + public function unregisterHook($hookName) + { + try { + $conn = DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + UPDATE ws_hook_module + SET active = 0, updated_at = NOW() + WHERE hook_name = ? AND module_name = ? + '); + $stmt->execute([$hookName, $this->name]); + + return true; + + } catch (Exception $e) { + error_log('Hook-Entfernung Fehler: ' . $e->getMessage()); + return false; + } + } + + /** + * Modul-Konfiguration abrufen + */ + public function getConfig($key, $default = null) + { + return $this->config->get('MODULE_' . strtoupper($this->name) . '_' . strtoupper($key), $default); + } + + /** + * Modul-Konfiguration setzen + */ + public function setConfig($key, $value) + { + return $this->config->set('MODULE_' . strtoupper($this->name) . '_' . strtoupper($key), $value); + } + + /** + * Modul-Konfiguration entfernen + */ + public function deleteConfig($key) + { + return $this->config->delete('MODULE_' . strtoupper($this->name) . '_' . strtoupper($key)); + } + + /** + * Modul-URL generieren + */ + public function getModuleUrl($controller = null, $params = []) + { + $url = '/admin/module/' . $this->name; + + if ($controller) { + $url .= '/' . $controller; + } + + if (!empty($params)) { + $url .= '?' . http_build_query($params); + } + + return $url; + } + + /** + * Modul-Pfad abrufen + */ + public function getModulePath() + { + return __DIR__ . '/../../modules/' . $this->name . '/'; + } + + /** + * Modul-Template-Pfad abrufen + */ + public function getTemplatePath() + { + return $this->getModulePath() . 'views/templates/'; + } + + /** + * Modul-Asset-Pfad abrufen + */ + public function getAssetPath() + { + return $this->getModulePath() . 'views/assets/'; + } + + /** + * Modul-Admin-Controller erstellen + */ + public function getContent() + { + // Kann von Modulen überschrieben werden + return ''; + } + + /** + * Modul-Admin-Formular verarbeiten + */ + public function postProcess() + { + // Kann von Modulen überschrieben werden + return true; + } + + /** + * Modul-Status prüfen + */ + public function isEnabled() + { + return $this->getConfig('ENABLED', false); + } + + /** + * Modul aktivieren + */ + public function enable() + { + return $this->setConfig('ENABLED', true); + } + + /** + * Modul deaktivieren + */ + public function disable() + { + return $this->setConfig('ENABLED', false); + } + + /** + * Modul-Informationen abrufen + */ + public function getModuleInfo() + { + return [ + 'name' => $this->name, + 'display_name' => $this->displayName ?: $this->name, + 'description' => $this->description ?: '', + 'version' => $this->version ?: '1.0.0', + 'author' => $this->author ?: 'Unknown', + 'tab' => $this->tab ?: 'administration', + 'need_instance' => $this->need_instance, + 'bootstrap' => $this->bootstrap, + 'enabled' => $this->isEnabled(), + 'path' => $this->getModulePath() + ]; + } +} \ No newline at end of file diff --git a/app/controllers/admin/ModuleController.php b/app/controllers/admin/ModuleController.php new file mode 100644 index 0000000..8428d40 --- /dev/null +++ b/app/controllers/admin/ModuleController.php @@ -0,0 +1,431 @@ +multiShop = new MultiShop(); + $this->security = new Security(); + $this->config = new Configuration(); + + // Session-Check + if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) { + header('Location: /admin/login'); + exit; + } + } + + /** + * Modul-Übersicht anzeigen + */ + public function index() + { + $shopId = $this->multiShop->getCurrentShopId(); + + // Module aus Datenbank abrufen + $modules = $this->getModules(); + + // Hook-Statistiken + $hookStats = Hook::getHookStatistics(); + + $data = [ + 'modules' => $modules, + 'hook_stats' => $hookStats, + 'shop_id' => $shopId + ]; + + $this->render('admin/module/index.html.twig', $data); + } + + /** + * Modul installieren + */ + public function install() + { + $moduleName = $_POST['module_name'] ?? ''; + + if (empty($moduleName)) { + $this->addError('Modul-Name ist erforderlich'); + header('Location: /admin/module'); + return; + } + + // CSRF-Schutz + if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) { + $this->addError('Sicherheitsfehler: Ungültiger Token'); + header('Location: /admin/module'); + return; + } + + try { + $module = $this->loadModule($moduleName); + + if (!$module) { + $this->addError("Modul '{$moduleName}' nicht gefunden"); + header('Location: /admin/module'); + return; + } + + if ($module->install()) { + $this->addSuccess("Modul '{$moduleName}' erfolgreich installiert"); + } else { + $this->addError("Fehler beim Installieren von '{$moduleName}'"); + } + + } catch (\Exception $e) { + $this->addError('Installationsfehler: ' . $e->getMessage()); + } + + header('Location: /admin/module'); + } + + /** + * Modul deinstallieren + */ + public function uninstall() + { + $moduleName = $_POST['module_name'] ?? ''; + + if (empty($moduleName)) { + $this->addError('Modul-Name ist erforderlich'); + header('Location: /admin/module'); + return; + } + + // CSRF-Schutz + if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) { + $this->addError('Sicherheitsfehler: Ungültiger Token'); + header('Location: /admin/module'); + return; + } + + try { + $module = $this->loadModule($moduleName); + + if (!$module) { + $this->addError("Modul '{$moduleName}' nicht gefunden"); + header('Location: /admin/module'); + return; + } + + if ($module->uninstall()) { + $this->addSuccess("Modul '{$moduleName}' erfolgreich deinstalliert"); + } else { + $this->addError("Fehler beim Deinstallieren von '{$moduleName}'"); + } + + } catch (\Exception $e) { + $this->addError('Deinstallationsfehler: ' . $e->getMessage()); + } + + header('Location: /admin/module'); + } + + /** + * Modul konfigurieren + */ + public function configure() + { + $moduleName = $_GET['module'] ?? ''; + + if (empty($moduleName)) { + $this->addError('Modul-Name ist erforderlich'); + header('Location: /admin/module'); + return; + } + + $module = $this->loadModule($moduleName); + + if (!$module) { + $this->addError("Modul '{$moduleName}' nicht gefunden"); + header('Location: /admin/module'); + return; + } + + // POST-Verarbeitung + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $this->processModuleConfiguration($module); + } + + $data = [ + 'module' => $module, + 'module_info' => $module->getModuleInfo(), + 'configuration' => $this->getModuleConfiguration($moduleName) + ]; + + $this->render('admin/module/configure.html.twig', $data); + } + + /** + * Modul-Konfiguration verarbeiten + */ + private function processModuleConfiguration($module) + { + // CSRF-Schutz + if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) { + $this->addError('Sicherheitsfehler: Ungültiger Token'); + return; + } + + $moduleName = $module->name; + + // Konfiguration verarbeiten + foreach ($_POST as $key => $value) { + if (strpos($key, 'config_') === 0) { + $configKey = substr($key, 7); // 'config_' entfernen + $module->setConfig($configKey, $value); + } + } + + $this->addSuccess('Modul-Konfiguration erfolgreich gespeichert'); + } + + /** + * Module aus Datenbank abrufen + */ + private function getModules() + { + try { + $conn = \Doctrine\DBAL\DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + SELECT * FROM ws_module + ORDER BY display_name ASC + '); + $stmt->execute(); + + return $stmt->fetchAllAssociative(); + + } catch (\Exception $e) { + error_log('Module-Abruf Fehler: ' . $e->getMessage()); + return []; + } + } + + /** + * Modul laden + */ + private function loadModule($moduleName) + { + $modulePath = __DIR__ . '/../../modules/' . $moduleName . '/' . $moduleName . '.php'; + + if (!file_exists($modulePath)) { + return null; + } + + require_once $modulePath; + $className = ucfirst($moduleName); + + if (class_exists($className)) { + return new $className(); + } + + return null; + } + + /** + * Modul-Konfiguration abrufen + */ + private function getModuleConfiguration($moduleName) + { + try { + $conn = \Doctrine\DBAL\DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $stmt = $conn->prepare(' + SELECT config_key, config_value + FROM ws_module_config + WHERE module_name = ? + '); + $stmt->execute([$moduleName]); + + $config = []; + while ($row = $stmt->fetchAssociative()) { + $config[$row['config_key']] = $row['config_value']; + } + + return $config; + + } catch (\Exception $e) { + error_log('Modul-Konfiguration Fehler: ' . $e->getMessage()); + return []; + } + } + + /** + * Modul-Logs abrufen + */ + public function logs() + { + $moduleName = $_GET['module'] ?? ''; + + try { + $conn = \Doctrine\DBAL\DriverManager::getConnection([ + 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' + ]); + + $sql = 'SELECT * FROM ws_module_log'; + $params = []; + + if (!empty($moduleName)) { + $sql .= ' WHERE module_name = ?'; + $params[] = $moduleName; + } + + $sql .= ' ORDER BY created_at DESC LIMIT 100'; + + $stmt = $conn->prepare($sql); + $stmt->execute($params); + + $logs = $stmt->fetchAllAssociative(); + + $data = [ + 'logs' => $logs, + 'module_name' => $moduleName + ]; + + $this->render('admin/module/logs.html.twig', $data); + + } catch (\Exception $e) { + $this->addError('Log-Abruf Fehler: ' . $e->getMessage()); + $this->render('admin/module/logs.html.twig', ['logs' => [], 'module_name' => $moduleName]); + } + } + + /** + * Hook-Übersicht anzeigen + */ + public function hooks() + { + $hookList = Hook::getHookList(); + $hookStats = Hook::getHookStatistics(); + + $data = [ + 'hook_list' => $hookList, + 'hook_stats' => $hookStats + ]; + + $this->render('admin/module/hooks.html.twig', $data); + } + + /** + * Modul-Upload verarbeiten + */ + public function upload() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /admin/module'); + return; + } + + // CSRF-Schutz + if (!$this->security->validateCSRFToken($_POST['csrf_token'] ?? '')) { + $this->addError('Sicherheitsfehler: Ungültiger Token'); + header('Location: /admin/module'); + return; + } + + if (!isset($_FILES['module_file']) || $_FILES['module_file']['error'] !== UPLOAD_ERR_OK) { + $this->addError('Fehler beim Datei-Upload'); + header('Location: /admin/module'); + return; + } + + $uploadDir = __DIR__ . '/../../modules/'; + $fileName = $_FILES['module_file']['name']; + $filePath = $_FILES['module_file']['tmp_name']; + + // ZIP-Datei extrahieren + $zip = new \ZipArchive(); + if ($zip->open($filePath) === TRUE) { + $moduleName = pathinfo($fileName, PATHINFO_FILENAME); + $extractPath = $uploadDir . $moduleName . '/'; + + // Verzeichnis erstellen + if (!is_dir($extractPath)) { + mkdir($extractPath, 0755, true); + } + + // Dateien extrahieren + $zip->extractTo($extractPath); + $zip->close(); + + $this->addSuccess("Modul '{$moduleName}' erfolgreich hochgeladen"); + } else { + $this->addError('Fehler beim Extrahieren der ZIP-Datei'); + } + + header('Location: /admin/module'); + } + + /** + * Template rendern + */ + private function render($template, $data = []) + { + // CSRF-Token generieren + $data['csrf_token'] = $this->security->generateCSRFToken(); + + // Flash-Messages + $data['success_messages'] = $_SESSION['success_messages'] ?? []; + $data['error_messages'] = $_SESSION['error_messages'] ?? []; + + // Session-Messages löschen + unset($_SESSION['success_messages'], $_SESSION['error_messages']); + + // Template laden + $templatePath = __DIR__ . '/../../templates/' . $template; + + if (file_exists($templatePath)) { + extract($data); + include $templatePath; + } else { + throw new \Exception("Template nicht gefunden: {$template}"); + } + } + + /** + * Erfolgs-Message hinzufügen + */ + private function addSuccess($message) + { + if (!isset($_SESSION['success_messages'])) { + $_SESSION['success_messages'] = []; + } + $_SESSION['success_messages'][] = $message; + } + + /** + * Fehler-Message hinzufügen + */ + private function addError($message) + { + if (!isset($_SESSION['error_messages'])) { + $_SESSION['error_messages'] = []; + } + $_SESSION['error_messages'][] = $message; + } +} \ No newline at end of file diff --git a/docker/mysql/init/02_modules_schema.sql b/docker/mysql/init/02_modules_schema.sql new file mode 100644 index 0000000..312d7d2 --- /dev/null +++ b/docker/mysql/init/02_modules_schema.sql @@ -0,0 +1,148 @@ +-- Module-System Schema +-- Erstellt die Tabellen für PrestaShop-kompatible Module + +-- Module-Tabelle +CREATE TABLE IF NOT EXISTS `ws_module` ( + `id_module` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `display_name` varchar(255) DEFAULT NULL, + `description` text, + `version` varchar(32) DEFAULT '1.0.0', + `author` varchar(255) DEFAULT 'Unknown', + `tab` varchar(64) DEFAULT 'administration', + `need_instance` tinyint(1) DEFAULT 0, + `bootstrap` tinyint(1) DEFAULT 0, + `active` tinyint(1) DEFAULT 1, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id_module`), + UNIQUE KEY `name` (`name`), + KEY `active` (`active`), + KEY `tab` (`tab`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Hook-Module-Tabelle (Verbindung zwischen Hooks und Modulen) +CREATE TABLE IF NOT EXISTS `ws_hook_module` ( + `id_hook_module` int(11) NOT NULL AUTO_INCREMENT, + `hook_name` varchar(64) NOT NULL, + `module_name` varchar(64) NOT NULL, + `position` int(11) DEFAULT 0, + `active` tinyint(1) DEFAULT 1, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id_hook_module`), + UNIQUE KEY `hook_module` (`hook_name`, `module_name`), + KEY `hook_name` (`hook_name`), + KEY `module_name` (`module_name`), + KEY `active` (`active`), + KEY `position` (`position`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Module-Konfiguration-Tabelle +CREATE TABLE IF NOT EXISTS `ws_module_config` ( + `id_module_config` int(11) NOT NULL AUTO_INCREMENT, + `module_name` varchar(64) NOT NULL, + `config_key` varchar(255) NOT NULL, + `config_value` text, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id_module_config`), + UNIQUE KEY `module_config` (`module_name`, `config_key`), + KEY `module_name` (`module_name`), + KEY `config_key` (`config_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Module-Logs-Tabelle +CREATE TABLE IF NOT EXISTS `ws_module_log` ( + `id_module_log` int(11) NOT NULL AUTO_INCREMENT, + `module_name` varchar(64) NOT NULL, + `action` varchar(64) NOT NULL, + `message` text, + `level` enum('info', 'warning', 'error') DEFAULT 'info', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id_module_log`), + KEY `module_name` (`module_name`), + KEY `action` (`action`), + KEY `level` (`level`), + KEY `created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Module-Dependencies-Tabelle +CREATE TABLE IF NOT EXISTS `ws_module_dependency` ( + `id_module_dependency` int(11) NOT NULL AUTO_INCREMENT, + `module_name` varchar(64) NOT NULL, + `dependency_name` varchar(64) NOT NULL, + `dependency_version` varchar(32) DEFAULT NULL, + `required` tinyint(1) DEFAULT 1, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id_module_dependency`), + KEY `module_name` (`module_name`), + KEY `dependency_name` (`dependency_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Module-Override-Tabelle +CREATE TABLE IF NOT EXISTS `ws_module_override` ( + `id_module_override` int(11) NOT NULL AUTO_INCREMENT, + `module_name` varchar(64) NOT NULL, + `override_type` enum('class', 'template', 'controller') NOT NULL, + `original_path` varchar(255) NOT NULL, + `override_path` varchar(255) NOT NULL, + `active` tinyint(1) DEFAULT 1, + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id_module_override`), + KEY `module_name` (`module_name`), + KEY `override_type` (`override_type`), + KEY `active` (`active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Standard-Hooks einfügen +INSERT IGNORE INTO `ws_hook_module` (`hook_name`, `module_name`, `position`) VALUES +-- Display Hooks +('displayHeader', 'system', 0), +('displayTop', 'system', 0), +('displayNav', 'system', 0), +('displayFooter', 'system', 0), +('displayHome', 'system', 0), + +-- Product Hooks +('displayProductAdditionalInfo', 'system', 0), +('displayProductListReviews', 'system', 0), +('displayProductButtons', 'system', 0), + +-- Cart Hooks +('displayShoppingCart', 'system', 0), +('actionCartUpdateQuantityBefore', 'system', 0), +('actionCartUpdateQuantityAfter', 'system', 0), + +-- Order Hooks +('displayOrderConfirmation', 'system', 0), +('actionValidateOrder', 'system', 0), + +-- Customer Hooks +('displayCustomerAccount', 'system', 0), +('actionCustomerAccountAdd', 'system', 0), + +-- Admin Hooks +('displayAdminOrder', 'system', 0), +('displayAdminProducts', 'system', 0), + +-- Payment Hooks +('displayPayment', 'system', 0), +('actionPaymentConfirmation', 'system', 0), + +-- Search Hooks +('displaySearch', 'system', 0), +('actionSearch', 'system', 0), + +-- Newsletter Hooks +('displayNewsletterRegistration', 'system', 0), +('actionNewsletterRegistrationAfter', 'system', 0), + +-- Security Hooks +('actionAuthentication', 'system', 0), +('actionCustomerLogoutAfter', 'system', 0); + +-- System-Modul eintragen +INSERT IGNORE INTO `ws_module` (`name`, `display_name`, `description`, `version`, `author`, `tab`, `need_instance`, `bootstrap`, `active`) VALUES +('system', 'System', 'Kern-System-Modul', '1.0.0', 'Webshop System', 'administration', 0, 0, 1); \ No newline at end of file