diff --git a/aurora-livecam/onboarding/branding.php b/aurora-livecam/onboarding/branding.php new file mode 100644 index 0000000..830d9ab --- /dev/null +++ b/aurora-livecam/onboarding/branding.php @@ -0,0 +1,253 @@ +isLoggedIn()) { + header('Location: /onboarding/register.php'); + exit; +} + +$user = $auth->getUser(); +$tenantId = $user['tenant_id'] ?? 0; + +$error = ''; +$branding = [ + 'site_name' => $user['tenant_name'] ?? '', + 'tagline' => '', + 'primary_color' => '#667eea', + 'secondary_color' => '#764ba2', +]; + +// Formular verarbeiten +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $branding = [ + 'site_name' => trim($_POST['site_name'] ?? ''), + 'site_name_full' => trim($_POST['site_name'] ?? ''), + 'tagline' => trim($_POST['tagline'] ?? ''), + 'primary_color' => $_POST['primary_color'] ?? '#667eea', + 'secondary_color' => $_POST['secondary_color'] ?? '#764ba2', + ]; + + try { + $onboarding = new OnboardingManager(); + $result = $onboarding->saveBranding($tenantId, $branding); + + if ($result['success']) { + header('Location: /onboarding/complete.php'); + exit; + } else { + $error = $result['error'] ?? 'Fehler beim Speichern'; + } + } catch (\Exception $e) { + $error = 'Fehler: ' . $e->getMessage(); + } +} + +// Skip +if (isset($_GET['skip'])) { + header('Location: /onboarding/complete.php'); + exit; +} +?> + + + + + + Branding - Aurora Livecam + + + + +
+
+
+
+
+
+
+
+ +
+

🎨 Branding

+

Personalisieren Sie Ihre Livecam

+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ + +
+
+

+

+
+
+ Live-Vorschau +
+
+ + +
+ + +
+
+ + + + diff --git a/aurora-livecam/onboarding/complete.php b/aurora-livecam/onboarding/complete.php new file mode 100644 index 0000000..3696d4d --- /dev/null +++ b/aurora-livecam/onboarding/complete.php @@ -0,0 +1,237 @@ +isLoggedIn()) { + header('Location: /onboarding/register.php'); + exit; +} + +$user = $auth->getUser(); +$tenantId = $user['tenant_id'] ?? 0; + +// Onboarding abschliessen +try { + $onboarding = new OnboardingManager(); + $onboarding->complete($tenantId); +} catch (\Exception $e) { + // Ignorieren wenn DB nicht verfügbar +} + +// Tenant-Info laden +$tenantSlug = 'demo'; +$subdomain = ''; + +try { + $db = Database::getInstance(); + $tenant = $db->fetchOne("SELECT slug FROM tenants WHERE id = ?", [$tenantId]); + if ($tenant) { + $tenantSlug = $tenant['slug']; + $subdomain = $tenantSlug . '.aurora-livecam.com'; + } +} catch (\Exception $e) { + // Fallback +} +?> + + + + + + Fertig! - Aurora Livecam + + + + +
+ +
+
+
🎉
+

Herzlichen Glückwunsch!

+

Ihre Livecam ist jetzt eingerichtet und bereit.

+ + +
+ +
https://
+
+ + + + +
+

Nächste Schritte

+
    +
  • Stream-URL im Dashboard anpassen (falls noch nicht geschehen)
  • +
  • Logo und Farben im Branding-Bereich hochladen
  • +
  • Wetter-Widget konfigurieren
  • +
  • Eigene Domain verbinden (optional)
  • + isBillingEnabled()): ?> +
  • Abo auswählen für mehr Funktionen
  • + +
