db = $db; } /** * Wykonuje aktualizację do następnej wersji. * * @return array{success: bool, log: array, no_updates?: bool} */ public function update(): array { global $settings; @file_put_contents( '../libraries/update_log.txt', '' ); $log = []; $log[] = '[START] Rozpoczęcie aktualizacji - ' . date( 'Y-m-d H:i:s' ); $log[] = '[INFO] Aktualna wersja: ' . \Shared\Helpers\Helpers::get_version(); \Shared\Helpers\Helpers::delete_session( 'new-version' ); $versionsUrl = 'https://shoppro.project-dc.pl/updates/versions.php?key=' . $settings['update_key']; $versions = @file_get_contents( $versionsUrl ); if ( $versions === false ) { $log[] = '[ERROR] Nie udało się pobrać listy wersji z: ' . $versionsUrl; $this->saveLog( $log ); return [ 'success' => false, 'log' => $log ]; } $log[] = '[OK] Pobrano listę wersji'; $versions = explode( PHP_EOL, $versions ); $log[] = '[INFO] Znaleziono ' . count( $versions ) . ' wersji do sprawdzenia'; foreach ( $versions as $ver ) { $ver = trim( $ver ); if ( floatval( $ver ) <= (float) \Shared\Helpers\Helpers::get_version() ) { continue; } $log[] = '[INFO] Aktualizacja do wersji: ' . $ver; $dir = strlen( $ver ) == 5 ? substr( $ver, 0, strlen( $ver ) - 2 ) . '0' : substr( $ver, 0, strlen( $ver ) - 1 ) . '0'; $result = $this->downloadAndApply( $ver, $dir, $log ); $this->saveLog( $result['log'] ); return $result; } $log[] = '[INFO] Brak nowych wersji do zainstalowania'; $this->saveLog( $log ); 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 === '' || strpos( $query, '--' ) === 0 ) { continue; } try { if ( $this->db->query( $query ) ) { $success++; } else { $errors++; $log[] = '[WARNING] Błąd SQL: ' . $query; } } catch ( \Exception $e ) { $errors++; $log[] = '[WARNING] Wyjątek SQL: ' . $e->getMessage() . ' | Query: ' . substr( $query, 0, 200 ); } } $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; $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'; $log[] = '[INFO] Katalog roboczy: ' . getcwd(); 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)'; // Wykonanie SQL $log = $this->executeSql( $baseUrl . '/ver_' . $ver . '_sql.txt', $log ); // Usuwanie plików $log = $this->deleteFiles( $baseUrl . '/ver_' . $ver . '_files.txt', $log ); // Rozpakowywanie ZIP $log = $this->extractZip( 'update.zip', $log ); // 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 ]; } private function executeSql( string $sqlUrl, array $log ): array { $log[] = '[INFO] Sprawdzanie aktualizacji SQL: ' . $sqlUrl; $ch = curl_init( $sqlUrl ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $ch, CURLOPT_HEADER, false ); $response = curl_exec( $ch ); $contentType = curl_getinfo( $ch, CURLINFO_CONTENT_TYPE ); $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); if ( !$response || strpos( $contentType, 'text/plain' ) === false ) { $log[] = '[INFO] Brak aktualizacji SQL (HTTP: ' . $httpCode . ')'; return $log; } // Usunięcie UTF-8 BOM i normalizacja końców linii $response = ltrim( $response, "\xEF\xBB\xBF" ); $response = str_replace( "\r\n", "\n", $response ); $response = str_replace( "\r", "\n", $response ); $queries = explode( "\n", $response ); $log[] = '[OK] Pobrano ' . count( $queries ) . ' zapytań SQL'; $success = 0; $errors = 0; foreach ( $queries as $query ) { $query = trim( $query ); if ( $query !== '' ) { if ( $this->db->query( $query ) ) { $success++; } else { $errors++; } } } $log[] = '[INFO] Wykonano zapytania SQL - sukces: ' . $success . ', błędy: ' . $errors; return $log; } private function deleteFiles( string $filesUrl, array $log ): array { $log[] = '[INFO] Sprawdzanie plików do usunięcia: ' . $filesUrl; $ch = curl_init( $filesUrl ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $ch, CURLOPT_HEADER, false ); $response = curl_exec( $ch ); $contentType = curl_getinfo( $ch, CURLINFO_CONTENT_TYPE ); curl_close( $ch ); if ( !$response || strpos( $contentType, 'text/plain' ) === false ) { $log[] = '[INFO] Brak plików do usunięcia'; return $log; } $files = explode( PHP_EOL, $response ); $deletedFiles = 0; $deletedDirs = 0; foreach ( $files as $entry ) { if ( strpos( $entry, 'F: ' ) !== false ) { $path = substr( $entry, 3 ); if ( file_exists( $path ) ) { if ( @unlink( $path ) ) { $deletedFiles++; } else { $log[] = '[WARNING] Nie udało się usunąć pliku: ' . $path; } } } if ( strpos( $entry, 'D: ' ) !== false ) { $path = substr( $entry, 3 ); if ( is_dir( $path ) ) { \Shared\Helpers\Helpers::delete_dir( $path ); $deletedDirs++; } } } $log[] = '[INFO] Usunięto plików: ' . $deletedFiles . ', katalogów: ' . $deletedDirs; return $log; } private function extractZip( string $fileName, array $log ): array { $log[] = '[INFO] Rozpoczęcie rozpakowywania pliku ZIP'; $path = pathinfo( realpath( $fileName ), PATHINFO_DIRNAME ); $path = substr( $path, 0, strlen( $path ) - 5 ); if ( !is_dir( $path ) || !is_writable( $path ) ) { $log[] = '[ERROR] Ścieżka docelowa nie istnieje lub brak uprawnień: ' . $path; return $log; } $zip = new \ZipArchive; $res = $zip->open( $fileName ); if ( $res !== true ) { $log[] = '[ERROR] Nie udało się otworzyć pliku ZIP (kod: ' . $res . ')'; return $log; } $log[] = '[OK] Otwarto archiwum ZIP, liczba plików: ' . $zip->numFiles; $extracted = 0; $errors = 0; for ( $i = 0; $i < $zip->numFiles; $i++ ) { $filename = str_replace( '\\', '/', $zip->getNameIndex( $i ) ); if ( substr( $filename, -1 ) === '/' ) { $dirPath = $path . '/' . $filename; if ( !is_dir( $dirPath ) ) { @mkdir( $dirPath, 0755, true ); } continue; } $targetFile = $path . '/' . $filename; $targetDir = dirname( $targetFile ); if ( !is_dir( $targetDir ) ) { @mkdir( $targetDir, 0755, true ); } $existed = file_exists( $targetFile ); $content = $zip->getFromIndex( $i ); if ( $content === false ) { $log[] = '[ERROR] Nie udało się odczytać z ZIP: ' . $filename; $errors++; continue; } if ( @file_put_contents( $targetFile, $content ) === false ) { $log[] = '[ERROR] Nie udało się zapisać: ' . $filename; $errors++; } else { $tag = $existed ? '[UPDATED]' : '[NEW]'; $log[] = $tag . ' ' . $filename . ' (' . strlen( $content ) . ' bajtów)'; $extracted++; } } $log[] = '[OK] Rozpakowano ' . $extracted . ' plików, błędów: ' . $errors; $zip->close(); if ( @unlink( $fileName ) ) { $log[] = '[OK] Usunięto plik update.zip'; } return $log; } private function saveLog( array $log ): void { @file_put_contents( '../libraries/update_log.txt', implode( "\n", $log ) ); } /** * Wykonuje zaległe migracje z tabeli pp_updates. */ public function runPendingMigrations(): void { $results = $this->db->select( 'pp_updates', [ 'name' ], [ 'done' => 0 ] ); if ( !is_array( $results ) ) { return; } foreach ( $results as $row ) { $method = $row['name']; if ( method_exists( $this, $method ) ) { $this->$method(); } } } public function update0197(): void { $rows = $this->db->select( 'pp_shop_order_products', [ 'id', 'product_id' ], [ 'parent_product_id' => null ] ); if ( is_array( $rows ) ) { foreach ( $rows as $row ) { $parentId = $this->db->get( 'pp_shop_products', 'parent_id', [ 'id' => $row['product_id'] ] ); $this->db->update( 'pp_shop_order_products', [ 'parent_product_id' => $parentId ?: $row['product_id'], ], [ 'id' => $row['id'] ] ); } } $this->db->update( 'pp_updates', [ 'done' => 1 ], [ 'name' => 'update0197' ] ); } }