loadConfiguration(); } public function isConfigured(): bool { return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true); } public function checkForUpdates(bool $recentOnly = true): array { if (! $this->githubRepo) { return [ 'has_updates' => false, 'error' => 'Geen GitHub repo geconfigureerd', ]; } $this->ensureSqlTableExists(); $result = $this->fetchSqlFilesFromGitHub($recentOnly); if (isset($result['error'])) { return [ 'has_updates' => false, 'error' => $result['error'], ]; } $sqlFiles = $result; if ($sqlFiles === []) { return [ 'has_updates' => false, 'message' => $recentOnly ? 'Geen SQL updates van de afgelopen week gevonden' : 'Geen SQL updates gevonden', ]; } $appliedHashes = $this->getAppliedSqlHashes(); $newSqlFiles = []; $alreadyApplied = []; foreach ($sqlFiles as $file) { $hash = $file['sha'] ?? md5((string) $file['name']); if (in_array($hash, $appliedHashes)) { $alreadyApplied[] = $file['name']; continue; } $newSqlFiles[] = $file; } if ($newSqlFiles === []) { return [ 'has_updates' => false, 'message' => 'Alle SQL updates zijn al toegepast (' . count($alreadyApplied) . ' stuks)', 'applied_count' => count($alreadyApplied), ]; } usort($newSqlFiles, fn ($a, $b) => strcmp((string) $a['name'], (string) $b['name'])); return [ 'has_updates' => true, 'count' => count($newSqlFiles), 'files' => $newSqlFiles, 'message' => count($newSqlFiles) . ' nieuwe SQL update(s) van deze week', 'applied_count' => count($alreadyApplied), ]; } public function runUpdates(): array { $sqlCheck = $this->checkForUpdates(false); if (! ($sqlCheck['has_updates'] ?? false)) { return ['success' => true, 'sql_updated' => false, 'message' => 'Geen nieuwe SQL updates']; } $results = [ 'success' => true, 'sql_updated' => true, 'files_run' => [], 'errors' => [], ]; foreach ($sqlCheck['files'] as $file) { $sqlResult = $this->downloadAndRunSql($file); if ($sqlResult['success']) { $results['files_run'][] = $file['name']; $this->markSqlAsApplied($file); } else { $results['errors'][] = $file['name'] . ': ' . $sqlResult['error']; } } if (count($results['errors']) > 0) { $results['message'] = count($results['files_run']) . ' SQL updates succesvol, ' . count($results['errors']) . ' met fouten'; } else { $results['message'] = count($results['files_run']) . ' SQL updates succesvol uitgevoerd!'; } return $results; } public function getAppliedUpdates(): array { $this->ensureSqlTableExists(); return DB::table(self::SQL_TABLE) ->orderBy('applied_at', 'desc') ->get() ->toArray(); } public function diagnose(): array { $diagnosis = [ 'table_exists' => false, 'applied_count' => 0, 'pending_count' => 0, 'error' => null, ]; try { $diagnosis['table_exists'] = Schema::hasTable(self::SQL_TABLE); if ($diagnosis['table_exists']) { $diagnosis['applied_count'] = DB::table(self::SQL_TABLE)->count(); } if ($this->githubRepo && $diagnosis['table_exists']) { $sqlCheck = $this->checkForUpdates(false); if (isset($sqlCheck['count'])) { $diagnosis['pending_count'] = $sqlCheck['count']; } } } catch (\Exception $e) { $diagnosis['error'] = $e->getMessage(); } return $diagnosis; } public function repair(): array { $actions = []; $errors = []; try { $this->ensureSqlTableExists(); $actions[] = 'SQL update tabel gecontroleerd'; $appliedCount = DB::table(self::SQL_TABLE)->count(); $actions[] = "SQL updates tabel OK ({$appliedCount} records)"; if ($this->githubRepo) { $sqlCheck = $this->checkForUpdates(false); if (isset($sqlCheck['error'])) { $actions[] = 'SQL update check: ' . $sqlCheck['error']; } elseif (! ($sqlCheck['has_updates'] ?? false)) { $actions[] = 'SQL updates: ' . ($sqlCheck['message'] ?? 'Allemaal up-to-date'); } else { $count = $sqlCheck['count'] ?? 0; $actions[] = "{$count} nieuwe SQL updates beschikbaar"; if ($this->isConfigured()) { $actions[] = 'SQL updates worden toegepast...'; $runResult = $this->runUpdates(); if ($runResult['success'] ?? false) { $filesRun = count($runResult['files_run'] ?? []); $actions[] = "{$filesRun} SQL updates toegepast"; } else { $errors[] = 'SQL updates: ' . ($runResult['error'] ?? 'Onbekende fout'); } } } } } catch (\Exception $e) { $errors[] = 'SQL repair: ' . $e->getMessage(); Log::error('[EmulatorSql] Repair failed', ['error' => $e->getMessage()]); } return [ 'success' => $errors === [], 'actions' => $actions, 'errors' => $errors, ]; } private function ensureSqlTableExists(): void { if (Schema::hasTable(self::SQL_TABLE)) { return; } Schema::create(self::SQL_TABLE, function ($table) { $table->id(); $table->string('file_name'); $table->string('file_hash')->unique(); $table->timestamp('applied_at'); $table->text('sql_content')->nullable(); }); } private function getAppliedSqlHashes(): array { $this->ensureSqlTableExists(); return DB::table(self::SQL_TABLE) ->pluck('file_hash') ->toArray(); } private function markSqlAsApplied(array $file): void { $this->ensureSqlTableExists(); $hash = $file['sha'] ?? md5((string) $file['name']); DB::table(self::SQL_TABLE)->updateOrInsert( ['file_hash' => $hash], [ 'file_name' => $file['name'], 'applied_at' => now(), ], ); } private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array { if (! $this->githubRepo) { return []; } $branch = $this->githubBranch ?: 'main'; $folderNames = [ 'Database%20Updates', 'Database Updates', 'database_updates', 'database/updates', 'sql/updates', 'sql', 'updates', ]; $knownSqlFiles = [ '07012026_UpdateDatabase_to_4-0-1.sql', '09012026_UpdateDatabase_to_4-0-2.sql', '12012026_Battle Banzai.sql', '12012026_Breeding Fixes.sql', '12012026_ChatBubbles.sql', '16032026_updateall_command.sql', '17032026_allow_underpass.sql', '19032026_hotel_timezone.sql', '21022026_user_prefixes.sql', 'Default_Camera.sql', 'UpdateDatabase_Allow_diagonale.sql', 'UpdateDatabase_BOT.sql', 'UpdateDatabase_Banners.sql', 'UpdateDatabase_DanceCMD.sql', 'UpdateDatabase_Happiness.sql', 'UpdateDatabase_Websocket.sql', 'UpdateDatabase_unignorable.sql', ]; try { if ($recentOnly) { return $this->fetchRecentSqlFiles($branch); } $sqlFiles = []; foreach ($knownSqlFiles as $filename) { foreach ($folderNames as $folderName) { $encodedFilename = str_replace(' ', '%20', $filename); $url = "https://raw.githubusercontent.com/{$this->githubRepo}/{$branch}/{$folderName}/{$encodedFilename}"; $response = Http::timeout(10)->get($url); if ($response->successful()) { $sqlFiles[] = [ 'name' => $filename, 'url' => $url, 'sha' => md5($response->body()), ]; break; } } } if ($sqlFiles !== []) { usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name'])); return $sqlFiles; } return []; } catch (\Exception $e) { Log::warning('[EmulatorSql] Could not fetch SQL files', ['error' => $e->getMessage()]); return []; } } private function fetchRecentSqlFiles(string $branch): array { $weekAgo = now()->subDays(7)->toIso8601String(); $githubToken = setting('github_token', ''); $headers = [ 'Accept' => 'application/vnd.github+json', 'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0', ]; if (! empty($githubToken)) { $headers['Authorization'] = 'Bearer ' . $githubToken; } $folderNames = ['Database Updates', 'database_updates', 'sql', 'updates']; try { foreach ($folderNames as $folderName) { $response = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits", [ 'path' => $folderName, 'sha' => $branch, 'since' => $weekAgo, 'per_page' => 100, ]); if ($response->successful()) { $commits = $response->json(); if (! is_array($commits) || $commits === []) { continue; } $sqlFiles = []; foreach ($commits as $commit) { $sha = $commit['sha'] ?? null; $commitDate = $commit['commit']['committer']['date'] ?? null; if (! $sha) { continue; } $commitResponse = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits/{$sha}"); if (! $commitResponse->successful()) { continue; } $commitData = $commitResponse->json(); $files = $commitData['files'] ?? []; foreach ($files as $file) { $filename = $file['filename'] ?? ''; if (! str_ends_with(strtolower((string) $filename), '.sql')) { continue; } $name = basename((string) $filename); $sqlFiles[] = [ 'name' => $name, 'url' => $file['raw_url'] ?? "https://raw.githubusercontent.com/{$this->githubRepo}/{$sha}/{$filename}", 'sha' => $sha, 'date' => $commitDate, ]; } } if ($sqlFiles !== []) { usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name'])); return $sqlFiles; } } } return []; } catch (\Exception $e) { Log::warning('[EmulatorSql] Could not fetch recent SQL files', ['error' => $e->getMessage()]); return []; } } private function downloadAndRunSql(array $file): array { try { $response = Http::timeout(60)->get($file['url']); if (! $response->successful()) { return ['success' => false, 'error' => 'Download mislukt']; } $sql = $this->cleanSql($response->body()); if (in_array(trim($sql), ['', '0'], true)) { return ['success' => true, 'message' => 'Lege SQL file overgeslagen']; } $host = setting('emulator_database_host', config('database.connections.emulator.host', '127.0.0.1')); $port = setting('emulator_database_port', config('database.connections.emulator.port', '3306')); $name = setting('emulator_database_name', config('database.connections.emulator.database', '')); $username = setting('emulator_database_username', config('database.connections.emulator.username', '')); $password = setting('emulator_database_password', config('database.connections.emulator.password', '')); if (empty($name) || empty($username)) { return ['success' => false, 'error' => 'Emulator database niet geconfigureerd']; } $pdo = new \PDO( "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4", $username, $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION], ); $statements = $this->splitSqlStatements($sql); $successCount = 0; $skipCount = 0; foreach ($statements as $statement) { $statement = trim((string) $statement); if ($statement === '' || $statement === '0') { continue; } try { $pdo->exec($statement); $successCount++; } catch (\PDOException $e) { if ($this->isDuplicateError($e)) { $skipCount++; continue; } Log::warning('[SQL Update] Statement error: ' . $e->getMessage()); } } Log::info('[SQL Update] Successfully ran: ' . $file['name'] . " ({$successCount} statements, {$skipCount} skipped)"); return ['success' => true, 'message' => "SQL uitgevoerd ({$successCount} statements, {$skipCount} duplicate)"]; } catch (\Exception $e) { Log::error('[SQL Update] Failed: ' . $file['name'], ['error' => $e->getMessage()]); return ['success' => false, 'error' => $e->getMessage()]; } } private function cleanSql(string $sql): string { $sql = preg_replace('/--.*$/m', '', $sql); $sql = preg_replace('/#.*$/m', '', (string) $sql); $sql = preg_replace('/SET FOREIGN_KEY_CHECKS.*?;/i', '', (string) $sql); return preg_replace('/SET @@SESSION.SQL_MODE.*?;/i', '', (string) $sql); } private function splitSqlStatements(string $sql): array { $parts = explode(';', $sql); return array_filter($parts, fn ($part) => trim((string) $part) !== ''); } private function isDuplicateError(\PDOException $e): bool { $message = strtolower($e->getMessage()); $patterns = [ 'duplicate entry', "table '", 'already exists', "can't create", 'duplicate key', 'duplicate index', 'error 1061', 'error 1062', 'error 1826', ]; return array_any($patterns, fn ($pattern) => str_contains($message, (string) $pattern)); } }