+
+
+
+ + + + diff --git a/aurora-livecam/onboarding/register.php b/aurora-livecam/onboarding/register.php new file mode 100644 index 0000000..3649dae --- /dev/null +++ b/aurora-livecam/onboarding/register.php @@ -0,0 +1,265 @@ +isSelfRegistrationEnabled()) { + header('Location: /'); + exit; +} + +$auth = new AuthManager(); + +// Bereits eingeloggt? +if ($auth->isLoggedIn()) { + header('Location: /dashboard/'); + exit; +} + +$errors = []; +$formData = []; +$success = false; + +// Formular verarbeiten +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $formData = [ + 'name' => trim($_POST['name'] ?? ''), + 'company_name' => trim($_POST['company_name'] ?? ''), + 'email' => trim($_POST['email'] ?? ''), + 'password' => $_POST['password'] ?? '', + 'password_confirm' => $_POST['password_confirm'] ?? '', + 'stream_url' => trim($_POST['stream_url'] ?? ''), + 'accept_terms' => isset($_POST['accept_terms']), + ]; + + try { + $onboarding = new OnboardingManager(); + $result = $onboarding->register($formData); + + if ($result['success']) { + // Session starten und User einloggen + $auth->login($formData['email'], $formData['password']); + + // Zur nächsten Seite weiterleiten + if ($onboarding->requiresEmailVerification()) { + // Token für Demo-Zwecke in Session speichern + $_SESSION['verification_token'] = $result['verification_token']; + header('Location: /onboarding/verify.php'); + } else { + header('Location: /onboarding/stream.php'); + } + exit; + } else { + $errors = $result['errors']; + } + } catch (\Exception $e) { + $errors['general'] = 'Registrierung fehlgeschlagen: ' . $e->getMessage(); + } +} + +$trialDays = $settingsManager->getTrialDays(); +?> + + + + + + Registrierung - Aurora Livecam + + + + +
+
+
+

Jetzt starten

+

Erstellen Sie Ihre eigene Live-Webcam

+ Tage kostenlos testen +
+ + +
+ + +
+
+
+ + + +

+ +
+ +
+ + + +

+ +
+
+ +
+ + + +

+ +
+ +
+
+ + + +

+ +
+ +
+ + + +

+ +
+
+ +
Optional
+ +
+ + +

Sie können die Stream-URL auch später im Dashboard hinzufügen

+ +

+ +
+ +
+ + +

+ +
+ + +
+ +

+ Bereits registriert? + Anmelden +

+
+
+ + diff --git a/aurora-livecam/onboarding/stream.php b/aurora-livecam/onboarding/stream.php new file mode 100644 index 0000000..0a4899a --- /dev/null +++ b/aurora-livecam/onboarding/stream.php @@ -0,0 +1,265 @@ +isLoggedIn()) { + header('Location: /onboarding/register.php'); + exit; +} + +$user = $auth->getUser(); +$tenantId = $user['tenant_id'] ?? 0; + +$error = ''; +$streamUrl = ''; +$streamType = 'hls'; +$validationResult = null; + +// Formular verarbeiten +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $streamUrl = trim($_POST['stream_url'] ?? ''); + $streamType = $_POST['stream_type'] ?? 'hls'; + + if (empty($streamUrl)) { + $error = 'Bitte geben Sie eine Stream-URL ein'; + } else { + try { + // Stream validieren + $validator = new StreamValidator(); + $validationResult = $validator->validate($streamUrl); + + if ($validationResult['valid']) { + // Speichern + $onboarding = new OnboardingManager(); + $result = $onboarding->saveStream($tenantId, $streamUrl, $streamType); + + if ($result['success']) { + header('Location: /onboarding/branding.php'); + exit; + } else { + $error = $result['error']; + } + } else { + $error = $validationResult['error'] ?? 'Stream-URL konnte nicht validiert werden'; + } + } catch (\Exception $e) { + $error = 'Fehler: ' . $e->getMessage(); + } + } +} + +// Skip erlauben +if (isset($_GET['skip'])) { + header('Location: /onboarding/branding.php'); + exit; +} +?> + + + + + + Stream einrichten - Aurora Livecam + + + + +
+
+
+
+
+
+
+
+ +
+

📹 Stream einrichten

+

Verbinden Sie Ihre Webcam oder Ihren Stream

+
+ + +
+ + +
+
+ +
+ + + + +
+
+ +
+ + +

Die vollständige URL zu Ihrem Stream

