Schneespur — Open-source winter service documentation software (PWA + Admin). GPS tracking via OwnTracks, weather data, photo evidence, and legally compliant service records for winter maintenance operators. License: AGPL-3.0-or-later
164 lines
4.6 KiB
PHP
164 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Log;
|
|
use RuntimeException;
|
|
use ZipArchive;
|
|
|
|
class SchneespurModuleInstaller
|
|
{
|
|
private string $modulesPath;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->modulesPath = rtrim(config('schneespur_modules.modules_path'), '/');
|
|
}
|
|
|
|
public function install(string $zipPath, string $slug): bool
|
|
{
|
|
$this->assertValidSlug($slug);
|
|
Log::info('schneespur-modules: install started', ['slug' => $slug]);
|
|
|
|
$targetDir = $this->modulePath($slug);
|
|
|
|
if (File::isDirectory($targetDir)) {
|
|
Log::warning('schneespur-modules: module directory already exists', ['slug' => $slug]);
|
|
return false;
|
|
}
|
|
|
|
return $this->extractZip($zipPath, $targetDir, $slug);
|
|
}
|
|
|
|
public function update(string $zipPath, string $slug): bool
|
|
{
|
|
$this->assertValidSlug($slug);
|
|
Log::info('schneespur-modules: update started', ['slug' => $slug]);
|
|
|
|
$targetDir = $this->modulePath($slug);
|
|
$backupDir = $this->backupPath($slug);
|
|
|
|
if (File::isDirectory($backupDir)) {
|
|
File::deleteDirectory($backupDir);
|
|
}
|
|
|
|
if (File::isDirectory($targetDir)) {
|
|
File::moveDirectory($targetDir, $backupDir);
|
|
}
|
|
|
|
if ($this->extractZip($zipPath, $targetDir, $slug)) {
|
|
if (File::isDirectory($backupDir)) {
|
|
File::deleteDirectory($backupDir);
|
|
}
|
|
Log::info('schneespur-modules: update complete', ['slug' => $slug]);
|
|
return true;
|
|
}
|
|
|
|
Log::error('schneespur-modules: update failed, triggering rollback', ['slug' => $slug]);
|
|
$this->rollback($slug);
|
|
return false;
|
|
}
|
|
|
|
public function remove(string $slug): bool
|
|
{
|
|
$this->assertValidSlug($slug);
|
|
$targetDir = $this->modulePath($slug);
|
|
|
|
if (! File::isDirectory($targetDir)) {
|
|
Log::warning('schneespur-modules: remove failed — directory not found', ['slug' => $slug]);
|
|
return false;
|
|
}
|
|
|
|
File::deleteDirectory($targetDir);
|
|
Log::info('schneespur-modules: module removed', ['slug' => $slug]);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function rollback(string $slug): bool
|
|
{
|
|
$this->assertValidSlug($slug);
|
|
$targetDir = $this->modulePath($slug);
|
|
$backupDir = $this->backupPath($slug);
|
|
|
|
if (! File::isDirectory($backupDir)) {
|
|
Log::error('schneespur-modules: rollback failed — no backup found', ['slug' => $slug]);
|
|
return false;
|
|
}
|
|
|
|
if (File::isDirectory($targetDir)) {
|
|
File::deleteDirectory($targetDir);
|
|
}
|
|
|
|
File::moveDirectory($backupDir, $targetDir);
|
|
Log::info('schneespur-modules: rollback triggered', ['slug' => $slug]);
|
|
|
|
return true;
|
|
}
|
|
|
|
private function extractZip(string $zipPath, string $targetDir, string $slug): bool
|
|
{
|
|
$zip = new ZipArchive();
|
|
$result = $zip->open($zipPath);
|
|
|
|
if ($result !== true) {
|
|
Log::error('schneespur-modules: ZIP open failed', [
|
|
'slug' => $slug,
|
|
'error' => $result,
|
|
]);
|
|
return false;
|
|
}
|
|
|
|
if (! $this->validateZipEntries($zip, $slug)) {
|
|
$zip->close();
|
|
return false;
|
|
}
|
|
|
|
File::ensureDirectoryExists($targetDir, 0755);
|
|
$zip->extractTo($targetDir);
|
|
$zip->close();
|
|
|
|
Log::info('schneespur-modules: unpack complete', ['slug' => $slug]);
|
|
|
|
return true;
|
|
}
|
|
|
|
private function validateZipEntries(ZipArchive $zip, string $slug): bool
|
|
{
|
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
|
$entry = $zip->getNameIndex($i);
|
|
|
|
if ($entry === false) {
|
|
continue;
|
|
}
|
|
|
|
if (str_contains($entry, '..') || str_starts_with($entry, '/')) {
|
|
Log::error('schneespur-modules: path traversal detected in ZIP', [
|
|
'slug' => $slug,
|
|
'entry' => $entry,
|
|
]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function assertValidSlug(string $slug): void
|
|
{
|
|
if (! preg_match('/^[a-z0-9_-]+$/i', $slug)) {
|
|
throw new RuntimeException("Ungültiger Modul-Slug: {$slug}");
|
|
}
|
|
}
|
|
|
|
private function modulePath(string $slug): string
|
|
{
|
|
return $this->modulesPath . '/' . $slug;
|
|
}
|
|
|
|
private function backupPath(string $slug): string
|
|
{
|
|
return $this->modulesPath . '/' . $slug . '.bak';
|
|
}
|
|
}
|