New Branch Structure
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Services;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Modules\Stylist\Theme\Theme;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
|
||||
final class ThemePackageManager
|
||||
{
|
||||
public function __construct(private readonly Filesystem $files, private readonly Loader $loader, private readonly ThemeValidator $validator) {}
|
||||
|
||||
public function install(UploadedFile $upload): Theme
|
||||
{
|
||||
if (strtolower($upload->getClientOriginalExtension()) !== 'zip') throw new RuntimeException('Only ZIP theme packages may be installed.');
|
||||
$temporary = storage_path('app/stylist/install-'.bin2hex(random_bytes(8)));
|
||||
$this->files->ensureDirectoryExists($temporary);
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($upload->getRealPath()) !== true) throw new RuntimeException('The uploaded ZIP could not be opened.');
|
||||
for ($i=0; $i<$zip->numFiles; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) { $zip->close(); throw new RuntimeException('The ZIP contains an unsafe path.'); }
|
||||
}
|
||||
$zip->extractTo($temporary); $zip->close();
|
||||
$manifest = collect($this->files->allFiles($temporary))->first(fn ($file) => $file->getFilename() === 'theme.json');
|
||||
if (! $manifest) { $this->files->deleteDirectory($temporary); throw new RuntimeException('The package does not contain theme.json.'); }
|
||||
$source = $manifest->getPath();
|
||||
$theme = $this->loader->fromPath($source);
|
||||
if ($theme->parent() && ! $this->loader->all()->has($theme->parent())) throw new RuntimeException('Parent theme '.$theme->parent().' is not installed.');
|
||||
$issues = $this->validator->validate($theme);
|
||||
if (collect($issues)->contains('severity', 'error')) throw new RuntimeException('Accessibility validation failed: '.collect($issues)->where('severity', 'error')->pluck('message')->implode(' '));
|
||||
$target = rtrim((string) config('stylist.themes_path'), '/').'/'.$theme->key();
|
||||
if ($this->files->isDirectory($target)) $this->files->deleteDirectory($target);
|
||||
$this->files->copyDirectory($source, $target);
|
||||
$this->files->deleteDirectory($temporary);
|
||||
$this->loader->flush();
|
||||
return $this->loader->find($theme->key());
|
||||
}
|
||||
|
||||
public function uninstall(Theme $theme): void
|
||||
{
|
||||
$projectRoot = realpath((string) config('stylist.themes_path'));
|
||||
$themePath = realpath($theme->path());
|
||||
if (! $projectRoot || ! $themePath || ! str_starts_with($themePath, $projectRoot.DIRECTORY_SEPARATOR)) throw new RuntimeException('Bundled themes cannot be uninstalled.');
|
||||
if (collect($this->loader->all())->contains(fn (Theme $candidate) => $candidate->parent() === $theme->key())) throw new RuntimeException('This theme has installed child themes.');
|
||||
$this->files->deleteDirectory($themePath); $this->loader->flush();
|
||||
}
|
||||
|
||||
public function export(Theme $theme): string
|
||||
{
|
||||
$directory = storage_path('app/stylist/exports'); $this->files->ensureDirectoryExists($directory);
|
||||
$path = $directory.'/'.$theme->key().'-'.$theme->version().'.zip';
|
||||
$zip = new ZipArchive(); if ($zip->open($path, ZipArchive::CREATE|ZipArchive::OVERWRITE) !== true) throw new RuntimeException('Unable to create theme archive.');
|
||||
foreach ($this->files->allFiles($theme->path()) as $file) $zip->addFile($file->getPathname(), $theme->key().'/'.$file->getRelativePathname());
|
||||
$zip->close(); return $path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user