- Added standard Laravel directory structure and configuration. - Included Svelte and Tailwind configuration for the admin interface. - Added core PHPUnit and testing scripts.
159 lines
5 KiB
PHP
159 lines
5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Setting;
|
|
use App\Support\ThemeManager;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\File;
|
|
use Exception;
|
|
|
|
/**
|
|
* Service to handle Theme-related operations.
|
|
*/
|
|
class ThemeService
|
|
{
|
|
/**
|
|
* @var ThemeManager
|
|
*/
|
|
protected ThemeManager $themeManager;
|
|
|
|
/**
|
|
* ThemeService constructor.
|
|
*
|
|
* @param ThemeManager $themeManager
|
|
*/
|
|
public function __construct(ThemeManager $themeManager)
|
|
{
|
|
$this->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();
|
|
}
|
|
}
|