refactor: modularize ProjectsController into service-based architecture
- Split monolithic 600+ line controller into 5 focused service classes - AuthorizationService: centralized auth/permission handling - FileSystemService: path management and file operations - MediaAnalysisService: image/video metadata extraction - ProjectService: business logic for project operations - AdsOverviewService: complex recursive tree generation - Apply Single Responsibility Principle for better maintainability - Preserve all existing functionality and API compatibility - Remove temporary backup and development files - Improve code organization and reusability
This commit is contained in:
@@ -1,236 +1,41 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../vendor/getid3/getid3.php';
|
||||
require_once __DIR__ . '/../Services/AuthorizationService.php';
|
||||
require_once __DIR__ . '/../Services/FileSystemService.php';
|
||||
require_once __DIR__ . '/../Services/MediaAnalysisService.php';
|
||||
require_once __DIR__ . '/../Services/ProjectService.php';
|
||||
require_once __DIR__ . '/../Services/AdsOverviewService.php';
|
||||
|
||||
/**
|
||||
* ProjectsController - Refactored and modularized
|
||||
* Main controller for project-related API endpoints
|
||||
*/
|
||||
class ProjectsController {
|
||||
|
||||
// Bestimmt den korrekten Pfad zum area-Ordner basierend auf der Umgebung
|
||||
private static function getAreaPath() {
|
||||
// Prüfe ob wir in der Development-Struktur sind (backend/src/Api)
|
||||
$devPath = __DIR__ . '/../../../area';
|
||||
if (is_dir($devPath)) {
|
||||
return '/../../../area';
|
||||
}
|
||||
|
||||
// Prüfe ob wir in der Deployment-Struktur sind (src/Api)
|
||||
$deployPath = __DIR__ . '/../../area';
|
||||
if (is_dir($deployPath)) {
|
||||
return '/../../area';
|
||||
}
|
||||
|
||||
// Fallback auf Development-Pfad
|
||||
return '/../../../area';
|
||||
}
|
||||
|
||||
// Zentrale Liste der zu ignorierenden Dateien
|
||||
private static function getIgnoredFiles() {
|
||||
return [
|
||||
'.DS_Store', // macOS System-Datei
|
||||
'Thumbs.db', // Windows Thumbnail-Cache
|
||||
'desktop.ini', // Windows Desktop-Konfiguration
|
||||
'.gitignore', // Git-Konfiguration
|
||||
'.gitkeep', // Git-Platzhalter
|
||||
'config.yaml', // Konfigurationsdateien
|
||||
'config.yml',
|
||||
'setup.yaml',
|
||||
'setup.yml',
|
||||
'.htaccess', // Apache-Konfiguration
|
||||
'index.php', // PHP-Index (außer in HTML-Ordnern)
|
||||
'web.config', // IIS-Konfiguration
|
||||
'.env', // Environment-Variablen
|
||||
'.env.local',
|
||||
'README.md', // Dokumentation
|
||||
'readme.txt',
|
||||
'license.txt',
|
||||
'LICENSE'
|
||||
];
|
||||
}
|
||||
|
||||
// Prüft, ob der eingeloggte Client auf den gewünschten Bereich zugreifen darf
|
||||
private static function requireClientAccess($clientDir) {
|
||||
$user = self::requireAuth();
|
||||
// Admins haben grundsätzlich Zugriff, aber prüfe 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;
|
||||
}
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
185
backend/src/Services/AdsOverviewService.php
Normal file
185
backend/src/Services/AdsOverviewService.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/MediaAnalysisService.php';
|
||||
require_once __DIR__ . '/FileSystemService.php';
|
||||
|
||||
/**
|
||||
* AdsOverviewService
|
||||
* Handles the complex ads overview tree generation with metadata
|
||||
*/
|
||||
class AdsOverviewService {
|
||||
|
||||
/**
|
||||
* Generate recursive ads overview as structured tree
|
||||
*/
|
||||
public static function generateOverview($clientDir, $projectName) {
|
||||
$adsRoot = __DIR__ . FileSystemService::getAreaPath() . "/$clientDir/$projectName/ads";
|
||||
$baseUrl = $_ENV['BACKEND_URL'] ?? (
|
||||
isset($_SERVER['HTTP_HOST'])
|
||||
? (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST']
|
||||
: ''
|
||||
);
|
||||
|
||||
$tree = self::scanAdsFolder($adsRoot, '', $baseUrl, $clientDir, $projectName);
|
||||
|
||||
return [
|
||||
'success' => 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);
|
||||
}
|
||||
}
|
||||
127
backend/src/Services/AuthorizationService.php
Normal file
127
backend/src/Services/AuthorizationService.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AuthorizationService
|
||||
* Handles authentication and authorization for the AdsPreview system
|
||||
*/
|
||||
class AuthorizationService {
|
||||
|
||||
/**
|
||||
* JWT-based authentication check for all API methods
|
||||
*/
|
||||
public 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__ . '/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;
|
||||
}
|
||||
}
|
||||
96
backend/src/Services/FileSystemService.php
Normal file
96
backend/src/Services/FileSystemService.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* FileSystemService
|
||||
* Handles file system operations and path management
|
||||
*/
|
||||
class FileSystemService {
|
||||
|
||||
/**
|
||||
* Determine the correct path to the area folder based on environment
|
||||
*/
|
||||
public static function getAreaPath() {
|
||||
// Check if we're in development structure (backend/src/Api)
|
||||
$devPath = __DIR__ . '/../../../area';
|
||||
if (is_dir($devPath)) {
|
||||
return '/../../../area';
|
||||
}
|
||||
|
||||
// Check if we're in deployment structure (src/Api)
|
||||
$deployPath = __DIR__ . '/../../area';
|
||||
if (is_dir($deployPath)) {
|
||||
return '/../../area';
|
||||
}
|
||||
|
||||
// Fallback to development path
|
||||
return '/../../../area';
|
||||
}
|
||||
|
||||
/**
|
||||
* Central list of files to ignore
|
||||
*/
|
||||
public static function getIgnoredFiles() {
|
||||
return [
|
||||
'.DS_Store', // macOS System file
|
||||
'Thumbs.db', // Windows Thumbnail cache
|
||||
'desktop.ini', // Windows Desktop configuration
|
||||
'.gitignore', // Git configuration
|
||||
'.gitkeep', // Git placeholder
|
||||
'config.yaml', // Configuration files
|
||||
'config.yml',
|
||||
'setup.yaml',
|
||||
'setup.yml',
|
||||
'.htaccess', // Apache configuration
|
||||
'index.php', // PHP index (except in HTML folders)
|
||||
'web.config', // IIS configuration
|
||||
'.env', // Environment variables
|
||||
'.env.local',
|
||||
'README.md', // Documentation
|
||||
'readme.txt',
|
||||
'license.txt',
|
||||
'LICENSE'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be ignored
|
||||
*/
|
||||
public static function shouldIgnoreFile($filename) {
|
||||
$ignoredFiles = self::getIgnoredFiles();
|
||||
return in_array(strtolower($filename), array_map('strtolower', $ignoredFiles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file type based on extension
|
||||
*/
|
||||
public static function getFileType($filename) {
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'])) {
|
||||
return 'image';
|
||||
} elseif (in_array($ext, ['mp4', 'mov', 'avi', 'webm'])) {
|
||||
return 'video';
|
||||
} elseif (in_array($ext, ['html', 'htm'])) {
|
||||
return 'html';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create safe URL for file access
|
||||
*/
|
||||
public static function createFileUrl($clientDir, $projectName, $adsFolders, $filename) {
|
||||
$urlParts = array_map('rawurlencode', $adsFolders);
|
||||
return "/area/" . rawurlencode($clientDir) . "/" . rawurlencode($projectName) . "/ads/"
|
||||
. implode('/', $urlParts) . (count($urlParts) ? '/' : '') . rawurlencode($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate path is within allowed client directory
|
||||
*/
|
||||
public static function validatePath($realPath, $clientDir) {
|
||||
$allowedBasePath = realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir);
|
||||
return $realPath && strpos($realPath, $allowedBasePath) === 0;
|
||||
}
|
||||
}
|
||||
131
backend/src/Services/MediaAnalysisService.php
Normal file
131
backend/src/Services/MediaAnalysisService.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../vendor/getid3/getid3.php';
|
||||
|
||||
/**
|
||||
* MediaAnalysisService
|
||||
* Handles media file analysis and metadata extraction
|
||||
*/
|
||||
class MediaAnalysisService {
|
||||
|
||||
/**
|
||||
* Get image dimensions
|
||||
*/
|
||||
public static function getImageMetadata($filePath) {
|
||||
$meta = ['size' => 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;
|
||||
}
|
||||
}
|
||||
246
backend/src/Services/ProjectService.php
Normal file
246
backend/src/Services/ProjectService.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../Services/AuthorizationService.php';
|
||||
require_once __DIR__ . '/../Services/FileSystemService.php';
|
||||
|
||||
/**
|
||||
* ProjectService
|
||||
* Handles project-related operations
|
||||
*/
|
||||
class ProjectService {
|
||||
|
||||
/**
|
||||
* Get projects for a specific client with ordering
|
||||
*/
|
||||
public static function getProjectsForClient($clientDir) {
|
||||
$base = realpath(__DIR__ . FileSystemService::getAreaPath() . '/' . $clientDir);
|
||||
|
||||
if (!$base || !is_dir($base)) {
|
||||
return [
|
||||
'success' => 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user