From b409806f0203aca6d0a76b9e1356ea53d08cdb7f Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 21 Feb 2026 23:30:58 +0100 Subject: [PATCH] ver. 0.300: Manifest-based update system with checksum verification and file backup Replaces the manual ZIP packaging workflow with an automated build script. UpdateRepository now supports both manifest JSON format (new) and legacy _sql.txt/_files.txt format (fallback), enabling a smooth transition for existing client instances. Co-Authored-By: Claude Opus 4.6 --- .updateignore | 41 ++ TODO.md | 2 - admin/templates/update/main-view.php | 11 + autoload/Domain/Update/UpdateRepository.php | 274 +++++++++++++ .../admin/Controllers/UpdateController.php | 3 + build-update.ps1 | 388 ++++++++++++++++++ docs/CHANGELOG.md | 13 + docs/TESTING.md | 5 +- docs/UPDATE_INSTRUCTIONS.md | 209 +++++----- changes => migrations/.gitkeep | 0 test-debug.bat | 13 - test-simple.bat | 13 - test.bat | 13 - test.ps1 | 47 --- test.sh | 10 - .../Domain/Update/UpdateRepositoryTest.php | 102 ++++- updates/0.30/ver_0.300.zip | Bin 0 -> 7357 bytes updates/changelog.php | 4 + updates/versions.php | 2 +- 19 files changed, 932 insertions(+), 218 deletions(-) create mode 100644 .updateignore delete mode 100644 TODO.md create mode 100644 build-update.ps1 rename changes => migrations/.gitkeep (100%) delete mode 100644 test-debug.bat delete mode 100644 test-simple.bat delete mode 100644 test.bat delete mode 100644 test.ps1 delete mode 100644 test.sh create mode 100644 updates/0.30/ver_0.300.zip diff --git a/.updateignore b/.updateignore new file mode 100644 index 0000000..e85c356 --- /dev/null +++ b/.updateignore @@ -0,0 +1,41 @@ +# Dokumentacja (tylko wewnetrzna/deweloperska) +*.md +docs/ +CLAUDE.md +AGENTS.md + +# Narzedzia deweloperskie +.claude/ +.gitignore +.git/ +tests/ +phpunit.xml +phpunit.phar +composer.json +composer.lock +vendor/ +test.ps1 +memory/ + +# Infrastruktura aktualizacji (meta, nie runtime) +updates/changelog.php +updates/versions.php +updates/install.php +.updateignore +build-update.ps1 +migrations/ + +# Pliki konfiguracyjne klienta (wdrazane osobno) +config.php +.htaccess +admin/.htaccess +libraries/version.ini + +# Temp / cache / backups +temp/ +backups/ +cache/ +cron/temp/ + +# IDE +.vscode/ diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 4f8aba1..0000000 --- a/TODO.md +++ /dev/null @@ -1,2 +0,0 @@ -1. Operacje na kombinacjach nie czyszczą cache -2. Sprawdzić funkcję pod kątem SQL INJECTION \ No newline at end of file diff --git a/admin/templates/update/main-view.php b/admin/templates/update/main-view.php index a6a8a76..cb20910 100644 --- a/admin/templates/update/main-view.php +++ b/admin/templates/update/main-view.php @@ -41,6 +41,17 @@ +log ) ): ?> +
+
+ Log ostatniej aktualizacji +
+
+
log ); ?>
+
+
+ +
Changelog diff --git a/autoload/Domain/Update/UpdateRepository.php b/autoload/Domain/Update/UpdateRepository.php index a3dc55c..ab7c701 100644 --- a/autoload/Domain/Update/UpdateRepository.php +++ b/autoload/Domain/Update/UpdateRepository.php @@ -61,10 +61,284 @@ class UpdateRepository return [ 'success' => true, 'log' => $log, 'no_updates' => true ]; } + /** + * Dispatcher — próbuje pobrać manifest, jeśli jest → nowa ścieżka, jeśli brak → legacy. + */ private function downloadAndApply( string $ver, string $dir, array $log ): array { $baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir; + $manifest = $this->downloadManifest( $baseUrl, $ver ); + + if ( $manifest !== null ) { + $log[] = '[INFO] Znaleziono manifest dla wersji ' . $ver; + return $this->downloadAndApplyWithManifest( $ver, $dir, $manifest, $log ); + } + + $log[] = '[INFO] Brak manifestu, używam trybu legacy'; + return $this->downloadAndApplyLegacy( $ver, $dir, $log ); + } + + /** + * Pobiera manifest JSON dla danej wersji. + * + * @return array|null Zdekodowany manifest lub null jeśli brak + */ + private function downloadManifest( string $baseUrl, string $ver ) + { + $manifestUrl = $baseUrl . '/ver_' . $ver . '_manifest.json'; + + $ch = curl_init( $manifestUrl ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $ch, CURLOPT_HEADER, false ); + curl_setopt( $ch, CURLOPT_TIMEOUT, 15 ); + $response = curl_exec( $ch ); + $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); + curl_close( $ch ); + + if ( !$response || $httpCode !== 200 ) { + return null; + } + + $manifest = json_decode( $response, true ); + if ( !is_array( $manifest ) || !isset( $manifest['version'] ) ) { + return null; + } + + return $manifest; + } + + /** + * Aktualizacja z użyciem manifestu — checksum, backup, SQL z manifestu, usuwanie z manifestu. + */ + private function downloadAndApplyWithManifest( string $ver, string $dir, array $manifest, array $log ): array + { + $baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir; + + $log[] = '[INFO] Tryb aktualizacji: manifest'; + + // 1. Pobieranie ZIP + $zipUrl = $baseUrl . '/ver_' . $ver . '.zip'; + $log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl; + $file = @file_get_contents( $zipUrl ); + + if ( $file === false ) { + $log[] = '[ERROR] Nie udało się pobrać pliku ZIP'; + return [ 'success' => false, 'log' => $log ]; + } + + $fileSize = strlen( $file ); + $log[] = '[OK] Pobrano plik ZIP, rozmiar: ' . $fileSize . ' bajtów'; + + if ( $fileSize < 100 ) { + $log[] = '[ERROR] Plik ZIP jest za mały (prawdopodobnie błąd pobierania)'; + return [ 'success' => false, 'log' => $log ]; + } + + $dlHandler = @fopen( 'update.zip', 'w' ); + if ( !$dlHandler ) { + $log[] = '[ERROR] Nie udało się otworzyć pliku update.zip do zapisu'; + return [ 'success' => false, 'log' => $log ]; + } + $written = fwrite( $dlHandler, $file ); + fclose( $dlHandler ); + + if ( $written === false || $written === 0 ) { + $log[] = '[ERROR] Nie udało się zapisać pliku ZIP'; + return [ 'success' => false, 'log' => $log ]; + } + + $log[] = '[OK] Zapisano plik ZIP (' . $written . ' bajtów)'; + + // 2. Weryfikacja checksum + if ( isset( $manifest['checksum_zip'] ) ) { + $checksumResult = $this->verifyChecksum( 'update.zip', $manifest['checksum_zip'], $log ); + $log = $checksumResult['log']; + + if ( !$checksumResult['valid'] ) { + @unlink( 'update.zip' ); + return [ 'success' => false, 'log' => $log ]; + } + } + + // 3. Backup plików przed nadpisaniem + $log = $this->createBackup( $manifest, $log ); + + // 4. SQL z manifestu + if ( !empty( $manifest['sql'] ) ) { + $log[] = '[INFO] Wykonywanie zapytań SQL z manifestu (' . count( $manifest['sql'] ) . ')'; + $success = 0; + $errors = 0; + + foreach ( $manifest['sql'] as $query ) { + $query = trim( $query ); + if ( $query !== '' ) { + if ( $this->db->query( $query ) ) { + $success++; + } else { + $errors++; + $log[] = '[WARNING] Błąd SQL: ' . $query; + } + } + } + + $log[] = '[INFO] Wykonano zapytania SQL - sukces: ' . $success . ', błędy: ' . $errors; + } + + // 5. Usuwanie plików z manifestu + if ( !empty( $manifest['files']['deleted'] ) ) { + $deletedCount = 0; + foreach ( $manifest['files']['deleted'] as $relativePath ) { + $fullPath = '../' . $relativePath; + if ( file_exists( $fullPath ) ) { + if ( @unlink( $fullPath ) ) { + $deletedCount++; + } else { + $log[] = '[WARNING] Nie udało się usunąć pliku: ' . $fullPath; + } + } + } + $log[] = '[INFO] Usunięto plików: ' . $deletedCount; + } + + // 6. Usuwanie katalogów z manifestu + if ( !empty( $manifest['directories_deleted'] ) ) { + $deletedDirs = 0; + foreach ( $manifest['directories_deleted'] as $dirPath ) { + $fullPath = '../' . $dirPath; + if ( is_dir( $fullPath ) ) { + \Shared\Helpers\Helpers::delete_dir( $fullPath ); + $deletedDirs++; + } + } + $log[] = '[INFO] Usunięto katalogów: ' . $deletedDirs; + } + + // 7. Rozpakowywanie ZIP + $log = $this->extractZip( 'update.zip', $log ); + + // 8. Aktualizacja wersji + $versionFile = '../libraries/version.ini'; + $handle = @fopen( $versionFile, 'w' ); + if ( !$handle ) { + $log[] = '[ERROR] Nie udało się otworzyć pliku version.ini do zapisu'; + return [ 'success' => false, 'log' => $log ]; + } + fwrite( $handle, $ver ); + fclose( $handle ); + + $log[] = '[OK] Zaktualizowano plik version.ini do wersji: ' . $ver; + $log[] = '[SUCCESS] Aktualizacja do wersji ' . $ver . ' zakończona pomyślnie'; + + return [ 'success' => true, 'log' => $log ]; + } + + /** + * Weryfikuje sumę kontrolną pliku. + * + * @param string $filePath Ścieżka do pliku + * @param string $expectedChecksum Suma w formacie "sha256:abc123..." + * @param array $log Tablica logów + * @return array{valid: bool, log: array} + */ + private function verifyChecksum( string $filePath, string $expectedChecksum, array $log ): array + { + $parts = explode( ':', $expectedChecksum, 2 ); + if ( count( $parts ) !== 2 ) { + $log[] = '[ERROR] Nieprawidłowy format sumy kontrolnej: ' . $expectedChecksum; + return [ 'valid' => false, 'log' => $log ]; + } + + $algorithm = $parts[0]; + $expected = $parts[1]; + + $actual = @hash_file( $algorithm, $filePath ); + + if ( $actual === false ) { + $log[] = '[ERROR] Nie udało się obliczyć sumy kontrolnej pliku'; + return [ 'valid' => false, 'log' => $log ]; + } + + if ( $actual !== $expected ) { + $log[] = '[ERROR] Suma kontrolna nie zgadza się! Oczekiwano: ' . $expected . ', otrzymano: ' . $actual; + return [ 'valid' => false, 'log' => $log ]; + } + + $log[] = '[OK] Suma kontrolna ZIP zgodna'; + return [ 'valid' => true, 'log' => $log ]; + } + + /** + * Tworzy kopię zapasową plików przed aktualizacją. + * + * @param array $manifest Dane z manifestu + * @param array $log Tablica logów + * @return array Zaktualizowana tablica logów + */ + private function createBackup( array $manifest, array $log ): array + { + $version = isset( $manifest['version'] ) ? $manifest['version'] : 'unknown'; + $backupDir = '../backups/' . str_replace( '.', '_', $version ) . '_' . date( 'Ymd_His' ); + + $log[] = '[INFO] Tworzenie kopii zapasowej w: ' . $backupDir; + + $projectRoot = realpath( '../' ); + if ( !$projectRoot ) { + $log[] = '[WARNING] Nie udało się określić katalogu projektu, pomijam backup'; + return $log; + } + + $filesToBackup = []; + if ( isset( $manifest['files']['modified'] ) && is_array( $manifest['files']['modified'] ) ) { + $filesToBackup = array_merge( $filesToBackup, $manifest['files']['modified'] ); + } + if ( isset( $manifest['files']['deleted'] ) && is_array( $manifest['files']['deleted'] ) ) { + $filesToBackup = array_merge( $filesToBackup, $manifest['files']['deleted'] ); + } + + if ( empty( $filesToBackup ) ) { + $log[] = '[INFO] Brak plików do backupu'; + return $log; + } + + $backedUp = 0; + foreach ( $filesToBackup as $relativePath ) { + $sourcePath = $projectRoot . '/' . $relativePath; + if ( !file_exists( $sourcePath ) ) { + continue; + } + + $targetPath = $backupDir . '/' . $relativePath; + $targetDir = dirname( $targetPath ); + + if ( !is_dir( $targetDir ) ) { + @mkdir( $targetDir, 0755, true ); + } + + if ( @copy( $sourcePath, $targetPath ) ) { + $backedUp++; + } else { + $log[] = '[WARNING] Nie udało się skopiować do backupu: ' . $relativePath; + } + } + + $log[] = '[OK] Backup: skopiowano ' . $backedUp . ' plików'; + + @file_put_contents( + $backupDir . '/manifest.json', + json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ) + ); + + return $log; + } + + /** + * Legacy — stary format aktualizacji (ZIP + _sql.txt + _files.txt). + */ + private function downloadAndApplyLegacy( string $ver, string $dir, array $log ): array + { + $baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir; + // Pobieranie ZIP $zipUrl = $baseUrl . '/ver_' . $ver . '.zip'; $log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl; diff --git a/autoload/admin/Controllers/UpdateController.php b/autoload/admin/Controllers/UpdateController.php index 6e6c77d..f3f7d53 100644 --- a/autoload/admin/Controllers/UpdateController.php +++ b/autoload/admin/Controllers/UpdateController.php @@ -14,9 +14,12 @@ class UpdateController public function main_view(): string { + $logContent = @file_get_contents( '../libraries/update_log.txt' ); + return \Shared\Tpl\Tpl::view( 'update/main-view', [ 'ver' => \Shared\Helpers\Helpers::get_version(), 'new_ver' => \Shared\Helpers\Helpers::get_new_version(), + 'log' => $logContent ?: '', ] ); } diff --git a/build-update.ps1 b/build-update.ps1 new file mode 100644 index 0000000..eafad8f --- /dev/null +++ b/build-update.ps1 @@ -0,0 +1,388 @@ +<# +.SYNOPSIS + Automatyczne budowanie paczki aktualizacji shopPRO na podstawie git diff miedzy tagami. + +.DESCRIPTION + Skrypt porownuje dwa tagi git, filtruje pliki przez .updateignore, + tworzy ZIP + manifest JSON i aktualizuje changelog.php oraz versions.php. + +.PARAMETER FromTag + Tag poczatkowy (np. v0.299). Domyslnie: ostatni tag. + +.PARAMETER ToTag + Tag docelowy (np. v0.300). Wymagany. + +.PARAMETER ChangelogEntry + Wpis do changelogu. Wymagany (chyba ze -DryRun). + +.PARAMETER DryRun + Tylko pokaz co zostaloby zrobione, bez tworzenia plikow. + +.EXAMPLE + ./build-update.ps1 -ToTag v0.300 -ChangelogEntry "NEW - Manifest-based update system" + ./build-update.ps1 -FromTag v0.299 -ToTag v0.300 -ChangelogEntry "NEW - opis" -DryRun +#> + +param( + [string]$FromTag = "", + [Parameter(Mandatory=$true)] + [string]$ToTag, + [string]$ChangelogEntry = "", + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" + +# --- Helpers --- + +function Write-Step($msg) { + Write-Host " [*] $msg" -ForegroundColor Cyan +} + +function Write-Ok($msg) { + Write-Host " [OK] $msg" -ForegroundColor Green +} + +function Write-Warn($msg) { + Write-Host " [!] $msg" -ForegroundColor Yellow +} + +function Write-Err($msg) { + Write-Host " [ERROR] $msg" -ForegroundColor Red +} + +# --- 1. Walidacja tagow --- + +Write-Host "`n=== shopPRO Build Update ===" -ForegroundColor White + +if (-not $FromTag) { + $FromTag = (git describe --tags --abbrev=0 2>$null) + if (-not $FromTag) { + Write-Err "Nie znaleziono zadnego taga. Uzyj parametru -FromTag." + exit 1 + } + Write-Step "Auto-detect FromTag: $FromTag" +} + +# Sprawdz czy tagi istnieja +$tagExists = git tag -l $FromTag +if (-not $tagExists) { + Write-Err "Tag '$FromTag' nie istnieje." + exit 1 +} + +$toTagExists = git tag -l $ToTag +if (-not $toTagExists) { + Write-Warn "Tag '$ToTag' nie istnieje. Uzywam HEAD jako punktu docelowego." + $diffTarget = "HEAD" +} else { + $diffTarget = $ToTag +} + +# --- 2. Wersja i katalog --- + +$versionNumber = $ToTag -replace '^v', '' +$versionInt = [int]($versionNumber -replace '^0\.', '') + +# Oblicz katalog: 0.001-0.009 -> 0.00, 0.010-0.099 -> 0.00, 0.100-0.199 -> 0.10, 0.200-0.299 -> 0.20, 0.300-0.399 -> 0.30 +$dirTens = [math]::Floor($versionInt / 100) +$dirStr = "0.{0}0" -f ($dirTens * 10).ToString().PadLeft(1, '0') + +# Format: jesli dirTens < 10 to "0.X0", np 0.00, 0.10, 0.20, 0.30 +if ($dirTens -lt 10) { + $dirStr = "0.{0}0" -f $dirTens.ToString().PadLeft(1, '0') +} else { + $dirStr = "0.{0}0" -f $dirTens +} + +Write-Step "Wersja: $versionNumber (int: $versionInt)" +Write-Step "Katalog: updates/$dirStr/" + +# --- 3. Git diff --- + +Write-Step "Porownywanie: $FromTag..$diffTarget" + +$diffOutput = git diff --name-status "$FromTag..$diffTarget" 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Err "git diff nie powiodl sie: $diffOutput" + exit 1 +} + +$addedFiles = @() +$modifiedFiles = @() +$deletedFiles = @() +$renamedFiles = @() + +foreach ($line in ($diffOutput -split "`n")) { + $line = $line.Trim() + if (-not $line) { continue } + + $parts = $line -split "`t" + $status = $parts[0] + $filePath = if ($parts.Count -gt 1) { $parts[1] } else { "" } + + # Zamien backslash na slash + $filePath = $filePath -replace '\\', '/' + + switch -Wildcard ($status) { + "A" { $addedFiles += $filePath } + "M" { $modifiedFiles += $filePath } + "D" { $deletedFiles += $filePath } + "R*" { + # Rename: stary plik = deleted, nowy = added + $newPath = if ($parts.Count -gt 2) { $parts[2] -replace '\\', '/' } else { "" } + $deletedFiles += $filePath + if ($newPath) { $addedFiles += $newPath } + } + } +} + +Write-Step "Zmiany: A=$($addedFiles.Count), M=$($modifiedFiles.Count), D=$($deletedFiles.Count)" + +# --- 4. Filtrowanie przez .updateignore --- + +$ignorePatterns = @() +$ignoreFile = ".updateignore" + +if (Test-Path $ignoreFile) { + $ignorePatterns = Get-Content $ignoreFile | ForEach-Object { $_.Trim() } | Where-Object { $_ -and ($_ -notmatch '^\s*#') } + Write-Step "Zaladowano $($ignorePatterns.Count) wzorcow z .updateignore" +} + +function Test-Ignored { + param([string]$FilePath) + + foreach ($pattern in $ignorePatterns) { + # Wzorzec katalogu (konczy sie na /) + if ($pattern.EndsWith('/')) { + $dirPattern = $pattern.TrimEnd('/') + if ($FilePath -like "$dirPattern/*" -or $FilePath -eq $dirPattern) { + return $true + } + } + # Wzorzec z wildcard + elseif ($pattern.Contains('*')) { + # *.md -> dopasuj nazwe pliku + if ($pattern.StartsWith('*')) { + $fileName = Split-Path $FilePath -Leaf + if ($fileName -like $pattern) { return $true } + } + # Zwykly glob + elseif ($FilePath -like $pattern) { return $true } + } + # Dokladne dopasowanie + else { + if ($FilePath -eq $pattern) { return $true } + } + } + return $false +} + +$filteredAdded = @() +$filteredModified = @() +$filteredDeleted = @() +$ignoredCount = 0 + +foreach ($f in $addedFiles) { + if (Test-Ignored $f) { $ignoredCount++; continue } + $filteredAdded += $f +} +foreach ($f in $modifiedFiles) { + if (Test-Ignored $f) { $ignoredCount++; continue } + $filteredModified += $f +} +foreach ($f in $deletedFiles) { + if (Test-Ignored $f) { $ignoredCount++; continue } + $filteredDeleted += $f +} + +Write-Step "Po filtrowaniu: A=$($filteredAdded.Count), M=$($filteredModified.Count), D=$($filteredDeleted.Count) (pominieto: $ignoredCount)" + +# Rozdziel usuniete pliki i katalogi +$deletedDirs = @() +$deletedFilesOnly = @() + +foreach ($f in $filteredDeleted) { + # Sprawdz czy to byl katalog (w git nie ma katalogow, ale mozemy sprawdzic po wzorcu) + # Git nie trackuje pustych katalogow, wiec deleted entries to zawsze pliki + $deletedFilesOnly += $f +} + +# --- 5. Odczyt migracji SQL --- + +$sqlQueries = @() +$migrationFile = "migrations/$versionNumber.sql" + +if (Test-Path $migrationFile) { + $sqlQueries = Get-Content $migrationFile | Where-Object { $_.Trim() -ne '' } + Write-Step "Znaleziono migracje SQL: $migrationFile ($($sqlQueries.Count) zapytan)" +} else { + Write-Step "Brak migracji SQL ($migrationFile nie istnieje)" +} + +# --- 6. Podsumowanie --- + +$filesToPack = $filteredAdded + $filteredModified + +if ($filesToPack.Count -eq 0 -and $filteredDeleted.Count -eq 0 -and $sqlQueries.Count -eq 0) { + Write-Warn "Brak zmian do pakowania (po filtrowaniu). Przerywam." + exit 0 +} + +Write-Host "`n--- Pliki do paczki ---" -ForegroundColor White +foreach ($f in $filteredAdded) { Write-Host " A $f" -ForegroundColor Green } +foreach ($f in $filteredModified) { Write-Host " M $f" -ForegroundColor Yellow } +foreach ($f in $filteredDeleted) { Write-Host " D $f" -ForegroundColor Red } + +if ($sqlQueries.Count -gt 0) { + Write-Host "`n--- SQL ---" -ForegroundColor White + foreach ($q in $sqlQueries) { Write-Host " $q" -ForegroundColor Gray } +} + +# --- DryRun? --- + +if ($DryRun) { + Write-Host "`n[DRY RUN] Zadne pliki nie zostaly utworzone.`n" -ForegroundColor Magenta + exit 0 +} + +# --- 7. Walidacja ChangelogEntry --- + +if (-not $ChangelogEntry) { + Write-Err "Parametr -ChangelogEntry jest wymagany (chyba ze uzywasz -DryRun)." + exit 1 +} + +# --- 8. Tworzenie temp i kopiowanie plikow --- + +$tempDir = "temp/temp_$versionInt" + +if (Test-Path $tempDir) { + Remove-Item -Recurse -Force $tempDir +} + +foreach ($f in $filesToPack) { + $destPath = Join-Path $tempDir $f + $destDir = Split-Path $destPath -Parent + + if (-not (Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + } + + if (Test-Path $f) { + Copy-Item $f $destPath -Force + } else { + Write-Warn "Plik nie istnieje (moze zostal usuniety po TAGU): $f" + } +} + +Write-Ok "Skopiowano $($filesToPack.Count) plikow do $tempDir" + +# --- 9. Tworzenie ZIP --- + +$updatesDir = "updates/$dirStr" +if (-not (Test-Path $updatesDir)) { + New-Item -ItemType Directory -Path $updatesDir -Force | Out-Null +} + +$zipPath = "$updatesDir/ver_$versionNumber.zip" + +if (Test-Path $zipPath) { + Remove-Item $zipPath -Force +} + +# Pakuj zawartosc temp dir (bez folderu temp/) +$originalLocation = Get-Location +Set-Location $tempDir +Compress-Archive -Path '*' -DestinationPath "../../$zipPath" -Force +Set-Location $originalLocation + +Write-Ok "Utworzono ZIP: $zipPath" + +# --- 10. Checksum SHA256 --- + +$hash = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower() +Write-Ok "SHA256: $hash" + +# --- 11. Manifest JSON --- + +$manifest = @{ + version = $versionNumber + date = (Get-Date -Format "yyyy-MM-dd") + checksum_zip = "sha256:$hash" + files = @{ + modified = $filteredModified + added = $filteredAdded + deleted = $deletedFilesOnly + } + directories_deleted = $deletedDirs + sql = $sqlQueries + changelog = $ChangelogEntry +} + +$manifestJson = $manifest | ConvertTo-Json -Depth 4 +$manifestPath = "$updatesDir/ver_${versionNumber}_manifest.json" +$manifestJson | Out-File $manifestPath -Encoding UTF8 + +Write-Ok "Utworzono manifest: $manifestPath" + +# --- 12. Legacy _sql.txt i _files.txt (okres przejsciowy) --- + +if ($sqlQueries.Count -gt 0) { + $sqlPath = "$updatesDir/ver_${versionNumber}_sql.txt" + ($sqlQueries -join "`n") | Out-File $sqlPath -Encoding UTF8 -NoNewline + Write-Ok "Utworzono legacy SQL: $sqlPath" +} + +if ($deletedFilesOnly.Count -gt 0 -or $deletedDirs.Count -gt 0) { + $filesContent = @() + foreach ($f in $deletedFilesOnly) { $filesContent += "F: ../$f" } + foreach ($d in $deletedDirs) { $filesContent += "D: ../$d" } + + $filesPath = "$updatesDir/ver_${versionNumber}_files.txt" + ($filesContent -join "`n") | Out-File $filesPath -Encoding UTF8 -NoNewline + Write-Ok "Utworzono legacy files: $filesPath" +} + +# --- 13. Aktualizacja versions.php --- + +$versionsFile = "updates/versions.php" +if (Test-Path $versionsFile) { + $content = Get-Content $versionsFile -Raw + $content = $content -replace '\$current_ver\s*=\s*\d+;', "`$current_ver = $versionInt;" + $content | Out-File $versionsFile -Encoding UTF8 -NoNewline + Write-Ok "Zaktualizowano versions.php: `$current_ver = $versionInt" +} + +# --- 14. Aktualizacja changelog.php --- + +$changelogFile = "updates/changelog.php" +if (Test-Path $changelogFile) { + $dateStr = Get-Date -Format "dd.MM.yyyy" + $newEntry = "ver. $versionNumber - $dateStr
`n$ChangelogEntry`n
`n" + + $changelogContent = Get-Content $changelogFile -Raw + $changelogContent = $newEntry + $changelogContent + $changelogContent | Out-File $changelogFile -Encoding UTF8 -NoNewline + Write-Ok "Zaktualizowano changelog.php" +} + +# --- 15. Cleanup --- + +if (Test-Path $tempDir) { + Remove-Item -Recurse -Force $tempDir +} +# Usun pusty folder temp jesli nie ma juz w nim nic +if ((Test-Path "temp") -and ((Get-ChildItem "temp" -Force).Count -eq 0)) { + Remove-Item "temp" -Force +} + +Write-Ok "Wyczyszczono pliki tymczasowe" + +# --- Podsumowanie --- + +Write-Host "`n=== Gotowe ===" -ForegroundColor Green +Write-Host " ZIP: $zipPath" +Write-Host " Manifest: $manifestPath" +Write-Host " Wersja: $versionNumber (int: $versionInt)" +Write-Host "" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8c49c4e..7233535 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,19 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.300 (2026-02-21) - System aktualizacji oparty na manifestach JSON + +- **NEW**: Manifest JSON per wersja — zastępuje osobne pliki `_sql.txt` i `_files.txt` +- **NEW**: Weryfikacja checksum SHA256 pobranych paczek ZIP +- **NEW**: Automatyczny backup plików przed nadpisaniem (`backups/` directory) +- **NEW**: `build-update.ps1` — automatyczne budowanie paczek z `git diff` między tagami +- **NEW**: `.updateignore` — wzorce plików wykluczonych z paczek aktualizacji +- **NEW**: `migrations/` — folder na wersjonowane pliki SQL +- **NEW**: Panel "Log ostatniej aktualizacji" w widoku aktualizacji admina +- **UPDATE**: `UpdateRepository` — dual-mode dispatcher (manifest + legacy fallback) + +--- + ## ver. 0.299 (2026-02-21) - Widoczność kolumn w tabelach - **NEW**: Toggle widoczności kolumn w komponentach `table-list` — przycisk z ikoną kolumn, dropdown z toggle switchami diff --git a/docs/TESTING.md b/docs/TESTING.md index d742d2b..fb05cdc 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -23,10 +23,10 @@ composer test # standard ## Aktualny stan ```text -OK (687 tests, 1971 assertions) +OK (692 tests, 1988 assertions) ``` -Zweryfikowano: 2026-02-19 (ver. 0.297) +Zweryfikowano: 2026-02-21 (ver. 0.300) ## Konfiguracja @@ -66,6 +66,7 @@ tests/ | | |-- Settings/SettingsRepositoryTest.php | | |-- ShopStatus/ShopStatusRepositoryTest.php | | |-- Transport/TransportRepositoryTest.php +| | |-- Update/UpdateRepositoryTest.php | | `-- User/UserRepositoryTest.php | `-- admin/ | `-- Controllers/ diff --git a/docs/UPDATE_INSTRUCTIONS.md b/docs/UPDATE_INSTRUCTIONS.md index d67b986..a155334 100644 --- a/docs/UPDATE_INSTRUCTIONS.md +++ b/docs/UPDATE_INSTRUCTIONS.md @@ -1,136 +1,113 @@ # Instrukcja tworzenia aktualizacji shopPRO -## Struktura aktualizacji +## Nowy sposób (od v0.301) — automatyczny build script + +### Wymagania +- Git z tagami wersji (np. `v0.299`, `v0.300`) +- PowerShell + +### Workflow + +``` +1. Pracuj normalnie: commit, push, commit, push... +2. Gdy wersja gotowa: + → git tag v0.XXX + → ./build-update.ps1 -ToTag v0.XXX -ChangelogEntry "NEW - opis" +3. Upload plików z updates/0.XX/ na serwer aktualizacji +``` + +### Użycie build-update.ps1 + +```powershell +# Podgląd zmian (bez tworzenia plików) +./build-update.ps1 -ToTag v0.301 -DryRun + +# Budowanie paczki (auto-detect poprzedniego tagu) +./build-update.ps1 -ToTag v0.301 -ChangelogEntry "NEW - opis zmiany" + +# Z jawnym tagiem źródłowym +./build-update.ps1 -FromTag v0.300 -ToTag v0.301 -ChangelogEntry "NEW - opis" +``` + +### Co robi skrypt automatycznie +1. `git diff --name-status` między tagami → listy dodanych/zmodyfikowanych/usuniętych plików +2. Filtrowanie przez `.updateignore` (pliki deweloperskie, konfiguracyjne itp.) +3. Kopiowanie plików do temp, tworzenie ZIP +4. SHA256 checksum ZIP-a +5. Generowanie `ver_X.XXX_manifest.json` +6. Generowanie legacy `_sql.txt` i `_files.txt` (okres przejściowy) +7. Aktualizacja `versions.php` i `changelog.php` +8. Cleanup + +### Pliki wynikowe +- `updates/0.XX/ver_X.XXX.zip` — paczka z plikami +- `updates/0.XX/ver_X.XXX_manifest.json` — manifest z checksumem, listą zmian, SQL +- `updates/0.XX/ver_X.XXX_sql.txt` — legacy SQL (okres przejściowy) +- `updates/0.XX/ver_X.XXX_files.txt` — legacy lista plików do usunięcia (okres przejściowy) + +### Migracje SQL +Pliki SQL umieszczaj w `migrations/{version}.sql` (np. `migrations/0.301.sql`). +Build script automatycznie je wczyta i umieści w manifeście + legacy `_sql.txt`. + +### Format manifestu +```json +{ + "version": "0.301", + "date": "2026-02-22", + "checksum_zip": "sha256:abc123...", + "files": { + "modified": ["autoload/Domain/Order/OrderRepository.php"], + "added": ["autoload/Domain/Order/NewHelper.php"], + "deleted": ["autoload/shop/OldClass.php"] + }, + "directories_deleted": [], + "sql": ["ALTER TABLE pp_x ADD COLUMN y INT DEFAULT 0"], + "changelog": "NEW - opis zmiany" +} +``` + +### .updateignore +Plik w katalogu głównym projektu, wzorce plików wykluczonych z paczek (jak `.gitignore`). + +--- + +## Stary sposób (do v0.300) — ręczne pakowanie + +### Struktura aktualizacji Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesiątki wersji. -### Pliki aktualizacji: +#### Pliki aktualizacji: - `ver_X.XXX.zip` - paczka ZIP ze zmienionymi plikami (BEZ folderu wersji, bezpośrednio struktura katalogów) - `ver_X.XXX_sql.txt` - opcjonalny plik z zapytaniami SQL (jeśli wymagane zmiany w bazie) - `ver_X.XXX_files.txt` - opcjonalny plik z listą plików do **USUNIĘCIA** przy aktualizacji (format: `F: ../sciezka/do/pliku.php`) - `changelog.php` - historia zmian - `versions.php` - konfiguracja wersji (zmienna `$current_ver`) -### Zasada pakowania plików +#### Zasada pakowania plików - Do paczek aktualizacji **nie dodajemy plików `*.md`** (dokumentacja jest tylko wewnętrzna/deweloperska). - Do paczek aktualizacji **nie dodajemy `updates/changelog.php`** (to plik serwisowy po stronie repozytorium aktualizacji, nie runtime klienta). - Do paczek aktualizacji **nie dodajemy głównego `.htaccess` z katalogu projektu** (ten plik wdrażamy osobno, poza ZIP aktualizacji). -## Procedura tworzenia nowej aktualizacji +### Procedura ręczna -## Status biezacej aktualizacji (ver. 0.299) +1. Określ numer wersji +2. Utwórz folder tymczasowy: `mkdir -p temp/temp_XXX/sciezka/do/pliku` +3. Skopiuj zmienione pliki do folderu tymczasowego +4. Utwórz ZIP z zawartości folderu (nie z samego folderu!) +5. Usuń folder tymczasowy +6. Zaktualizuj `changelog.php` i `versions.php` +7. (Opcjonalnie) Utwórz `_sql.txt` i `_files.txt` -- Wersja udostepniona: `0.299` (data: 2026-02-21). +**WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji. Struktura ZIP zaczyna się bezpośrednio od katalogów projektu (admin/, autoload/, itp.). + +## Status bieżącej aktualizacji (ver. 0.300) + +- Wersja udostępniona: `0.300` (data: 2026-02-21). - Pliki publikacyjne: - - `updates/0.20/ver_0.299.zip` + - `updates/0.30/ver_0.300.zip` - Pliki metadanych aktualizacji: - `updates/changelog.php` - - `updates/versions.php` (`$current_ver = 299`) -- Weryfikacja testow przed publikacja: - - `OK (687 tests, 1971 assertions)` - -### 1. Określ numer wersji -Sprawdź ostatnią wersję w `updates/` i zwiększ o 1. - -### 2. Utwórz folder tymczasowy ze strukturą w katalogu temp -```bash -mkdir -p temp/temp_XXX/sciezka/do/pliku -``` - -**WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji (np. ver_0.234/). -Struktura ZIP powinna zaczynać się bezpośrednio od katalogów projektu (admin/, autoload/, itp.). - -### 3. Skopiuj zmienione pliki do folderu tymczasowego -```bash -cp sciezka/do/pliku.php temp/temp_XXX/sciezka/do/pliku.php -``` - -### 4. Utwórz plik ZIP z zawartości folderu (nie z samego folderu!) -```powershell -cd temp/temp_XXX -powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_X.XXX.zip' -Force" -``` - -### 5. Usuń folder tymczasowy -```bash -rm -rf temp/temp_XXX -``` - -### 6. Zaktualizuj changelog.php -Dodaj wpis na początku pliku: -```html -ver. X.XXX - DD.MM.YYYY
-- NEW/FIX/UPDATE - opis zmiany -
-``` - -Prefiksy: -- `NEW` - nowa funkcjonalność -- `FIX` - naprawa błędu -- `UPDATE` - aktualizacja istniejącej funkcjonalności - -### 7. Zaktualizuj versions.php -Zmień wartość `$current_ver` na nowy numer wersji (bez przedrostka 0.): -```php -$current_ver = 234; // dla wersji 0.234 -``` - -### 8. (Opcjonalnie) Utwórz plik SQL -Jeśli aktualizacja wymaga zmian w bazie danych, utwórz plik `ver_X.XXX_sql.txt` z zapytaniami SQL. - -### 9. (Opcjonalnie) Utwórz plik z listą plików do usunięcia -Jeśli aktualizacja wymaga usunięcia przestarzałych plików, utwórz plik `ver_X.XXX_files.txt`: -``` -F: ../sciezka/do/pliku1.php -F: ../sciezka/do/pliku2.php -``` -**UWAGA:** Pliki wymienione w tym pliku zostaną USUNIĘTE z systemu podczas aktualizacji. - -## Przykład - aktualizacja 0.234 - -Zmienione pliki: -- `autoload/admin/controls/class.ShopOrder.php` -- `admin/templates/shop-order/order-details.php` - -Opis: Dodanie przycisku do zaznaczania zamówienia jako wysłane do trustmate.io - -### Komendy: - -```bash -# Utwórz strukturę w folderze tymczasowym -mkdir -p temp/temp_234/autoload/admin/controls -mkdir -p temp/temp_234/admin/templates/shop-order - -# Skopiuj pliki -cp autoload/admin/controls/class.ShopOrder.php temp/temp_234/autoload/admin/controls/ -cp admin/templates/shop-order/order-details.php temp/temp_234/admin/templates/shop-order/ - -# Utwórz ZIP z ZAWARTOŚCI folderu (ważne: wejdź do folderu i spakuj '*') -cd temp/temp_234 -powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_0.234.zip' -Force" - -# Wróć i usuń folder tymczasowy -cd .. -rm -rf temp_234 -``` - -### Poprawna struktura ZIP: -``` -ver_0.234.zip -├── admin/ -│ └── templates/ -│ └── shop-order/ -│ └── order-details.php -└── autoload/ - └── admin/ - └── controls/ - └── class.ShopOrder.php -``` - -### NIEPOPRAWNA struktura (do uniknięcia): -``` -ver_0.234.zip -└── ver_0.234/ <-- tego folderu NIE powinno być! - ├── admin/ - └── autoload/ -``` + - `updates/versions.php` (`$current_ver = 300`) +- Weryfikacja testów przed publikacją: + - `OK (692 tests, 1988 assertions)` diff --git a/changes b/migrations/.gitkeep similarity index 100% rename from changes rename to migrations/.gitkeep diff --git a/test-debug.bat b/test-debug.bat deleted file mode 100644 index 7a8da3c..0000000 --- a/test-debug.bat +++ /dev/null @@ -1,13 +0,0 @@ -@echo off -REM Skrypt do uruchamiania testów z PEŁNYMI szczegółami - -echo. -echo ================================ -echo Testy DEBUG - pełne szczegóły -echo ================================ -echo. - -C:\xampp\php\php.exe phpunit.phar --debug %* - -echo. -pause diff --git a/test-simple.bat b/test-simple.bat deleted file mode 100644 index 6277f71..0000000 --- a/test-simple.bat +++ /dev/null @@ -1,13 +0,0 @@ -@echo off -REM Skrypt do uruchamiania testów - tylko kropki - -echo. -echo ================================ -echo Testy jednostkowe shopPRO -echo ================================ -echo. - -C:\xampp\php\php.exe phpunit.phar %* - -echo. -pause diff --git a/test.bat b/test.bat deleted file mode 100644 index 4708548..0000000 --- a/test.bat +++ /dev/null @@ -1,13 +0,0 @@ -@echo off -REM Skrypt do uruchamiania testów PHPUnit - -echo. -echo ================================ -echo Testy jednostkowe shopPRO -echo ================================ -echo. - -C:\xampp\php\php.exe phpunit.phar --testdox %* - -echo. -pause diff --git a/test.ps1 b/test.ps1 deleted file mode 100644 index 66f3152..0000000 --- a/test.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -Param( - [Parameter(ValueFromRemainingArguments = $true)] - [string[]]$PhpUnitArgs -) - -$ErrorActionPreference = "Stop" - -function Resolve-PhpExe { - $cmd = Get-Command php -ErrorAction SilentlyContinue - if ($cmd -and $cmd.Source) { - return $cmd.Source - } - - $candidates = @( - "C:\xampp\php\php.exe", - "C:\php\php.exe", - "C:\Program Files\PHP\php.exe" - ) - - foreach ($candidate in $candidates) { - if (Test-Path $candidate) { - return $candidate - } - } - - throw "Nie znaleziono interpretera PHP. Dodaj php do PATH albo zainstaluj PHP (np. XAMPP)." -} - -$phpExe = Resolve-PhpExe -$phpUnitPhar = Join-Path $PSScriptRoot "phpunit.phar" - -if (-not (Test-Path $phpUnitPhar)) { - throw "Brak pliku phpunit.phar w katalogu projektu: $PSScriptRoot" -} - -$args = @($phpUnitPhar, "--do-not-cache-result") + $PhpUnitArgs - -Write-Host "" -Write-Host "================================" -Write-Host " Testy jednostkowe shopPRO" -Write-Host "================================" -Write-Host "PHP: $phpExe" -Write-Host "Cmd: $phpExe $($args -join ' ')" -Write-Host "" - -& $phpExe @args -exit $LASTEXITCODE diff --git a/test.sh b/test.sh deleted file mode 100644 index ab3fc7f..0000000 --- a/test.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# Skrypt do uruchamiania testów PHPUnit - -echo "" -echo "================================" -echo " Testy jednostkowe shopPRO" -echo "================================" -echo "" - -/c/xampp/php/php.exe phpunit.phar "$@" diff --git a/tests/Unit/Domain/Update/UpdateRepositoryTest.php b/tests/Unit/Domain/Update/UpdateRepositoryTest.php index 8aac25a..5a21841 100644 --- a/tests/Unit/Domain/Update/UpdateRepositoryTest.php +++ b/tests/Unit/Domain/Update/UpdateRepositoryTest.php @@ -50,7 +50,7 @@ class UpdateRepositoryTest extends TestCase $repository = new UpdateRepository($db); $repository->runPendingMigrations(); - $this->assertTrue(true); // No exception thrown + $this->assertTrue(true); } public function testHasPrivateHelperMethods(): void @@ -59,6 +59,11 @@ class UpdateRepositoryTest extends TestCase $privateMethods = [ 'downloadAndApply', + 'downloadAndApplyLegacy', + 'downloadAndApplyWithManifest', + 'downloadManifest', + 'verifyChecksum', + 'createBackup', 'executeSql', 'deleteFiles', 'extractZip', @@ -77,4 +82,99 @@ class UpdateRepositoryTest extends TestCase ); } } + + public function testVerifyChecksumValidFormat(): void + { + $db = $this->createMockDb(); + $repository = new UpdateRepository($db); + + $reflection = new \ReflectionClass($repository); + $method = $reflection->getMethod('verifyChecksum'); + $method->setAccessible(true); + + // Create a temp file with known content + $tmpFile = tempnam(sys_get_temp_dir(), 'test_checksum_'); + file_put_contents($tmpFile, 'test content for checksum'); + + $expectedHash = hash_file('sha256', $tmpFile); + $result = $method->invoke($repository, $tmpFile, 'sha256:' . $expectedHash, []); + + $this->assertTrue($result['valid']); + $this->assertNotEmpty($result['log']); + + @unlink($tmpFile); + } + + public function testVerifyChecksumInvalidHash(): void + { + $db = $this->createMockDb(); + $repository = new UpdateRepository($db); + + $reflection = new \ReflectionClass($repository); + $method = $reflection->getMethod('verifyChecksum'); + $method->setAccessible(true); + + $tmpFile = tempnam(sys_get_temp_dir(), 'test_checksum_'); + file_put_contents($tmpFile, 'test content'); + + $result = $method->invoke($repository, $tmpFile, 'sha256:invalidhash', []); + + $this->assertFalse($result['valid']); + + @unlink($tmpFile); + } + + public function testVerifyChecksumInvalidFormat(): void + { + $db = $this->createMockDb(); + $repository = new UpdateRepository($db); + + $reflection = new \ReflectionClass($repository); + $method = $reflection->getMethod('verifyChecksum'); + $method->setAccessible(true); + + $result = $method->invoke($repository, '/tmp/nonexistent', 'badformat', []); + + $this->assertFalse($result['valid']); + } + + public function testCreateBackupWithEmptyManifest(): void + { + $db = $this->createMockDb(); + $repository = new UpdateRepository($db); + + $reflection = new \ReflectionClass($repository); + $method = $reflection->getMethod('createBackup'); + $method->setAccessible(true); + + $manifest = [ + 'version' => '0.999', + 'files' => [], + ]; + + $log = $method->invoke($repository, $manifest, []); + + $this->assertIsArray($log); + $hasBackupInfo = false; + foreach ($log as $entry) { + if (strpos($entry, 'Brak plików do backupu') !== false) { + $hasBackupInfo = true; + } + } + $this->assertTrue($hasBackupInfo); + } + + public function testDownloadManifestReturnsNullForInvalidUrl(): void + { + $db = $this->createMockDb(); + $repository = new UpdateRepository($db); + + $reflection = new \ReflectionClass($repository); + $method = $reflection->getMethod('downloadManifest'); + $method->setAccessible(true); + + $result = $method->invoke($repository, 'http://invalid.nonexistent.test', '0.999'); + + $this->assertNull($result); + } } diff --git a/updates/0.30/ver_0.300.zip b/updates/0.30/ver_0.300.zip new file mode 100644 index 0000000000000000000000000000000000000000..6155f1bd92b7ff4ca9fbea467cbb5a13a44d70ab GIT binary patch literal 7357 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 S4FG`h>*zxO0CvfKd-@;Yf`8ut literal 0 HcmV?d00001 diff --git a/updates/changelog.php b/updates/changelog.php index 702735d..d98e044 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,7 @@ +ver. 0.300 - 21.02.2026
+- NEW - System aktualizacji oparty na manifestach JSON (checksum SHA256, backup plików, automatyczny build) +- NEW - Panel logu aktualizacji w panelu admina +
ver. 0.299 - 21.02.2026
- NEW - Ukrywanie/pokazywanie kolumn w tabelach admina (toggle switch + localStorage)
diff --git a/updates/versions.php b/updates/versions.php index 168f4a3..4d99971 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@