themeManager = $themeManager; } /** * Upload and install a new theme from a ZIP file. * * @param UploadedFile $zipFile The uploaded theme ZIP file. * @return array Result containing success status and theme metadata. * @throws Exception If ZIP extraction or validation fails. */ public function upload(UploadedFile $zipFile): array { /** * Initialize temporary extraction directory. * Using a unique ID ensures no collisions if multiple themes are uploaded simultaneously. */ $tempPath = storage_path('app/temp/theme_upload_' . uniqid()); File::makeDirectory($tempPath, 0755, true); try { $zipFilePath = $zipFile->getRealPath(); /** * 1. ZIP Extraction. * We prioritize the native PHP ZipArchive class for performance. * If not available, we fall back to the system 'unzip' command (common on Linux). */ if (class_exists('ZipArchive')) { $zip = new \ZipArchive(); if ($zip->open($zipFilePath) === true) { $zip->extractTo($tempPath); $zip->close(); } else { throw new Exception('Failed to open ZIP file via ZipArchive.'); } } else { $command = "unzip -q " . escapeshellarg($zipFilePath) . " -d " . escapeshellarg($tempPath); $output = []; $returnVar = 0; exec($command, $output, $returnVar); if ($returnVar !== 0) { throw new Exception('Failed to extract ZIP file via system unzip command.'); } } /** * 2. Path Discovery. * We check if the ZIP contains a single root folder or if the files are at the root. * This handles different ZIP packaging styles (folders vs flat). */ $rootContents = File::directories($tempPath); $rootFiles = File::files($tempPath); $extractDir = $tempPath; $themeSlug = null; if (count($rootContents) === 1 && count($rootFiles) === 0) { // ZIP has a single top-level folder $extractDir = $rootContents[0]; $themeSlug = basename($extractDir); } else { // ZIP is flat (files are at root) $themeSlug = pathinfo($zipFile->getClientOriginalName(), PATHINFO_FILENAME); } // Sanitize theme slug to prevent filesystem issues $themeSlug = strtolower(preg_replace('/[^a-z0-9_-]/i', '-', $themeSlug)); /** * 3. Validation. * A valid theme MUST contain a 'theme.md' file. */ if (! File::exists($extractDir . '/theme.md')) { File::deleteDirectory($tempPath); throw new Exception('Invalid theme: theme.md is missing.'); } /** * 4. Final Deployment. * Move the extracted files to the project-wide 'themes' directory. * Existing themes with the same slug are overwritten. */ $finalPath = base_path('themes/' . $themeSlug); if (File::exists($finalPath)) { File::deleteDirectory($finalPath); } File::copyDirectory($extractDir, $finalPath); File::deleteDirectory($tempPath); return [ 'success' => true, 'slug' => $themeSlug, 'metadata' => $this->themeManager->getMetadata($themeSlug), ]; } catch (Exception $e) { File::deleteDirectory($tempPath); throw $e; } } /** * Activate a theme. * * @param string $themeSlug The slug of the theme to activate. * @return bool True if activation was successful. * @throws Exception If the theme is not found. */ public function activate(string $themeSlug): bool { if (! file_exists(base_path('themes/' . $themeSlug))) { throw new Exception('Theme not found.'); } Setting::set('active_theme', $themeSlug, 'cms'); return true; } /** * List all installed themes with their metadata. * * @return array List of installed themes. */ public function list(): array { return $this->themeManager->getThemes(); } }