+
+ + +
+ + +
+ + Erkannter Typ: + +
+ +
+ + + +
+ + +
+
+ + + + diff --git a/aurora-livecam/onboarding/verify.php b/aurora-livecam/onboarding/verify.php new file mode 100644 index 0000000..080be0e --- /dev/null +++ b/aurora-livecam/onboarding/verify.php @@ -0,0 +1,214 @@ +isLoggedIn()) { + header('Location: /onboarding/register.php'); + exit; +} + +$user = $auth->getUser(); +$message = ''; +$error = ''; +$verified = false; + +// Token aus URL verarbeiten +if (isset($_GET['token'])) { + try { + $onboarding = new OnboardingManager(); + $result = $onboarding->verifyEmail($_GET['token']); + + if ($result['success']) { + $verified = true; + $message = 'E-Mail erfolgreich verifiziert!'; + } else { + $error = $result['error']; + } + } catch (\Exception $e) { + $error = 'Verifikation fehlgeschlagen'; + } +} + +// E-Mail erneut senden +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['resend'])) { + try { + $onboarding = new OnboardingManager(); + $result = $onboarding->resendVerification($user['id']); + + if ($result['success']) { + $_SESSION['verification_token'] = $result['token']; + $message = 'Verifikations-E-Mail wurde erneut gesendet!'; + } else { + $error = $result['error']; + } + } catch (\Exception $e) { + $error = 'Fehler beim Senden'; + } +} + +// Demo: Token anzeigen (in Produktion würde eine E-Mail gesendet) +$demoToken = $_SESSION['verification_token'] ?? null; +?> + + + + + + E-Mail verifizieren - Aurora Livecam + + + + +
+
+
+
+
+
+
+
+ + +
+

E-Mail verifiziert!

+

Ihre E-Mail-Adresse wurde erfolgreich bestätigt.

+ + Weiter zur Stream-Konfiguration + + +
📧
+

E-Mail bestätigen

+

+ Wir haben eine Bestätigungs-E-Mail an
+
+ gesendet. +

+ + +
+ + + +
+ + + +
+

⚠️ Demo-Modus

+

In der Produktion würde eine E-Mail gesendet. Für Demo-Zwecke:

+ + Klicken Sie hier um zu verifizieren + +
+ + +

+ Keine E-Mail erhalten? +

+ +
+ +
+ + +

+ + Abmelden + +

