diff --git a/backend/src/Api/ProjectsController.php b/backend/src/Api/ProjectsController.php index 6f15c13..199a86a 100644 --- a/backend/src/Api/ProjectsController.php +++ b/backend/src/Api/ProjectsController.php @@ -1,236 +1,41 @@ false, - 'error' => [ - 'code' => 'FORBIDDEN', - 'message' => "Zugriff auf Client '{$clientDir}' ist für diesen Administrator nicht erlaubt." - ] - ]); - exit; - } - return $user; - } - if ($user['role'] === 'client' && $user['dir'] !== $clientDir) { - http_response_code(403); - header('Content-Type: application/json'); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'FORBIDDEN', - 'message' => 'Nicht berechtigt für diesen Bereich.' - ] - ]); - exit; - } - return $user; - } - - // JWT-basierte Authentifizierungsprüfung für alle API-Methoden - private static function requireAuth() { - $headers = getallheaders(); - $authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? null; - if (!$authHeader || !preg_match('/^Bearer\s+(.*)$/i', $authHeader, $matches)) { - http_response_code(401); - header('Content-Type: application/json'); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'UNAUTHORIZED', - 'message' => 'Kein gültiges Auth-Token übergeben.' - ] - ]); - exit; - } - $jwt = $matches[1]; - require_once __DIR__ . '/../Services/AuthService.php'; - $user = AuthService::verifyJWT($jwt); - if (!$user) { - http_response_code(401); - header('Content-Type: application/json'); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'UNAUTHORIZED', - 'message' => 'Token ungültig oder abgelaufen.' - ] - ]); - exit; - } - // Optional: User-Infos für spätere Nutzung zurückgeben - return $user; - } - - // Prüft ob eine Datei ignoriert werden soll - private static function shouldIgnoreFile($filename) { - $ignoredFiles = self::getIgnoredFiles(); - return in_array(strtolower($filename), array_map('strtolower', $ignoredFiles)); - } - + /** + * List projects for a specific client + */ public static function listForClient($clientDir) { - self::requireClientAccess($clientDir); - $base = realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir); - // Lade clients.json, um den Anzeigenamen des Clients zu bekommen - $loginsFile = __DIR__ . '/../../../storage/data/clients.json'; - $clientDisplay = $clientDir; - if (file_exists($loginsFile)) { - $logins = json_decode(file_get_contents($loginsFile), true); - foreach ($logins as $login) { - if (isset($login['dir']) && $login['dir'] === $clientDir) { - $clientDisplay = $login['dir']; - break; - } - } - } - if (!$base || !is_dir($base)) { + AuthorizationService::requireClientAccess($clientDir); + + $result = ProjectService::getProjectsForClient($clientDir); + + if (!$result['success']) { http_response_code(404); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'NOT_FOUND', - 'message' => 'Kundenordner nicht gefunden.' - ] - ]); - return; } - // Lade project_order.json falls vorhanden - $projectOrderFile = $base . '/project_order.json'; - $projectOrder = []; - if (file_exists($projectOrderFile)) { - $orderContent = file_get_contents($projectOrderFile); - $decoded = json_decode($orderContent, true); - if (is_array($decoded)) { - $projectOrder = $decoded; - } - } - - $projects = []; - $foundProjects = []; - - // Sammle alle verfügbaren Projekte - foreach (scandir($base) as $entry) { - if ($entry === '.' || $entry === '..') continue; - $path = $base . '/' . $entry; - if (is_dir($path)) { - $setupDir = $path . '/setup'; - $poster = null; - $logo = null; - if (is_dir($setupDir)) { - foreach (scandir($setupDir) as $file) { - if (!$poster && stripos($file, 'poster') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { - $poster = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($entry) . '/setup/' . rawurlencode($file); - } - if (!$logo && stripos($file, 'logo') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { - $logo = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($entry) . '/setup/' . rawurlencode($file); - } - if ($poster && $logo) break; - } - } - $isHE = (stripos($entry, 'HE_') === 0); - $foundProjects[$entry] = [ - 'name' => $entry, - 'path' => $entry, - 'poster' => $poster, - 'logo' => $logo, - 'isHE' => $isHE, - 'client' => $clientDisplay - ]; - } - } - - // Sortiere nach project_order.json falls vorhanden - if (!empty($projectOrder)) { - // Erst die Projekte in der definierten Reihenfolge - foreach ($projectOrder as $orderedProject) { - if (isset($foundProjects[$orderedProject])) { - $projects[] = $foundProjects[$orderedProject]; - unset($foundProjects[$orderedProject]); - } - } - // Neue Projekte (nicht in order-Liste) kommen an den ANFANG - $remainingProjects = array_values($foundProjects); - usort($remainingProjects, function($a, $b) { - return strcmp($a['name'], $b['name']); - }); - // Neue Projekte VOR die sortierten einfügen - $projects = array_merge($remainingProjects, $projects); - } else { - // Fallback: Alphabetische Sortierung - $projects = array_values($foundProjects); - usort($projects, function($a, $b) { - return strcmp($a['name'], $b['name']); - }); - } - echo json_encode([ - 'success' => true, - 'projects' => $projects - ]); + header('Content-Type: application/json'); + echo json_encode($result); } - - // Speichert die Projekt-Reihenfolge in project_order.json + + /** + * Save project order for a client + */ public static function saveProjectOrder($clientDir) { - self::requireClientAccess($clientDir); + AuthorizationService::requireClientAccess($clientDir); $clientDir = rawurldecode($clientDir); - // JSON-Body lesen + // Read JSON body $input = file_get_contents('php://input'); $data = json_decode($input, true); @@ -246,71 +51,41 @@ class ProjectsController { return; } - $base = realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir); - if (!$base || !is_dir($base)) { - http_response_code(404); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'NOT_FOUND', - 'message' => 'Kundenordner nicht gefunden.' - ] - ]); - return; + $result = ProjectService::saveProjectOrder($clientDir, $data); + + if (!$result['success']) { + http_response_code($result['error']['code'] === 'NOT_FOUND' ? 404 : 500); } - $projectOrderFile = $base . '/project_order.json'; - - // Validiere dass alle Projekte im order Array auch wirklich existieren - $existingProjects = []; - foreach (scandir($base) as $entry) { - if ($entry === '.' || $entry === '..') continue; - if (is_dir($base . '/' . $entry)) { - $existingProjects[] = $entry; - } - } - - foreach ($data['order'] as $projectName) { - if (!in_array($projectName, $existingProjects)) { - http_response_code(400); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'INVALID_PROJECT', - 'message' => "Projekt '$projectName' existiert nicht." - ] - ]); - return; - } - } - - // Speichere die neue Reihenfolge - $result = file_put_contents($projectOrderFile, json_encode($data['order'], JSON_PRETTY_PRINT)); - - if ($result === false) { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'WRITE_ERROR', - 'message' => 'Fehler beim Speichern der project_order.json.' - ] - ]); - return; - } - - echo json_encode([ - 'success' => true, - 'message' => 'Projekt-Reihenfolge gespeichert.' - ]); + header('Content-Type: application/json'); + echo json_encode($result); } - - public static function adsFolders($clientDir, $projectName) { - self::requireClientAccess($clientDir); + + /** + * Get project details (poster, logo, etc.) + */ + public static function projectDetails($clientDir, $projectName) { + AuthorizationService::requireClientAccess($clientDir); $clientDir = rawurldecode($clientDir); $projectName = rawurldecode($projectName); - $adsPath = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; + + $result = ProjectService::getProjectDetails($clientDir, $projectName); + + header('Content-Type: application/json'); + echo json_encode($result); + } + + /** + * Get ads folders for a project + */ + public static function adsFolders($clientDir, $projectName) { + AuthorizationService::requireClientAccess($clientDir); + $clientDir = rawurldecode($clientDir); + $projectName = rawurldecode($projectName); + + $adsPath = __DIR__ . FileSystemService::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; $folders = []; + if (is_dir($adsPath)) { foreach (scandir($adsPath) as $entry) { if ($entry === '.' || $entry === '..') continue; @@ -319,20 +94,27 @@ class ProjectsController { } } } + header('Content-Type: application/json'); echo json_encode(['folders' => $folders]); } - + + /** + * Get subfolders within ads + */ public static function adsSubfolders($clientDir, $projectName, ...$adsFolders) { - self::requireClientAccess($clientDir); + AuthorizationService::requireClientAccess($clientDir); $adsFolders = array_map('rawurldecode', $adsFolders); - $base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; + + $base = __DIR__ . FileSystemService::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; if (!empty($adsFolders)) { $base .= '/' . implode('/', $adsFolders); } + $real = realpath($base); $folders = []; - if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) { + + if (FileSystemService::validatePath($real, $clientDir) && is_dir($real)) { foreach (scandir($real) as $entry) { if ($entry === '.' || $entry === '..') continue; if (is_dir($real . '/' . $entry)) { @@ -340,82 +122,70 @@ class ProjectsController { } } } + header('Content-Type: application/json'); echo json_encode(['folders' => $folders]); } - - // Liefert die Sub-Subfolder eines Subfolders in ads - public static function adsSubSubfolders($clientDir, $projectName, $adsFolder, $subFolder) { - self::requireClientAccess($clientDir); - $adsFolder = rawurldecode($adsFolder); - $subFolder = rawurldecode($subFolder); - $base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads/' . $adsFolder . '/' . $subFolder; - $real = realpath($base); - $folders = []; - if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) { - foreach (scandir($real) as $entry) { - if ($entry === '.' || $entry === '..') continue; - if (is_dir($real . '/' . $entry)) { - $folders[] = $entry; - } - } - } - header('Content-Type: application/json'); - echo json_encode(['folders' => $folders]); - } - - // Liefert die Dateien eines beliebigen Unterordners in ads (beliebige Tiefe) + + /** + * Get files within ads folder (any depth) + */ public static function adsFolderFiles($clientDir, $projectName, ...$adsFolders) { - self::requireClientAccess($clientDir); + AuthorizationService::requireClientAccess($clientDir); $adsFolders = array_map('rawurldecode', $adsFolders); - $base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; + + $base = __DIR__ . FileSystemService::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; if (!empty($adsFolders)) { $base .= '/' . implode('/', $adsFolders); } + $real = realpath($base); $files = []; - if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) { + if (FileSystemService::validatePath($real, $clientDir) && is_dir($real)) { foreach (scandir($real) as $entry) { if ($entry === '.' || $entry === '..') continue; $full = $real . '/' . $entry; + if (is_file($full)) { - // Ignoriere bestimmte Dateien - if (self::shouldIgnoreFile($entry)) { + // Skip ignored files + if (FileSystemService::shouldIgnoreFile($entry)) { continue; } - $ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION)); - $type = 'other'; - if (in_array($ext, ['jpg','jpeg','png','gif','webp','svg'])) $type = 'image'; - elseif (in_array($ext, ['mp4','mov','avi','webm'])) $type = 'video'; - elseif (in_array($ext, ['html','htm'])) $type = 'html'; - $urlParts = array_map('rawurlencode', $adsFolders); + $type = FileSystemService::getFileType($entry); + $url = FileSystemService::createFileUrl($clientDir, $projectName, $adsFolders, $entry); + $files[] = [ 'name' => $entry, 'type' => $type, - 'url' => "/area/$clientDir/$projectName/ads/" . implode('/', $urlParts) . (count($urlParts) ? '/' : '') . rawurlencode($entry) + 'url' => $url ]; } } } + header('Content-Type: application/json'); echo json_encode(['files' => $files]); } - - // Liefert sowohl Ordner als auch Dateien eines beliebigen Unterordners in ads (beliebige Tiefe) + + /** + * Get both folders and files within ads folder (any depth) + */ public static function adsFolderContent($clientDir, $projectName, ...$adsFolders) { - self::requireClientAccess($clientDir); + AuthorizationService::requireClientAccess($clientDir); $adsFolders = array_map('rawurldecode', $adsFolders); - $base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; + + $base = __DIR__ . FileSystemService::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; if (!empty($adsFolders)) { $base .= '/' . implode('/', $adsFolders); } + $real = realpath($base); $folders = []; $files = []; - if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) { + if (FileSystemService::validatePath($real, $clientDir) && is_dir($real)) { foreach (scandir($real) as $entry) { if ($entry === '.' || $entry === '..') continue; $full = $real . '/' . $entry; @@ -423,21 +193,18 @@ class ProjectsController { if (is_dir($full)) { $folders[] = $entry; } elseif (is_file($full)) { - // Ignoriere bestimmte Dateien - if (self::shouldIgnoreFile($entry)) { + // Skip ignored files + if (FileSystemService::shouldIgnoreFile($entry)) { continue; } - $ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION)); - $type = 'other'; - if (in_array($ext, ['jpg','jpeg','png','gif','webp','svg'])) $type = 'image'; - elseif (in_array($ext, ['mp4','mov','avi','webm'])) $type = 'video'; - elseif (in_array($ext, ['html','htm'])) $type = 'html'; - $urlParts = array_map('rawurlencode', $adsFolders); + $type = FileSystemService::getFileType($entry); + $url = FileSystemService::createFileUrl($clientDir, $projectName, $adsFolders, $entry); + $files[] = [ 'name' => $entry, 'type' => $type, - 'url' => "/area/$clientDir/$projectName/ads/" . implode('/', $urlParts) . (count($urlParts) ? '/' : '') . rawurlencode($entry) + 'url' => $url ]; } } @@ -449,18 +216,22 @@ class ProjectsController { 'files' => $files ]); } - - // Liefert config.yaml aus einem beliebigen Ordnerpfad + + /** + * Get config.yaml from any folder path + */ public static function getConfig($clientDir, $projectName, ...$adsFolders) { - self::requireClientAccess($clientDir); + AuthorizationService::requireClientAccess($clientDir); $adsFolders = array_map('rawurldecode', $adsFolders); - $base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; + + $base = __DIR__ . FileSystemService::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads'; if (!empty($adsFolders)) { $base .= '/' . implode('/', $adsFolders); } + $real = realpath($base); - if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) { + if (FileSystemService::validatePath($real, $clientDir) && is_dir($real)) { $configFile = $real . '/config.yaml'; if (file_exists($configFile)) { header('Content-Type: text/plain; charset=utf-8'); @@ -479,272 +250,47 @@ class ProjectsController { ] ]); } - - // Liefert die Details eines Projekts (inkl. Logo, Poster etc.) - public static function projectDetails($clientDir, $projectName) { - self::requireClientAccess($clientDir); - $clientDir = rawurldecode($clientDir); - $projectName = rawurldecode($projectName); - - $setupDir = __DIR__ . self::getAreaPath() . "/$clientDir/$projectName/setup"; - $poster = null; - $logo = null; - - if (is_dir($setupDir)) { - foreach (scandir($setupDir) as $file) { - if ($file === '.' || $file === '..') continue; - if (stripos($file, 'poster') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { - $poster = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($projectName) . '/setup/' . rawurlencode($file); - } - if (!$logo && stripos($file, 'logo') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { - $logo = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($projectName) . '/setup/' . rawurlencode($file); - } - if ($poster && $logo) break; - } - } - - header('Content-Type: application/json'); - echo json_encode([ - 'success' => true, - 'data' => [ - 'poster' => $poster, - 'logo' => $logo, - ] - ]); - } - - // Rekursive Übersicht aller Ads als strukturierter Baum + + /** + * Get recursive ads overview as structured tree with metadata + */ public static function adsOverview($clientDir, $projectName) { - self::requireClientAccess($clientDir); + AuthorizationService::requireClientAccess($clientDir); $clientDir = rawurldecode($clientDir); $projectName = rawurldecode($projectName); - $adsRoot = __DIR__ . self::getAreaPath() . "/$clientDir/$projectName/ads"; - $baseUrl = $_ENV['BACKEND_URL'] ?? (isset($_SERVER['HTTP_HOST']) ? (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] : ''); - - // Hilfsfunktion für schöne Tab-Titel - function beautifyTabName($name) { - // Ersetze _ und - durch Leerzeichen, dann jedes Wort groß - $name = str_replace(['_', '-'], ' ', $name); - $name = mb_strtolower($name, 'UTF-8'); - $name = preg_replace_callback('/\b\w/u', function($m) { return mb_strtoupper($m[0], 'UTF-8'); }, $name); - return $name; - } - - function scanAdsFolder($absPath, $relPath, $baseUrl, $clientDir, $projectName) { - $result = []; - if (!is_dir($absPath)) return $result; - $entries = array_diff(scandir($absPath), ['.', '..']); - foreach ($entries as $entry) { - $entryAbs = "$absPath/$entry"; - $entryRel = $relPath ? "$relPath/$entry" : $entry; - if (is_dir($entryAbs)) { - $children = scanAdsFolder($entryAbs, $entryRel, $baseUrl, $clientDir, $projectName); - $depth = substr_count($entryRel, '/'); - $type = match($depth) { - 0 => 'category', - 1 => 'subcategory', - 2 => 'ad', - default => 'folder' - }; - $node = [ - 'name' => $entry, - 'title' => beautifyTabName($entry), - 'type' => $type, - ]; - if ($type === 'ad') { - // Prüfe, ob ein Unterordner mit 'html' im Namen existiert - $htmlFolder = null; - foreach (scandir($entryAbs) as $sub) { - if ($sub === '.' || $sub === '..') continue; - if (is_dir("$entryAbs/$sub") && stripos($sub, 'html') !== false) { - $htmlFolder = $sub; - break; - } - } - // Dateien sammeln, aber config.yaml (und config.yml) ignorieren - $node['files'] = array_values(array_filter($children, function($child) { - if (in_array($child['type'], ['category','subcategory','ad','folder'])) return false; - if (isset($child['name']) && preg_match('/^config\.ya?ml$/i', $child['name'])) return false; - return true; - })); - // Spezialfall: HTML-Ordner gefunden - if ($htmlFolder) { - $htmlAbs = "$entryAbs/$htmlFolder"; - $htmlIndex = "$htmlAbs/index.html"; - // Suche config.yaml eine Ebene über dem HTML-Ordner (im Ad-Ordner) - $configYaml = "$entryAbs/config.yaml"; - $meta = []; - if (is_file($configYaml)) { - $yaml = file_get_contents($configYaml); - $width = 0; - $height = 0; - $lines = preg_split('/\r?\n/', $yaml); - foreach ($lines as $line) { - if ($width === 0 && preg_match('/width\s*:\s*["\']?([\d,.]+)\s*([a-zA-Z%]*)["\']?/i', $line, $wMatch)) { - $val = str_replace([',', ' '], '', $wMatch[1]); - $width = (int)floatval($val); - } - if ($height === 0 && preg_match('/height\s*:\s*["\']?([\d,.]+)\s*([a-zA-Z%]*)["\']?/i', $line, $hMatch)) { - $val = str_replace([',', ' '], '', $hMatch[1]); - $height = (int)floatval($val); - } - } - $meta['width'] = $width; - $meta['height'] = $height; - } else { - // Fallback: Versuche Dimensionen aus dem Ad-Ordnernamen zu extrahieren (z.B. 800x250) - $adFolderName = basename($entryAbs); - if (preg_match('/(\d{2,5})[xX](\d{2,5})/', $adFolderName, $matches)) { - $meta['width'] = (int)$matches[1]; - $meta['height'] = (int)$matches[2]; - // file_put_contents(__DIR__.'/debug.log', "Fallback: Dimensionen aus Ordnername erkannt width={$meta['width']} height={$meta['height']} ($adFolderName)\n", FILE_APPEND); - } - } - if (is_file($htmlIndex)) { - $url = "$baseUrl/area/" . rawurlencode($clientDir) . "/" . rawurlencode($projectName) . "/ads/" . ($relPath ? str_replace('%2F', '/', rawurlencode($relPath)) . '/' : '') . rawurlencode($entry) . "/" . rawurlencode($htmlFolder) . "/index.html"; - $fileMeta = [ - 'name' => 'index.html', - 'type' => 'html', - 'url' => $url, - 'size' => is_file($htmlIndex) ? filesize($htmlIndex) : null, - 'width' => (isset($meta['width'])) ? $meta['width'] : 123, - 'height' => (isset($meta['height'])) ? $meta['height'] : 456 - ]; - // Füge index.html nur hinzu, wenn sie nicht schon in files ist - $already = false; - foreach ($node['files'] as $f) { - if ($f['name'] === 'index.html') { $already = true; break; } - } - if (!$already) { - $node['files'][] = $fileMeta; - } - } - } - } else { - $node['children'] = array_filter($children, fn($c) => in_array($c['type'], ['category','subcategory','ad','folder'])); - } - $result[] = $node; - } else { - // Datei - $ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION)); - $type = match(true) { - in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp']) => 'image', - in_array($ext, ['mp4', 'mov', 'webm']) => 'video', - $ext === 'html' => 'html', - default => 'file' - }; - $url = "$baseUrl/area/" . rawurlencode($clientDir) . "/" . rawurlencode($projectName) . "/ads/" . ($relPath ? str_replace('%2F', '/', rawurlencode($relPath)) . '/' : '') . rawurlencode($entry); - - $meta = []; - $fileAbs = $entryAbs; - // Dateigröße - $meta['size'] = is_file($fileAbs) ? filesize($fileAbs) : null; - - // Dimensionen für Bilder - if ($type === 'image') { - $imgInfo = @getimagesize($fileAbs); - if ($imgInfo) { - $meta['width'] = $imgInfo[0]; - $meta['height'] = $imgInfo[1]; - } - } - // Dimensionen und Dauer für Videos (nur wenn ffprobe verfügbar) - if ($type === 'video' && is_file($fileAbs)) { - $ffprobePath = shell_exec('which ffprobe'); - $ffprobeUsed = false; - if ($ffprobePath) { - $ffprobe = trim($ffprobePath); - if ($ffprobe !== '') { - $cmd = escapeshellcmd($ffprobe) . ' -v error -select_streams v:0 -show_entries stream=width,height,duration -of default=noprint_wrappers=1:nokey=1 ' . escapeshellarg($fileAbs); - $out = shell_exec($cmd); - if ($out) { - $lines = explode("\n", trim($out)); - if (count($lines) >= 3) { - $meta['width'] = (int)$lines[0]; - $meta['height'] = (int)$lines[1]; - $meta['duration'] = (float)$lines[2]; - $ffprobeUsed = true; - } - } - } - } - // Fallback auf getID3, falls ffprobe nicht verfügbar oder fehlgeschlagen - if (!$ffprobeUsed) { - try { - $getID3 = new \getID3(); - $info = $getID3->analyze($fileAbs); - if (!empty($info['video']['resolution_x']) && !empty($info['video']['resolution_y'])) { - $meta['width'] = (int)$info['video']['resolution_x']; - $meta['height'] = (int)$info['video']['resolution_y']; - } - if (!empty($info['playtime_seconds'])) { - $meta['duration'] = (float)$info['playtime_seconds']; - } - } catch (\Exception $e) { - // Fehler ignorieren, Metadaten bleiben leer - } - } - } - - $result[] = array_merge([ - 'name' => $entry, - 'title' => beautifyTabName($entry), - 'type' => $type, - 'url' => $url - ], $meta); - } - } - usort($result, function($a, $b) { - if (($a['type'] === 'file') !== ($b['type'] === 'file')) { - return $a['type'] === 'file' ? 1 : -1; - } - return strnatcasecmp($a['name'], $b['name']); - }); - return $result; - } - - $tree = scanAdsFolder($adsRoot, '', $baseUrl, $clientDir, $projectName); + + $result = AdsOverviewService::generateOverview($clientDir, $projectName); + header('Content-Type: application/json'); - echo json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + echo json_encode($result['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } - - // Projekt→Client Mapping für Admin Smart URL Resolution + + /** + * Get project→client mapping for admin smart URL resolution + */ public static function getProjectClientMapping() { - // Nur Admins dürfen diese API verwenden - $user = self::requireAuth(); - if (!isset($user['role']) || $user['role'] !== 'admin') { - http_response_code(403); - header('Content-Type: application/json'); - echo json_encode([ - 'success' => false, - 'error' => [ - 'code' => 'FORBIDDEN', - 'message' => 'Nur Administratoren können das Projekt-Client-Mapping abrufen.' - ] - ]); - exit; - } - - // Lade Admin-Daten für Client-Berechtigungen - $adminData = self::getAdminData($user['username']); + $user = AuthorizationService::requireAdminAccess(); + + // Load admin data for client permissions + $adminData = AuthorizationService::getAdminData($user['username']); $disallowedClients = $adminData['disallowedClients'] ?? []; - - $areaPath = __DIR__ . self::getAreaPath(); + + $areaPath = __DIR__ . FileSystemService::getAreaPath(); $mapping = []; - - // Durchsuche alle Client-Ordner + + // Scan all client directories if (is_dir($areaPath)) { $clientDirs = scandir($areaPath); foreach ($clientDirs as $clientDir) { if ($clientDir === '.' || $clientDir === '..' || !is_dir($areaPath . '/' . $clientDir)) { continue; } - - // Prüfe ob Admin Zugriff auf diesen Client hat + + // Check if admin has access to this client if (in_array($clientDir, $disallowedClients)) { - continue; // Client ist für diesen Admin nicht erlaubt + continue; } - + $clientPath = $areaPath . '/' . $clientDir; if (is_dir($clientPath)) { $projects = scandir($clientPath); @@ -752,19 +298,19 @@ class ProjectsController { if ($project === '.' || $project === '..' || !is_dir($clientPath . '/' . $project)) { continue; } - - // Ignoriere spezielle Dateien + + // Ignore special files if (in_array($project, ['project_order.json', 'logins.json'])) { continue; } - - // Füge Projekt→Client Mapping hinzu + + // Add project→client mapping $mapping[$project] = $clientDir; } } } } - + header('Content-Type: application/json'); echo json_encode([ 'success' => true, @@ -775,18 +321,4 @@ class ProjectsController { ] ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } - - // Hilfsfunktion: Admin-Daten laden - private static function getAdminData($username) { - $adminFile = __DIR__ . '/../../storage/data/admins.json'; - if (file_exists($adminFile)) { - $admins = json_decode(file_get_contents($adminFile), true); - foreach ($admins as $admin) { - if ($admin['username'] === $username) { - return $admin; - } - } - } - return []; - } } diff --git a/backend/src/Services/AdsOverviewService.php b/backend/src/Services/AdsOverviewService.php new file mode 100644 index 0000000..4eb3bd6 --- /dev/null +++ b/backend/src/Services/AdsOverviewService.php @@ -0,0 +1,185 @@ + true, + 'data' => $tree + ]; + } + + /** + * Recursively scan ads folder structure + */ + private static function scanAdsFolder($absPath, $relPath, $baseUrl, $clientDir, $projectName) { + $result = []; + if (!is_dir($absPath)) return $result; + + $entries = array_diff(scandir($absPath), ['.', '..']); + + foreach ($entries as $entry) { + $entryAbs = "$absPath/$entry"; + $entryRel = $relPath ? "$relPath/$entry" : $entry; + + if (is_dir($entryAbs)) { + $children = self::scanAdsFolder($entryAbs, $entryRel, $baseUrl, $clientDir, $projectName); + $depth = substr_count($entryRel, '/'); + + $type = match($depth) { + 0 => 'category', + 1 => 'subcategory', + 2 => 'ad', + default => 'folder' + }; + + $node = [ + 'name' => $entry, + 'title' => MediaAnalysisService::beautifyTabName($entry), + 'type' => $type, + ]; + + if ($type === 'ad') { + $node = self::processAdFolder($node, $entryAbs, $children, $baseUrl, $clientDir, $projectName, $relPath, $entry); + } else { + $node['children'] = array_filter($children, fn($c) => in_array($c['type'], ['category','subcategory','ad','folder'])); + } + + $result[] = $node; + + } else { + // File + $fileNode = self::processFile($entryAbs, $entry, $baseUrl, $clientDir, $projectName, $relPath); + if ($fileNode) { + $result[] = $fileNode; + } + } + } + + // Sort: folders first, then files + usort($result, function($a, $b) { + if (($a['type'] === 'file') !== ($b['type'] === 'file')) { + return $a['type'] === 'file' ? 1 : -1; + } + return strnatcasecmp($a['name'], $b['name']); + }); + + return $result; + } + + /** + * Process ad folder with HTML detection and config parsing + */ + private static function processAdFolder($node, $entryAbs, $children, $baseUrl, $clientDir, $projectName, $relPath, $entry) { + // Check if subfolder with 'html' in name exists + $htmlFolder = null; + foreach (scandir($entryAbs) as $sub) { + if ($sub === '.' || $sub === '..') continue; + if (is_dir("$entryAbs/$sub") && stripos($sub, 'html') !== false) { + $htmlFolder = $sub; + break; + } + } + + // Collect files, but ignore config.yaml + $node['files'] = array_values(array_filter($children, function($child) { + if (in_array($child['type'], ['category','subcategory','ad','folder'])) return false; + if (isset($child['name']) && preg_match('/^config\.ya?ml$/i', $child['name'])) return false; + return true; + })); + + // Special case: HTML folder found + if ($htmlFolder) { + $htmlAbs = "$entryAbs/$htmlFolder"; + $htmlIndex = "$htmlAbs/index.html"; + + // Look for config.yaml one level above HTML folder (in ad folder) + $configYaml = "$entryAbs/config.yaml"; + $meta = []; + + if (is_file($configYaml)) { + $meta = MediaAnalysisService::parseAdConfig($configYaml); + } else { + // Fallback: try to extract dimensions from ad folder name (e.g. 800x250) + $adFolderName = basename($entryAbs); + $meta = MediaAnalysisService::parseDimensionsFromFolderName($adFolderName); + } + + if (is_file($htmlIndex)) { + $url = "$baseUrl/area/" . rawurlencode($clientDir) . "/" . rawurlencode($projectName) . "/ads/" + . ($relPath ? str_replace('%2F', '/', rawurlencode($relPath)) . '/' : '') + . rawurlencode($entry) . "/" . rawurlencode($htmlFolder) . "/index.html"; + + $fileMeta = [ + 'name' => 'index.html', + 'type' => 'html', + 'url' => $url, + 'size' => filesize($htmlIndex), + 'width' => $meta['width'] ?? 123, + 'height' => $meta['height'] ?? 456 + ]; + + // Add index.html only if not already in files + $already = false; + foreach ($node['files'] as $f) { + if ($f['name'] === 'index.html') { + $already = true; + break; + } + } + if (!$already) { + $node['files'][] = $fileMeta; + } + } + } + + return $node; + } + + /** + * Process individual file with metadata + */ + private static function processFile($fileAbs, $entry, $baseUrl, $clientDir, $projectName, $relPath) { + $type = FileSystemService::getFileType($entry); + + $url = "$baseUrl/area/" . rawurlencode($clientDir) . "/" . rawurlencode($projectName) . "/ads/" + . ($relPath ? str_replace('%2F', '/', rawurlencode($relPath)) . '/' : '') . rawurlencode($entry); + + $meta = ['size' => filesize($fileAbs)]; + + // Get metadata based on file type + if ($type === 'image') { + $imageMeta = MediaAnalysisService::getImageMetadata($fileAbs); + $meta = array_merge($meta, $imageMeta); + } elseif ($type === 'video') { + $videoMeta = MediaAnalysisService::getVideoMetadata($fileAbs); + $meta = array_merge($meta, $videoMeta); + } + + return array_merge([ + 'name' => $entry, + 'title' => MediaAnalysisService::beautifyTabName($entry), + 'type' => $type === 'other' ? 'file' : $type, + 'url' => $url + ], $meta); + } +} diff --git a/backend/src/Services/AuthorizationService.php b/backend/src/Services/AuthorizationService.php new file mode 100644 index 0000000..2dffe21 --- /dev/null +++ b/backend/src/Services/AuthorizationService.php @@ -0,0 +1,127 @@ + false, + 'error' => [ + 'code' => 'UNAUTHORIZED', + 'message' => 'Kein gültiges Auth-Token übergeben.' + ] + ]); + exit; + } + + $jwt = $matches[1]; + require_once __DIR__ . '/AuthService.php'; + $user = AuthService::verifyJWT($jwt); + + if (!$user) { + http_response_code(401); + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error' => [ + 'code' => 'UNAUTHORIZED', + 'message' => 'Token ungültig oder abgelaufen.' + ] + ]); + exit; + } + + return $user; + } + + /** + * Check if the logged-in user can access the requested client directory + */ + public static function requireClientAccess($clientDir) { + $user = self::requireAuth(); + + // Admins have general access, but check disallowedClients + if (isset($user['role']) && $user['role'] === 'admin') { + $adminData = self::getAdminData($user['username']); + $disallowedClients = $adminData['disallowedClients'] ?? []; + + if (in_array($clientDir, $disallowedClients)) { + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error' => [ + 'code' => 'FORBIDDEN', + 'message' => "Zugriff auf Client '{$clientDir}' ist für diesen Administrator nicht erlaubt." + ] + ]); + exit; + } + return $user; + } + + // Client users can only access their own directory + if ($user['role'] === 'client' && $user['dir'] !== $clientDir) { + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error' => [ + 'code' => 'FORBIDDEN', + 'message' => 'Nicht berechtigt für diesen Bereich.' + ] + ]); + exit; + } + + return $user; + } + + /** + * Load admin data from storage + */ + public static function getAdminData($username) { + $adminFile = __DIR__ . '/../../storage/data/admins.json'; + if (file_exists($adminFile)) { + $admins = json_decode(file_get_contents($adminFile), true); + foreach ($admins as $admin) { + if ($admin['username'] === $username) { + return $admin; + } + } + } + return []; + } + + /** + * Check if user is admin + */ + public static function requireAdminAccess() { + $user = self::requireAuth(); + if (!isset($user['role']) || $user['role'] !== 'admin') { + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error' => [ + 'code' => 'FORBIDDEN', + 'message' => 'Nur Administratoren können diese Funktion nutzen.' + ] + ]); + exit; + } + return $user; + } +} diff --git a/backend/src/Services/FileSystemService.php b/backend/src/Services/FileSystemService.php new file mode 100644 index 0000000..5d1e481 --- /dev/null +++ b/backend/src/Services/FileSystemService.php @@ -0,0 +1,96 @@ + filesize($filePath)]; + + $imgInfo = @getimagesize($filePath); + if ($imgInfo) { + $meta['width'] = $imgInfo[0]; + $meta['height'] = $imgInfo[1]; + } + + return $meta; + } + + /** + * Get video metadata (dimensions and duration) + */ + public static function getVideoMetadata($filePath) { + $meta = ['size' => filesize($filePath)]; + + // Try ffprobe first + $ffprobePath = shell_exec('which ffprobe'); + $ffprobeUsed = false; + + if ($ffprobePath) { + $ffprobe = trim($ffprobePath); + if ($ffprobe !== '') { + $cmd = escapeshellcmd($ffprobe) . ' -v error -select_streams v:0 -show_entries stream=width,height,duration -of default=noprint_wrappers=1:nokey=1 ' . escapeshellarg($filePath); + $out = shell_exec($cmd); + + if ($out) { + $lines = explode("\n", trim($out)); + if (count($lines) >= 3) { + $meta['width'] = (int)$lines[0]; + $meta['height'] = (int)$lines[1]; + $meta['duration'] = (float)$lines[2]; + $ffprobeUsed = true; + } + } + } + } + + // Fallback to getID3 if ffprobe not available or failed + if (!$ffprobeUsed) { + try { + $getID3 = new \getID3(); + $info = $getID3->analyze($filePath); + + if (!empty($info['video']['resolution_x']) && !empty($info['video']['resolution_y'])) { + $meta['width'] = (int)$info['video']['resolution_x']; + $meta['height'] = (int)$info['video']['resolution_y']; + } + + if (!empty($info['playtime_seconds'])) { + $meta['duration'] = (float)$info['playtime_seconds']; + } + } catch (\Exception $e) { + // Ignore errors, metadata remains empty + } + } + + return $meta; + } + + /** + * Parse config.yaml for ad dimensions + */ + public static function parseAdConfig($configPath) { + if (!file_exists($configPath)) { + return []; + } + + $yaml = file_get_contents($configPath); + $meta = []; + $width = 0; + $height = 0; + + $lines = preg_split('/\r?\n/', $yaml); + foreach ($lines as $line) { + if ($width === 0 && preg_match('/width\s*:\s*["\']?([\d,.]+)\s*([a-zA-Z%]*)["\']?/i', $line, $wMatch)) { + $val = str_replace([',', ' '], '', $wMatch[1]); + $width = (int)floatval($val); + } + if ($height === 0 && preg_match('/height\s*:\s*["\']?([\d,.]+)\s*([a-zA-Z%]*)["\']?/i', $line, $hMatch)) { + $val = str_replace([',', ' '], '', $hMatch[1]); + $height = (int)floatval($val); + } + } + + if ($width > 0) $meta['width'] = $width; + if ($height > 0) $meta['height'] = $height; + + return $meta; + } + + /** + * Extract dimensions from folder name (e.g., "800x250") + */ + public static function parseDimensionsFromFolderName($folderName) { + if (preg_match('/(\d{2,5})[xX](\d{2,5})/', $folderName, $matches)) { + return [ + 'width' => (int)$matches[1], + 'height' => (int)$matches[2] + ]; + } + return []; + } + + /** + * Beautify tab names (replace _ and - with spaces, capitalize) + */ + public static function beautifyTabName($name) { + $name = str_replace(['_', '-'], ' ', $name); + $name = mb_strtolower($name, 'UTF-8'); + $name = preg_replace_callback('/\b\w/u', function($m) { + return mb_strtoupper($m[0], 'UTF-8'); + }, $name); + return $name; + } +} diff --git a/backend/src/Services/ProjectService.php b/backend/src/Services/ProjectService.php new file mode 100644 index 0000000..2853b70 --- /dev/null +++ b/backend/src/Services/ProjectService.php @@ -0,0 +1,246 @@ + false, + 'error' => [ + 'code' => 'NOT_FOUND', + 'message' => 'Kundenordner nicht gefunden.' + ] + ]; + } + + // Load client display name + $clientDisplay = self::getClientDisplayName($clientDir); + + // Load project order if available + $projectOrder = self::loadProjectOrder($base); + + // Collect available projects + $foundProjects = self::scanProjectsInDirectory($base, $clientDir, $clientDisplay); + + // Sort projects according to order + $projects = self::sortProjectsByOrder($foundProjects, $projectOrder); + + return [ + 'success' => true, + 'projects' => $projects + ]; + } + + /** + * Save project order to project_order.json + */ + public static function saveProjectOrder($clientDir, $orderData) { + $base = realpath(__DIR__ . FileSystemService::getAreaPath() . '/' . $clientDir); + + if (!$base || !is_dir($base)) { + return [ + 'success' => false, + 'error' => [ + 'code' => 'NOT_FOUND', + 'message' => 'Kundenordner nicht gefunden.' + ] + ]; + } + + // Validate that all projects in order array actually exist + $existingProjects = self::getExistingProjectNames($base); + + foreach ($orderData['order'] as $projectName) { + if (!in_array($projectName, $existingProjects)) { + return [ + 'success' => false, + 'error' => [ + 'code' => 'INVALID_PROJECT', + 'message' => "Projekt '$projectName' existiert nicht." + ] + ]; + } + } + + // Save new order + $projectOrderFile = $base . '/project_order.json'; + $result = file_put_contents($projectOrderFile, json_encode($orderData['order'], JSON_PRETTY_PRINT)); + + if ($result === false) { + return [ + 'success' => false, + 'error' => [ + 'code' => 'WRITE_ERROR', + 'message' => 'Fehler beim Speichern der project_order.json.' + ] + ]; + } + + return [ + 'success' => true, + 'message' => 'Projekt-Reihenfolge gespeichert.' + ]; + } + + /** + * Get project details (logo, poster, etc.) + */ + public static function getProjectDetails($clientDir, $projectName) { + $setupDir = __DIR__ . FileSystemService::getAreaPath() . "/$clientDir/$projectName/setup"; + $poster = null; + $logo = null; + + if (is_dir($setupDir)) { + foreach (scandir($setupDir) as $file) { + if ($file === '.' || $file === '..') continue; + + if (stripos($file, 'poster') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { + $poster = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($projectName) . '/setup/' . rawurlencode($file); + } + + if (!$logo && stripos($file, 'logo') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { + $logo = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($projectName) . '/setup/' . rawurlencode($file); + } + + if ($poster && $logo) break; + } + } + + return [ + 'success' => true, + 'data' => [ + 'poster' => $poster, + 'logo' => $logo, + ] + ]; + } + + /** + * Private helper methods + */ + + private static function getClientDisplayName($clientDir) { + $loginsFile = __DIR__ . '/../../storage/data/clients.json'; + $clientDisplay = $clientDir; + + if (file_exists($loginsFile)) { + $logins = json_decode(file_get_contents($loginsFile), true); + foreach ($logins as $login) { + if (isset($login['dir']) && $login['dir'] === $clientDir) { + $clientDisplay = $login['dir']; + break; + } + } + } + + return $clientDisplay; + } + + private static function loadProjectOrder($basePath) { + $projectOrderFile = $basePath . '/project_order.json'; + $projectOrder = []; + + if (file_exists($projectOrderFile)) { + $orderContent = file_get_contents($projectOrderFile); + $decoded = json_decode($orderContent, true); + if (is_array($decoded)) { + $projectOrder = $decoded; + } + } + + return $projectOrder; + } + + private static function scanProjectsInDirectory($base, $clientDir, $clientDisplay) { + $foundProjects = []; + + foreach (scandir($base) as $entry) { + if ($entry === '.' || $entry === '..') continue; + + $path = $base . '/' . $entry; + if (is_dir($path)) { + $setupDir = $path . '/setup'; + $poster = null; + $logo = null; + + if (is_dir($setupDir)) { + foreach (scandir($setupDir) as $file) { + if (!$poster && stripos($file, 'poster') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { + $poster = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($entry) . '/setup/' . rawurlencode($file); + } + if (!$logo && stripos($file, 'logo') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) { + $logo = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($entry) . '/setup/' . rawurlencode($file); + } + if ($poster && $logo) break; + } + } + + $isHE = (stripos($entry, 'HE_') === 0); + $foundProjects[$entry] = [ + 'name' => $entry, + 'path' => $entry, + 'poster' => $poster, + 'logo' => $logo, + 'isHE' => $isHE, + 'client' => $clientDisplay + ]; + } + } + + return $foundProjects; + } + + private static function sortProjectsByOrder($foundProjects, $projectOrder) { + $projects = []; + + if (!empty($projectOrder)) { + // First add projects in defined order + foreach ($projectOrder as $orderedProject) { + if (isset($foundProjects[$orderedProject])) { + $projects[] = $foundProjects[$orderedProject]; + unset($foundProjects[$orderedProject]); + } + } + + // New projects (not in order list) come at the BEGINNING + $remainingProjects = array_values($foundProjects); + usort($remainingProjects, function($a, $b) { + return strcmp($a['name'], $b['name']); + }); + + // Insert new projects BEFORE sorted ones + $projects = array_merge($remainingProjects, $projects); + } else { + // Fallback: Alphabetical sorting + $projects = array_values($foundProjects); + usort($projects, function($a, $b) { + return strcmp($a['name'], $b['name']); + }); + } + + return $projects; + } + + private static function getExistingProjectNames($basePath) { + $existingProjects = []; + foreach (scandir($basePath) as $entry) { + if ($entry === '.' || $entry === '..') continue; + if (is_dir($basePath . '/' . $entry)) { + $existingProjects[] = $entry; + } + } + return $existingProjects; + } +}