+
+
+ + diff --git a/aurora-livecam/src/Onboarding/OnboardingManager.php b/aurora-livecam/src/Onboarding/OnboardingManager.php new file mode 100644 index 0000000..98bd38c --- /dev/null +++ b/aurora-livecam/src/Onboarding/OnboardingManager.php @@ -0,0 +1,366 @@ +db = $db ?? Database::getInstance(); + $this->tenantManager = new TenantManager($this->db); + $this->streamValidator = new StreamValidator(); + } + + /** + * Startet den Onboarding-Prozess (Registrierung) + */ + public function register(array $data): array + { + $errors = $this->validateRegistration($data); + + if (!empty($errors)) { + return ['success' => false, 'errors' => $errors]; + } + + try { + $this->db->beginTransaction(); + + // Tenant erstellen + $tenantId = $this->tenantManager->create([ + 'name' => $data['company_name'] ?? $data['name'], + 'email' => $data['email'], + 'subdomain' => $this->generateSubdomain($data['company_name'] ?? $data['name']), + 'stream_url' => $data['stream_url'] ?? '', + 'stream_type' => $data['stream_type'] ?? 'hls', + ]); + + // Admin-User für den Tenant erstellen + $auth = new AuthManager($this->db); + $userId = $auth->register([ + 'tenant_id' => $tenantId, + 'email' => $data['email'], + 'password' => $data['password'], + 'name' => $data['name'], + 'role' => 'tenant_admin', + ]); + + // Verification-Token generieren + $verificationToken = $this->generateVerificationToken($userId); + + $this->db->commit(); + + return [ + 'success' => true, + 'tenant_id' => $tenantId, + 'user_id' => $userId, + 'verification_token' => $verificationToken, + 'next_step' => self::STEP_VERIFY_EMAIL, + ]; + + } catch (\Exception $e) { + $this->db->rollback(); + return ['success' => false, 'errors' => ['general' => $e->getMessage()]]; + } + } + + /** + * Validiert Registrierungsdaten + */ + private function validateRegistration(array $data): array + { + $errors = []; + + // Name + if (empty($data['name'])) { + $errors['name'] = 'Name ist erforderlich'; + } + + // Company/Site Name + if (empty($data['company_name'])) { + $errors['company_name'] = 'Firmen-/Site-Name ist erforderlich'; + } + + // Email + if (empty($data['email'])) { + $errors['email'] = 'E-Mail ist erforderlich'; + } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { + $errors['email'] = 'Ungültige E-Mail-Adresse'; + } else { + // Prüfe ob Email bereits existiert + $existing = $this->db->fetchOne("SELECT id FROM users WHERE email = ?", [strtolower($data['email'])]); + if ($existing) { + $errors['email'] = 'Diese E-Mail-Adresse ist bereits registriert'; + } + } + + // Password + if (empty($data['password'])) { + $errors['password'] = 'Passwort ist erforderlich'; + } elseif (strlen($data['password']) < 8) { + $errors['password'] = 'Passwort muss mindestens 8 Zeichen lang sein'; + } + + // Password Confirmation + if (($data['password'] ?? '') !== ($data['password_confirm'] ?? '')) { + $errors['password_confirm'] = 'Passwörter stimmen nicht überein'; + } + + // Stream URL (optional, aber wenn angegeben, validieren) + if (!empty($data['stream_url'])) { + $validation = $this->streamValidator->validate($data['stream_url']); + if (!$validation['valid']) { + $errors['stream_url'] = $validation['error'] ?? 'Stream-URL ungültig'; + } + } + + // Terms + if (empty($data['accept_terms'])) { + $errors['accept_terms'] = 'Sie müssen die AGB akzeptieren'; + } + + return $errors; + } + + /** + * Generiert eine Subdomain aus dem Firmennamen + */ + private function generateSubdomain(string $name): string + { + // Umlaute ersetzen + $replacements = ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss']; + $slug = str_replace(array_keys($replacements), array_values($replacements), strtolower($name)); + + // Nur alphanumerische Zeichen und Bindestriche + $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); + $slug = trim($slug, '-'); + + // Max 30 Zeichen + $slug = substr($slug, 0, 30); + + // Eindeutigkeit prüfen + $baseSlug = $slug; + $counter = 1; + while (!$this->tenantManager->isDomainAvailable($slug . '.aurora-livecam.com')) { + $slug = $baseSlug . '-' . $counter; + $counter++; + } + + return $slug; + } + + /** + * Generiert einen E-Mail-Verification-Token + */ + private function generateVerificationToken(int $userId): string + { + $token = bin2hex(random_bytes(32)); + + // Token in einer separaten Tabelle speichern (oder im User) + // Vereinfacht: Wir nutzen remember_token temporär + $this->db->update('users', ['remember_token' => hash('sha256', $token)], 'id = ?', [$userId]); + + return $token; + } + + /** + * Verifiziert E-Mail-Adresse + */ + public function verifyEmail(string $token): array + { + $hashedToken = hash('sha256', $token); + + $user = $this->db->fetchOne( + "SELECT id, tenant_id FROM users WHERE remember_token = ? AND email_verified_at IS NULL", + [$hashedToken] + ); + + if (!$user) { + return ['success' => false, 'error' => 'Ungültiger oder abgelaufener Token']; + } + + $this->db->update('users', [ + 'email_verified_at' => date('Y-m-d H:i:s'), + 'remember_token' => null, + ], 'id = ?', [$user['id']]); + + // Onboarding-Status aktualisieren + $this->updateOnboardingStep($user['tenant_id'], self::STEP_STREAM); + + return [ + 'success' => true, + 'user_id' => $user['id'], + 'tenant_id' => $user['tenant_id'], + 'next_step' => self::STEP_STREAM, + ]; + } + + /** + * Speichert Stream-Konfiguration + */ + public function saveStream(int $tenantId, string $url, string $type = 'hls'): array + { + // Validieren + $validation = $this->streamValidator->validate($url); + + if (!$validation['valid']) { + return ['success' => false, 'error' => $validation['error']]; + } + + // Speichern + $existing = $this->db->fetchOne( + "SELECT id FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1", + [$tenantId] + ); + + if ($existing) { + $this->db->update('tenant_streams', [ + 'stream_url' => $url, + 'stream_type' => $validation['type'] ?? $type, + 'last_status' => 'online', + 'last_check_at' => date('Y-m-d H:i:s'), + ], 'id = ?', [$existing['id']]); + } else { + $this->db->insert('tenant_streams', [ + 'tenant_id' => $tenantId, + 'stream_url' => $url, + 'stream_type' => $validation['type'] ?? $type, + 'is_primary' => 1, + 'last_status' => 'online', + 'last_check_at' => date('Y-m-d H:i:s'), + ]); + } + + // Onboarding-Schritt aktualisieren + $this->updateOnboardingStep($tenantId, self::STEP_BRANDING, ['stream_verified' => 1]); + + return [ + 'success' => true, + 'stream_type' => $validation['type'], + 'next_step' => self::STEP_BRANDING, + ]; + } + + /** + * Speichert Basis-Branding + */ + public function saveBranding(int $tenantId, array $branding): array + { + $this->tenantManager->updateBranding($tenantId, $branding); + + // Onboarding-Schritt aktualisieren + $this->updateOnboardingStep($tenantId, self::STEP_COMPLETE, ['branding_configured' => 1]); + + return [ + 'success' => true, + 'next_step' => self::STEP_COMPLETE, + ]; + } + + /** + * Schliesst das Onboarding ab + */ + public function complete(int $tenantId): array + { + $this->db->update('tenant_onboarding', [ + 'current_step' => self::STEP_COMPLETE, + 'completed_at' => date('Y-m-d H:i:s'), + ], 'tenant_id = ?', [$tenantId]); + + // Tenant aktivieren + $this->tenantManager->activate($tenantId); + + return ['success' => true, 'completed' => true]; + } + + /** + * Aktualisiert den Onboarding-Schritt + */ + private function updateOnboardingStep(int $tenantId, int $step, array $extra = []): void + { + $data = array_merge(['current_step' => $step], $extra); + $this->db->update('tenant_onboarding', $data, 'tenant_id = ?', [$tenantId]); + } + + /** + * Gibt den aktuellen Onboarding-Status zurück + */ + public function getStatus(int $tenantId): array + { + $onboarding = $this->db->fetchOne( + "SELECT * FROM tenant_onboarding WHERE tenant_id = ?", + [$tenantId] + ); + + if (!$onboarding) { + return [ + 'current_step' => self::STEP_REGISTER, + 'completed' => false, + ]; + } + + return [ + 'current_step' => (int)$onboarding['current_step'], + 'stream_verified' => (bool)$onboarding['stream_verified'], + 'branding_configured' => (bool)$onboarding['branding_configured'], + 'payment_configured' => (bool)$onboarding['payment_configured'], + 'completed' => $onboarding['completed_at'] !== null, + 'completed_at' => $onboarding['completed_at'], + ]; + } + + /** + * Prüft ob E-Mail-Verification erforderlich ist + */ + public function requiresEmailVerification(): bool + { + // Aus Settings laden + $settingsFile = dirname(__DIR__, 2) . '/SettingsManager.php'; + if (file_exists($settingsFile)) { + require_once $settingsFile; + $settings = new \SettingsManager(); + return $settings->get('saas_features.email_verification_required') ?? true; + } + return true; + } + + /** + * Sendet Verification-E-Mail erneut + */ + public function resendVerification(int $userId): array + { + $user = $this->db->fetchOne("SELECT email, email_verified_at FROM users WHERE id = ?", [$userId]); + + if (!$user) { + return ['success' => false, 'error' => 'Benutzer nicht gefunden']; + } + + if ($user['email_verified_at']) { + return ['success' => false, 'error' => 'E-Mail bereits verifiziert']; + } + + $token = $this->generateVerificationToken($userId); + + return [ + 'success' => true, + 'token' => $token, + 'email' => $user['email'], + ]; + } +} diff --git a/aurora-livecam/src/Onboarding/StreamValidator.php b/aurora-livecam/src/Onboarding/StreamValidator.php new file mode 100644 index 0000000..786de30 --- /dev/null +++ b/aurora-livecam/src/Onboarding/StreamValidator.php @@ -0,0 +1,263 @@ + false, + 'type' => null, + 'error' => null, + 'details' => [], + ]; + + // URL-Format prüfen + if (!filter_var($url, FILTER_VALIDATE_URL)) { + $result['error'] = 'Ungültiges URL-Format'; + return $result; + } + + // Stream-Typ erkennen + $type = $this->detectStreamType($url); + $result['type'] = $type; + $result['details']['detected_type'] = $type; + + // Je nach Typ validieren + switch ($type) { + case 'hls': + return $this->validateHls($url, $result); + case 'rtmp': + return $this->validateRtmp($url, $result); + case 'iframe': + return $this->validateIframe($url, $result); + default: + // Generische HTTP-Prüfung + return $this->validateHttp($url, $result); + } + } + + /** + * Erkennt den Stream-Typ anhand der URL + */ + public function detectStreamType(string $url): string + { + $url = strtolower($url); + + if (str_contains($url, '.m3u8')) { + return 'hls'; + } + + if (str_starts_with($url, 'rtmp://') || str_starts_with($url, 'rtmps://')) { + return 'rtmp'; + } + + if (str_contains($url, 'youtube.com') || str_contains($url, 'youtu.be') || + str_contains($url, 'vimeo.com') || str_contains($url, 'twitch.tv')) { + return 'iframe'; + } + + if (str_contains($url, '.mp4') || str_contains($url, '.webm')) { + return 'video'; + } + + return 'unknown'; + } + + /** + * Validiert HLS-Stream + */ + private function validateHls(string $url, array $result): array + { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_HTTPHEADER => [ + 'User-Agent: Mozilla/5.0 (compatible; StreamValidator/1.0)' + ], + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $error = curl_error($ch); + curl_close($ch); + + $result['details']['http_code'] = $httpCode; + $result['details']['content_type'] = $contentType; + + if ($error) { + $result['error'] = 'Verbindungsfehler: ' . $error; + return $result; + } + + if ($httpCode !== 200) { + $result['error'] = "HTTP-Fehler: $httpCode"; + return $result; + } + + // Prüfe ob es ein gültiges M3U8 ist + if (!str_contains($response, '#EXTM3U')) { + $result['error'] = 'Keine gültige HLS-Playlist gefunden'; + return $result; + } + + $result['valid'] = true; + $result['details']['is_master'] = str_contains($response, '#EXT-X-STREAM-INF'); + $result['details']['segments'] = substr_count($response, '#EXTINF'); + + return $result; + } + + /** + * Validiert RTMP-Stream (nur Format-Check) + */ + private function validateRtmp(string $url, array $result): array + { + // RTMP kann nicht einfach per HTTP geprüft werden + // Wir prüfen nur das Format + + $parsed = parse_url($url); + + if (!isset($parsed['host']) || empty($parsed['host'])) { + $result['error'] = 'RTMP-URL enthält keinen gültigen Host'; + return $result; + } + + // DNS-Check + $ip = gethostbyname($parsed['host']); + if ($ip === $parsed['host']) { + $result['error'] = 'RTMP-Host nicht erreichbar (DNS-Fehler)'; + return $result; + } + + $result['valid'] = true; + $result['details']['host'] = $parsed['host']; + $result['details']['note'] = 'RTMP-Streams können erst zur Laufzeit vollständig validiert werden'; + + return $result; + } + + /** + * Validiert iFrame-Embed URL + */ + private function validateIframe(string $url, array $result): array + { + // Bekannte Embed-Plattformen + $embedPatterns = [ + 'youtube' => '/(?:youtube\.com\/(?:embed|watch)|youtu\.be)/i', + 'vimeo' => '/vimeo\.com/i', + 'twitch' => '/(?:twitch\.tv|player\.twitch\.tv)/i', + 'dailymotion' => '/dailymotion\.com/i', + ]; + + $platform = 'unknown'; + foreach ($embedPatterns as $name => $pattern) { + if (preg_match($pattern, $url)) { + $platform = $name; + break; + } + } + + $result['details']['platform'] = $platform; + + // HTTP-Check + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_NOBODY => true, // HEAD request + CURLOPT_SSL_VERIFYPEER => false, + ]); + + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $result['details']['http_code'] = $httpCode; + + if ($httpCode >= 200 && $httpCode < 400) { + $result['valid'] = true; + } else { + $result['error'] = "URL nicht erreichbar (HTTP $httpCode)"; + } + + return $result; + } + + /** + * Generische HTTP-Validierung + */ + private function validateHttp(string $url, array $result): array + { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_NOBODY => true, + CURLOPT_SSL_VERIFYPEER => false, + ]); + + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $error = curl_error($ch); + curl_close($ch); + + $result['details']['http_code'] = $httpCode; + $result['details']['content_type'] = $contentType; + + if ($error) { + $result['error'] = 'Verbindungsfehler: ' . $error; + return $result; + } + + if ($httpCode >= 200 && $httpCode < 400) { + $result['valid'] = true; + } else { + $result['error'] = "URL nicht erreichbar (HTTP $httpCode)"; + } + + return $result; + } + + /** + * Schnelle Erreichbarkeitsprüfung + */ + public function isReachable(string $url): bool + { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_NOBODY => true, + CURLOPT_SSL_VERIFYPEER => false, + ]); + + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return $httpCode >= 200 && $httpCode < 400; + } +}