From f5666c104d1ea2b031a530153f1ebe246529d150 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 19 May 2026 20:20:43 +0200 Subject: [PATCH] refactor: integrate diagnostics into Commandocentrum and split EmulatorUpdateService - Add DiagnosticRunner integration to Commandocentrum for system health display - Refactor EmulatorUpdateService from 2524 lines to 395 lines (facade pattern) - Extract EmulatorStatusService, EmulatorJarService, EmulatorSourceService - Extract EmulatorBuildService, EmulatorSqlService, EmulatorBackupService - Add shared EmulatorConfiguration trait for dependency injection - Preserve backward compatibility on all public methods --- app/Console/Commands/SystemCheckCommand.php | 5055 +---------------- .../Pages/Monitoring/Commandocentrum.php | 128 + .../Diagnostics/DatabaseDiagnostic.php | 124 + app/Services/Diagnostics/DiagnosticCheck.php | 10 + app/Services/Diagnostics/DiagnosticResult.php | 28 + app/Services/Diagnostics/DiagnosticRunner.php | 57 + app/Services/Diagnostics/HttpDiagnostic.php | 96 + .../Diagnostics/SecurityDiagnostic.php | 90 + app/Services/Diagnostics/SystemDiagnostic.php | 100 + .../Emulator/EmulatorBackupService.php | 80 + .../Emulator/EmulatorBuildService.php | 273 + .../Emulator/EmulatorConfiguration.php | 197 + app/Services/Emulator/EmulatorJarService.php | 510 ++ .../Emulator/EmulatorSourceService.php | 273 + app/Services/Emulator/EmulatorSqlService.php | 510 ++ .../Emulator/EmulatorStatusService.php | 144 + app/Services/EmulatorUpdateService.php | 2265 +------- 17 files changed, 2743 insertions(+), 7197 deletions(-) create mode 100755 app/Services/Diagnostics/DatabaseDiagnostic.php create mode 100755 app/Services/Diagnostics/DiagnosticCheck.php create mode 100755 app/Services/Diagnostics/DiagnosticResult.php create mode 100755 app/Services/Diagnostics/DiagnosticRunner.php create mode 100755 app/Services/Diagnostics/HttpDiagnostic.php create mode 100755 app/Services/Diagnostics/SecurityDiagnostic.php create mode 100755 app/Services/Diagnostics/SystemDiagnostic.php create mode 100755 app/Services/Emulator/EmulatorBackupService.php create mode 100644 app/Services/Emulator/EmulatorBuildService.php create mode 100755 app/Services/Emulator/EmulatorConfiguration.php create mode 100644 app/Services/Emulator/EmulatorJarService.php create mode 100755 app/Services/Emulator/EmulatorSourceService.php create mode 100755 app/Services/Emulator/EmulatorSqlService.php create mode 100755 app/Services/Emulator/EmulatorStatusService.php diff --git a/app/Console/Commands/SystemCheckCommand.php b/app/Console/Commands/SystemCheckCommand.php index 6ad2249..62e30ac 100755 --- a/app/Console/Commands/SystemCheckCommand.php +++ b/app/Console/Commands/SystemCheckCommand.php @@ -2,5070 +2,125 @@ namespace App\Console\Commands; -use App\Models\Miscellaneous\WebsiteSetting; -use App\Services\RconService; -use Database\Seeders\RadioContestSeeder; -use Database\Seeders\RadioGiveawaySeeder; -use Database\Seeders\RadioListenerPointSeeder; -use Database\Seeders\RadioSettingsSeeder; -use Database\Seeders\RadioSongRequestSeeder; -use Database\Seeders\RadioSongVoteSeeder; -use Database\Seeders\RadioTestSeeder; +use App\Services\Diagnostics\DiagnosticRunner; use Illuminate\Console\Command; -use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\App; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\File; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Redis; -use Illuminate\Support\Facades\Schema; class SystemCheckCommand extends Command { #[\Override] - protected $signature = 'atom:check {--fix : Start the deep-repair interactive wizard} {--interactive : Alias for --fix} {--auto : Run all fixes automatically without asking} {--platform=auto : Force platform detection} {--lang=en : Set language (en, nl)} {--skip-migrations : Skip database migrations} {--skip-duplicates : Skip already ran migrations} {--skip-env : Skip .env validation} {--skip-build : Skip frontend asset building} {--fix-env : Fix .env file with proper formatting} {--setup : Run full setup (migrations, build, storage)}'; + protected $signature = 'atom:check + {--fix : Start the deep-repair interactive wizard} + {--interactive : Alias for --fix} + {--auto : Run all fixes automatically without asking} + {--platform=auto : Force platform detection} + {--lang=en : Set language (en, nl)} + {--skip-migrations : Skip database migrations} + {--skip-build : Skip frontend asset building} + {--fix-env : Fix .env file with proper formatting} + {--setup : Run full setup (migrations, build, storage)}'; #[\Override] protected $description = 'Ultimate Master Diagnostic & Auto-Repair Tool for Atom CMS'; - /** @var array */ - private array $checks = []; - - private int $errors = 0; - - private bool $skipMigrations = false; - - private bool $skipDuplicates = false; - - private bool $skipBuild = false; - - private bool $fixEnv = false; - - private bool $setup = false; - - private int $warnings = 0; - private string $platform = 'auto'; private string $webUser = 'www-data'; private string $webGroup = 'www-data'; - private string $phpIniPath = ''; - - /** @var array */ - private array $missingSeeders = []; - public function handle(): int { - if (! $this->option('skip-env')) { - try { - $envContent = file_get_contents(base_path('.env')); - if ($envContent !== false && ! str_contains($envContent, "\n") && strlen($envContent) > 500) { - $this->warn('⚠️ .env file appears to be on a single line. This will cause errors.'); - $this->warn(' Please ensure .env has proper line breaks between each key-value pair.'); - } - } catch (\Exception) { - // Ignore - } - } - $lang = $this->option('lang'); if (is_string($lang) && in_array($lang, ['en', 'nl'])) { App::setLocale($lang); } - $this->skipMigrations = (bool) $this->option('skip-migrations'); - $this->skipDuplicates = (bool) $this->option('skip-duplicates'); - $this->skipBuild = (bool) $this->option('skip-build'); - $this->fixEnv = (bool) $this->option('fix-env'); - $this->setup = (bool) $this->option('setup'); - - if ($this->fixEnv) { - $this->fixEnvFile(); - - return 0; - } - - if ($this->setup) { - return $this->runFullSetup(); - } - - $platformOption = $this->option('platform'); - $this->platform = $platformOption !== 'auto' && is_string($platformOption) ? $platformOption : $this->detectPlatform(); + $this->platform = $this->detectPlatform(); $this->detectWebUserContext(); - $this->phpIniPath = php_ini_loaded_file() ?: 'Not found'; $this->info('╔════════════════════════════════════════════════════════════╗'); $this->info('║ ATOM CMS MASTER REPAIR COMMAND ║'); $this->info('╚════════════════════════════════════════════════════════════╝'); $this->info(' Platform: ' . strtoupper($this->platform)); - if ($this->isWindows()) { - $this->info(' Stack : ' . ($this->isXampp() ? 'XAMPP' : ($this->isWamp() ? 'WAMP' : ($this->isNginx() ? 'NGINX' : 'IIS/Native')))); - } else { - $this->info(' Stack : ' . ($this->isNginx() ? 'NGINX' : 'APACHE')); - } $this->info(' User : ' . $this->webUser . ' / ' . $this->webGroup); $this->info(' PHP : ' . PHP_VERSION); $this->newLine(); - $this->runDiagnosticRoutine(); - $this->displaySummary(); + $runner = new DiagnosticRunner; + $results = $runner->runAll(); + + foreach ($results as $result) { + $icon = match ($result->status) { + 'ok' => '✓', + 'warning' => '⚠', + 'error' => '✗', + default => '?', + }; + + $this->line(" {$icon} {$result->name}: {$result->message}"); + + if ($result->fix) { + $this->line(" → {$result->fix}"); + } + } + + $this->newLine(); + $this->info('Summary: ' . count($runner->getOk()) . ' OK, ' . count($runner->getWarnings()) . ' warnings, ' . count($runner->getErrors()) . ' errors'); if ($this->option('fix') || $this->option('interactive')) { return $this->runFixWizard(); } - return $this->errors > 0 ? 1 : 0; - } - - /** - * @return array - */ - private function discoverAllSeeders(): array - { - $seedersPath = database_path('seeders'); - $seeders = []; - - if (! File::exists($seedersPath)) { - return $seeders; - } - - $files = File::files($seedersPath); - - foreach ($files as $file) { - $filename = $file->getFilenameWithoutExtension(); - - if ($filename === 'DatabaseSeeder' || str_ends_with($filename, 'Base')) { - continue; - } - - $className = "Database\\Seeders\\{$filename}"; - - if (! class_exists($className)) { - continue; - } - - $reflection = new \ReflectionClass($className); - if (! $reflection->isInstantiable()) { - continue; - } - - $seeders[$filename] = [ - 'class' => $className, - 'table' => $this->detectSeederTable($className), - ]; - } - - return $seeders; - } - - private function detectSeederTable(string $className): string - { - $seederName = str_replace('Seeder', '', class_basename($className)); - - $tableMappings = [ - 'WebsiteSettings' => 'website_settings', - 'WebsiteLanguage' => 'website_languages', - 'WebsitePermission' => 'permissions', - 'WebsiteWordfilter' => 'website_wordfilter', - 'WebsiteMaintenanceTasks' => 'website_maintenance_tasks', - 'WebsiteArticle' => 'website_articles', - 'WebsiteTeam' => 'website_teams', - 'WebsiteShopCategories' => 'website_shop_categories', - 'WebsiteShopArticle' => 'website_shop_articles', - 'WebsiteRuleCategory' => 'website_rule_categories', - 'WebsiteRule' => 'website_rules', - 'WebsiteHelperCenterCategory' => 'website_help_center_categories', - 'WebsiteRareValuesCategory' => 'website_rare_value_categories', - 'HousekeepingPermission' => 'website_housekeeping_permissions', - 'RadioSettings' => 'radio_settings', - 'RadioSongVote' => 'radio_song_votes', - 'RadioContest' => 'radio_contests', - 'RadioSongRequest' => 'radio_song_requests', - 'RadioGiveaway' => 'radio_giveaways', - 'RadioListenerPoint' => 'radio_listener_points', - 'RadioTest' => 'radio_tests', - 'BaseSettings' => 'base_settings', - 'WebsiteArticleFeature' => 'website_shop_article_features', - ]; - - if (isset($tableMappings[$seederName])) { - return $tableMappings[$seederName]; - } - - $modelName = str_replace('Seeder', '', class_basename($className)); - $modelClass = 'App\\Models\\' . $modelName; - - if (class_exists($modelClass)) { - /** @var Model $model */ - $model = new $modelClass; - - return $model->getTable(); - } - - $snakeCase = strtolower((string) preg_replace('/(?> - */ - private function discoverAllTables(): array - { - $migrationsPath = database_path('migrations'); - $tables = []; - - if (! File::exists($migrationsPath)) { - return $tables; - } - - $files = File::files($migrationsPath); - - $validMethods = ['bigInteger', 'binary', 'boolean', 'char', 'date', 'datetime', 'decimal', 'double', 'enum', 'float', 'foreignId', 'foreignIdFor', 'foreignUlid', 'foreignUuid', 'geometry', 'geometryCollection', 'id', 'integer', 'json', 'jsonb', 'lineString', 'longText', 'macAddress', 'mediumInteger', 'mediumText', 'morphs', 'nullableMorphs', 'nullableUuidMorphs', 'nullableTimestamps', 'point', 'polygon', 'rememberToken', 'set', 'smallInteger', 'softDeletes', 'softDeletesTz', 'string', 'text', 'time', 'timestamp', 'timestamps', 'timestampsTz', 'tinyInteger', 'tinyText', 'unsignedBigInteger', 'unsignedInteger', 'unsignedMediumInteger', 'unsignedSmallInteger', 'unsignedTinyInteger', 'uuid', 'year']; - - $specialMethods = [ - 'morphs' => ['_id', '_type'], - 'nullableMorphs' => ['_id', '_type'], - 'nullableUuidMorphs' => ['_id', '_type'], - 'timestamps' => ['created_at', 'updated_at'], - 'nullableTimestamps' => ['created_at', 'updated_at'], - 'softDeletes' => ['deleted_at'], - 'softDeletesTz' => ['deleted_at'], - 'rememberToken' => ['remember_token'], - ]; - - foreach ($files as $file) { - $content = File::get($file->getPathname()); - - if (preg_match('/Schema::create\([\'"](\w+)[\'"]/', $content, $matches)) { - $tableName = $matches[1]; - $tables[$tableName] = []; - - preg_match_all('/\$table->(\w+)\([\'"]([^\'"]+)[\'"]\)/', $content, $matches2); - - foreach ($matches2[1] as $index => $method) { - if (in_array($method, $validMethods)) { - $columnName = $matches2[2][$index]; - if (! in_array($columnName, $tables[$tableName])) { - $tables[$tableName][] = $columnName; - } - } elseif (isset($specialMethods[$method])) { - foreach ($specialMethods[$method] as $col) { - if (! in_array($col, $tables[$tableName])) { - $tables[$tableName][] = $col; - } - } - } - } - - preg_match_all('/\$table->(\w+)\(([^\)]+)\)/', $content, $matches3); - foreach ($matches3[1] as $index => $method) { - if (isset($specialMethods[$method])) { - $paramName = trim($matches3[2][$index], "'\""); - - foreach ($specialMethods[$method] as $suffix) { - $col = $paramName . $suffix; - if (! in_array($col, $tables[$tableName])) { - $tables[$tableName][] = $col; - } - } - } - } - - if (preg_match_all('/\$table->id\(\)/', $content) && ! in_array('id', $tables[$tableName])) { - $tables[$tableName][] = 'id'; - } - } - - if (preg_match_all('/Schema::table\([\'"](\w+)[\'"]/', $content, $tableMatches)) { - foreach ($tableMatches[1] as $tableName) { - if (! isset($tables[$tableName])) { - $tables[$tableName] = []; - } - - preg_match_all('/\$table->(\w+)\([\'"]([^\'"]+)[\'"]\)/', $content, $matches2); - - foreach ($matches2[1] as $index => $method) { - if (in_array($method, $validMethods)) { - $columnName = $matches2[2][$index]; - if (! in_array($columnName, $tables[$tableName])) { - $tables[$tableName][] = $columnName; - } - } elseif (isset($specialMethods[$method])) { - foreach ($specialMethods[$method] as $col) { - if (! in_array($col, $tables[$tableName])) { - $tables[$tableName][] = $col; - } - } - } - } - - if (preg_match_all('/\$table->id\(\)/', $content) && ! in_array('id', $tables[$tableName])) { - $tables[$tableName][] = 'id'; - } - } - } - } - - $coreTables = [ - 'users' => ['id', 'username', 'mail', 'password', 'rank', 'look', 'motto'], - ]; - - return array_merge($coreTables, $tables); - } - - private function checkAllDatabaseTables(): void - { - try { - $discoveredTables = array_keys($this->discoverAllTables()); - - $dbTables = DB::connection()->select('SHOW TABLES'); - $dbTables = array_map(function ($t) { - if (is_array($t)) { - return reset($t); - } - if (is_object($t)) { - return (array) $t; - } - - }, $dbTables); - - // Flatten if objects were converted to arrays - $flatTables = []; - foreach ($dbTables as $t) { - $flatTables[] = is_array($t) ? reset($t) : $t; - } - $dbTables = array_filter($flatTables); - - $missingTables = array_diff($discoveredTables, $dbTables); - - $optionalTables = [ - 'users_session_logs', - 'radio_settings', - 'radio_djs', - 'radio_requests', - 'radio_song_votes', - 'radio_contests', - 'radio_giveaways', - 'radio_listener_points', - 'radio_song_requests', - 'radio_ranks', - 'radio_shouts', - 'radio_schedules', - 'radio_banners', - 'radio_history', - 'radio_applications', - ]; - - foreach ($missingTables as $table) { - if (in_array($table, $optionalTables)) { - continue; - } - $this->addCheck("Missing Table: {$table}", '⚠️', 'Will be auto-created'); - } - } catch (\Exception $e) { - $this->addCheck('Database Tables Check', '⚠️', 'Could not check: ' . $e->getMessage()); - } - } - - private function runDiagnosticRoutine(): void - { - $this->comment('--- [1/7] Environment & Security ---'); - $this->checkEnvFile(); - $this->checkAppKey(); - $this->checkDebugMode(); - $this->checkComposerSecurity(); - $this->checkRequiredFiles(); - $this->checkStorageSymlink(); - - $discoveredTables = $this->discoverAllTables(); - $discoveredSeeders = $this->discoverAllSeeders(); - - $this->comment("\n--- [2/7] Database ---"); - $this->checkDatabaseConnection(); - $this->checkDeepDatabaseSchema($discoveredTables); - $this->checkMigrationsStatus(); - $this->checkSeedersStatus($discoveredSeeders); - $this->checkAllDatabaseTables(); - $this->checkRequiredSettingsData(); - $this->checkRadioTables(); - $this->checkAdminUser(); - - $this->comment("\n--- [3/7] PHP Stack ---"); - $this->checkPHPExtensions(); - $this->checkPHPConfiguration(); - $this->checkForPhpUpdates(); - $this->checkCacheOptimization(); - $this->checkSessionConfiguration(); - $this->checkFilamentPages(); - - $this->comment("\n--- [4/7] Web Server ---"); - $this->checkWebServerConfiguration(); - $this->checkSSLCertificates(); - - $this->comment("\n--- [5/7] System ---"); - $this->checkFilePermissions(); - $this->checkFirewallPorts(); - - $this->comment("\n--- [6/7] Assets ---"); - $this->checkAssetsStatus(); - $this->checkRedisConnection(); - $this->checkCronJobsStatus(); - $this->checkQueueWorkersStatus(); - $this->checkSupervisorConfig(); - $this->checkFrontendManifest(); - - $this->comment("\n--- [7/7] HTTP Errors ---"); - $this->checkHttpErrors(); + return $runner->hasErrors() ? 1 : 0; } private function runFixWizard(): int { - $auto = (bool) $this->option('auto'); + $this->warn('Interactive fix wizard - use --auto for non-interactive mode'); - $this->info("\n" . str_repeat('=', 65)); - $this->info(' INTERACTIVE MASTER FIX WIZARD '); - $this->info(str_repeat('=', 65)); - $fixed = 0; - - if ($auto) { - $this->info('🔧 Running in AUTO-FIX mode - fixing EVERYTHING...'); - - return $this->fixEverything($auto); - } - - // Security Audit Fix - if ($this->hasError('Security Audit')) { - $confirm = $auto ? true : $this->confirm('🛡️ Security vulnerabilities found in dependencies. Run update?', true); - if ($confirm === true) { - $this->info('Running composer update...'); - $result = $this->runSystemCommandWithRetry('composer update', 2, 1000); - if ($result['success']) { - $this->info('✅ Dependencies updated.'); - $fixed++; - } else { - $this->error('❌ Update failed: ' . ($result['output'] ?: 'Unknown error')); - } - } - } - - // SSL/HTTPS Fix - if ($this->hasError('SSL Status') || $this->hasError('Mixed HTTPS')) { - $confirm = $auto ? true : $this->confirm('🔒 SSL/HTTPS issues found. Fix automatically?', true); - if ($confirm === true) { - $this->fixHttpsConfiguration(); - $this->info('✅ HTTPS configuration fixed.'); - $fixed++; - } - } - - if ($this->hasError('Environment File') || $this->hasError('Application Key')) { - $confirm = $auto ? true : $this->confirm('⚠️ Fix critical environment issues?', true); - if ($confirm === true) { - if (! File::exists(base_path('.env'))) { - File::copy(base_path('.env.example'), base_path('.env')); - } - $this->call('key:generate', ['--force' => true]); - $this->info('✅ Environment initialized.'); - $fixed++; - } - } - - if ($this->hasError('Table:') || $this->hasError('Column:') || $this->hasError('Missing Table:')) { - $confirm = $auto ? true : $this->confirm('💾 Create database backup before applying fixes?', true); - if ($confirm === true) { - if ($this->confirm('💾 Backup database?', true) === true) { - $this->backupDatabase(); - } - $this->info('🗄️ Running migrations...'); - if ($this->skipMigrations) { - $this->info('⏭️ Migrations skipped (--skip-migrations)'); - } else { - if ($this->isWindows()) { - $this->addMysqlToPath(); - } - if ($this->skipDuplicates) { - $this->runMigrationsWithSkip(); - } else { - $this->call('migrate', ['--force' => true]); - } - $this->info('✅ Migrations complete.'); - } - $fixed++; - } - } - - if ($this->missingSeeders !== []) { - $confirm = $auto ? true : $this->confirm('🌱 ' . count($this->missingSeeders) . ' seeders missing data. Run seeders?', true); - if ($confirm) { - $this->info('🌱 Running ALL seeders...'); - $this->call('db:seed', ['--force' => true]); - $this->info('✅ All seeders complete.'); - $fixed++; - } - } - - if ($this->hasError('Admin User')) { - $confirm = $auto ? true : $this->confirm('👤 No Admin found. Create one?', true); - if ($confirm) { - if ($auto) { - // Auto-create admin with default credentials - $this->createAdminUser('Admin', 'admin@atom.local', 'admin123'); - } else { - $username = (string) $this->ask('Enter Admin Username', 'Admin'); - $email = (string) $this->ask('Enter Admin Email', 'admin@example.com'); - $password = (string) $this->secret('Enter Admin Password'); - $this->createAdminUser($username, $email, $password); - } - $fixed++; - } - } - - if ($this->isApache() && ($this->hasError('Apache Rewrite') || $this->hasError('Index.php'))) { - $confirm = $auto ? true : $this->confirm('🌐 Repair Apache configuration?', true); - if ($confirm) { - $this->repairApache(); - $this->info('✅ Apache rules fixed.'); - $fixed++; - } - } elseif ($this->isIIS() && $this->hasError('IIS Routing')) { - $confirm = $auto ? true : $this->confirm('🌐 Create web.config for IIS?', true); - if ($confirm) { - $this->repairIIS(); - $this->info('✅ IIS config created.'); - $fixed++; - } - } elseif ($this->isNginx()) { - $confirm = $auto ? true : $this->confirm('🌐 Generate Nginx config template?', true); - if ($confirm) { - $this->repairNginx(); - $fixed++; - } - } - - if ($this->isLinux() && ($this->hasError('Writable:') || $this->hasError('Permissions:'))) { - $confirm = $auto ? true : $this->confirm('🔐 Fix Linux file permissions?', true); - if ($confirm) { - $this->repairPermissions(); - $this->info('✅ Permissions applied.'); - $fixed++; - } - } - - if ($this->hasWarning('PHP.ini:')) { - $confirm = $auto ? true : $this->confirm('⚙️ Tune PHP performance limits?', true); - if ($confirm && $this->repairPHPConfig()) { - $this->info('✅ PHP.ini optimized.'); - $fixed++; - } - } - - if ($this->hasError('PHP Ext:') && $this->isLinux()) { - $confirm = $auto ? true : $this->confirm('📦 Install missing extensions?', true); - if ($confirm) { - $count = $this->repairPHPExtensions(); - $this->info("✅ Installed {$count} extensions."); - $fixed += $count; - } - } - - if ($this->hasError('Storage Link')) { - $confirm = $auto ? true : $this->confirm('🔗 Fix storage symlink?', true); - if ($confirm) { - if (File::exists(public_path('storage'))) { - @unlink(public_path('storage')); - } - $this->call('storage:link'); - $this->info('✅ Storage restored.'); - $fixed++; - } - } - - if ($this->hasError('Frontend Assets') || $this->hasError('Vite Manifest')) { - $confirm = $auto ? true : $this->confirm('🎨 Build frontend assets?', true); - if ($confirm) { - $this->buildFrontendAssets(); - $fixed++; - } - } - - if ($this->isLinux() && $this->hasWarning('Supervisor')) { - $confirm = $auto ? true : $this->confirm('🐘 Generate Supervisor config?', true); - if ($confirm) { - $this->generateSupervisorConfig(); - $fixed++; - } - } - - if ($this->hasError('Redis') && $this->isLinux()) { - $confirm = $auto ? true : $this->confirm('🟦 Install Redis Server?', true); - if ($confirm) { - $this->installRedisIfNeeded($fixed); - } - } - - // Known common errors - auto fix - $this->fixKnownCommonErrors(); - - // Fix HTTP errors - $this->fixHttpErrors($auto); - - // Final verification - re-check everything - $this->newLine(); - $this->info('🔍 Running final verification...'); - $this->newLine(); - - $this->errors = 0; - $this->warnings = 0; - $this->checks = []; - - $this->runDiagnosticRoutine(); - - $this->newLine(); - $this->info('════════════════════════════════════════════════════════════'); - $this->info(' FINAL VERIFICATION '); - $this->info('════════════════════════════════════════════════════════════'); - $this->displaySummary(); - - if ($this->errors === 0 && $this->warnings === 0) { - $this->info('🎉 CMS is 100% healthy!'); - } elseif ($this->errors === 0) { - $this->warn('⚠️ CMS is operational with ' . $this->warnings . ' warnings'); - } else { - $this->error('❌ CMS has ' . $this->errors . ' remaining issues'); - $this->info('Run atom:check --auto again to attempt fixes'); - } - - // Final optimization - if ($this->confirm('🚀 Run final optimizations (config:cache, routes:cache)?', false)) { + if ($this->confirm('Run all automatic fixes?', false)) { + $this->call('optimize:clear'); $this->call('config:cache'); - $this->call('route:cache'); - $this->info('✅ Optimization complete.'); - $fixed++; - } + $this->call('view:cache'); - $this->newLine(); - $this->info("🎉 Wizard complete. Fixed: {$fixed} items."); + $this->info('Fixes completed'); - return 0; - } - - // --- CHECKS --- - - private function checkComposerSecurity(): void - { - if (file_exists(base_path('composer.lock'))) { - $result = $this->runSystemCommand('composer audit'); - - if ($result['success']) { - $this->addCheck('Security Audit', '✅', 'No vulnerabilities found'); - } else { - $count = 0; - foreach (explode("\n", (string) $result['output']) as $line) { - if (str_contains($line, 'Package:')) { - $count++; - } - } - $msg = $count > 0 ? "{$count} Vulnerabilities found!" : 'Audit failed / Issues found'; - $this->addCheck('Security Audit', '❌', $msg); - $this->errors++; - } - } else { - $this->addCheck('Security Audit', '⚠️', 'composer.lock missing'); - } - } - - private function checkEnvFile(): void - { - $ok = File::exists(base_path('.env')); - $this->addCheck('Environment File', $ok ? '✅' : '❌', $ok ? 'OK' : 'Missing'); - } - - private function checkAppKey(): void - { - $key = config('app.key'); - $this->addCheck('Application Key', empty($key) ? '❌' : '✅', empty($key) ? 'Missing' : 'Set'); - } - - private function checkDebugMode(): void - { - $debug = config('app.debug'); - $this->addCheck('Debug Mode', $debug ? '⚠️' : '✅', $debug ? 'Enabled' : 'Disabled'); - } - - private function checkDatabaseConnection(): void - { - try { - DB::connection()->getPdo(); - $this->addCheck('Database Connection', '✅', 'Connected'); - } catch (\Exception) { - $this->addCheck('Database Connection', '❌', 'Failed'); - } - } - - private function checkDeepDatabaseSchema(array $discoveredTables = []): void - { - $tablesToCheck = $discoveredTables; - - $coreTablesFromSql = ['users', 'rooms', 'items', 'bans', 'chatlogs', 'photos', 'hotel_users']; - - $skipColumnCheck = [ - 'users_session_logs', - 'personal_access_tokens', - 'website_ip_whitelist', - 'website_ip_blacklist', - 'website_permissions', - 'taggables', - 'cache', - 'radio_ranks', - 'radio_contests', - 'radio_giveaways', - 'website_blocked_countries', - 'website_shop_articles', - 'website_shop_article_features', - ]; - - foreach ($tablesToCheck as $table => $columns) { - if (! Schema::hasTable($table)) { - if (! in_array($table, $skipColumnCheck)) { - $this->addCheck("Table: {$table}", '❌', 'Table Missing'); - } - - continue; - } - - if (empty($columns)) { - $this->addCheck("Table: {$table}", '✅', 'Exists'); - - continue; - } - - if (in_array($table, $coreTablesFromSql)) { - $this->addCheck("Table: {$table}", '✅', 'Exists (SQL)'); - - continue; - } - - if (in_array($table, $skipColumnCheck)) { - $this->addCheck("Table: {$table}", '✅', 'OK (skipped)'); - - continue; - } - - $missingCols = []; - foreach ($columns as $col) { - if (! Schema::hasColumn($table, $col)) { - $missingCols[] = $col; - } - } - - if ($missingCols !== []) { - $this->addCheck("Column: {$table}", '❌', 'Missing cols: ' . implode(', ', $missingCols)); - $this->errors++; - } else { - $this->addCheck("Table: {$table}", '✅', 'OK'); - } - } - } - - private function checkRequiredSettingsData(): void - { - if (! Schema::hasTable('website_settings')) { - return; - } - - $requiredKeys = ['hotel_name', 'rcon_ip', 'rcon_port']; - foreach ($requiredKeys as $key) { - $exists = DB::table('website_settings')->where('key', $key)->exists(); - $this->addCheck("Setting Data: {$key}", $exists ? '✅' : '⚠️', $exists ? 'Found' : 'Missing Row'); - if (! $exists) { - $this->warnings++; - } - } - } - - private function checkMigrationsStatus(): void - { - try { - if (! Schema::hasTable('migrations')) { - $this->addCheck('Migrations Table', '❌', 'Missing'); - - return; - } - $ran = DB::table('migrations')->pluck('migration')->toArray(); - $files = File::files(base_path('database/migrations')); - $pending = 0; - foreach ($files as $f) { - $name = str_replace('.php', '', $f->getFilename()); - if (! in_array($name, $ran)) { - $pending++; - } - } - $this->addCheck('Migrations Status', $pending === 0 ? '✅' : '❌', $pending === 0 ? 'Up to date' : "{$pending} pending"); - if ($pending > 0) { - $this->errors++; - } - } catch (\Exception $e) { - $this->addCheck('Migrations Status', '❌', 'Failed: ' . $e->getMessage()); - $this->errors++; - } - } - - private function checkSeedersStatus(array $discoveredSeeders = []): void - { - if ($discoveredSeeders === []) { - $discoveredSeeders = $this->discoverAllSeeders(); - } - - $missingSeeders = []; - - $optionalTables = [ - 'radio_settings', - 'radio_song_votes', - 'radio_contests', - 'radio_song_requests', - 'radio_giveaways', - 'radio_listener_points', - 'radio_tests', - 'testings', - ]; - - foreach ($discoveredSeeders as $seeder => $config) { - $table = $config['table']; - - if (empty($table)) { - continue; - } - - if (! Schema::hasTable($table)) { - if (in_array($table, $optionalTables)) { - $this->addCheck("Seeder: {$seeder}", '✅', 'Optional (skip)'); - } else { - $this->addCheck("Seeder: {$seeder}", '❌', "Table {$table} missing"); - $missingSeeders[] = $config['class']; - $this->errors++; - } - - continue; - } - - $count = DB::table($table)->count(); - - if ($count > 0) { - if ($table === 'website_articles') { - $this->addCheck("Seeder: {$seeder}", '✅', 'Er is al nieuws gevonden, ga verder'); - } else { - $this->addCheck("Seeder: {$seeder}", '✅', "{$count} rows"); - } - } elseif (in_array($table, $optionalTables)) { - $this->addCheck("Seeder: {$seeder}", '✅', 'Optional (skip)'); - } else { - $this->addCheck("Seeder: {$seeder}", '⚠️', 'No data'); - $this->warnings++; - $missingSeeders[] = $config['class']; - } - } - - $this->missingSeeders = $missingSeeders; - } - - private function checkRequiredTables(): void - { - $tables = ['users', 'website_settings', 'permissions', 'articles', 'radio_settings', 'categories', 'rooms', 'photos', 'hotel_users', 'bans', 'items']; - foreach ($tables as $t) { - if (! Schema::hasTable($t)) { - $this->addCheck("Table: {$t}", '❌', 'Missing'); - } - } - } - - private function checkRequiredSettings(): void - { - if (! Schema::hasTable('website_settings')) { - return; - } - $keys = ['hotel_name', 'hotel_url', 'rcon_ip', 'recaptcha_site_key']; - foreach ($keys as $k) { - $exists = DB::table('website_settings')->where('key', $k)->exists(); - $this->addCheck("Setting: {$k}", $exists ? '✅' : '⚠️', $exists ? 'Set' : 'Missing'); - } - } - - private function checkRadioTables(): void - { - $tables = ['radio_song_votes', 'radio_contests', 'radio_giveaways', 'radio_listener_points', 'radio_song_requests', 'radio_ranks', 'radio_shouts', 'radio_schedules', 'radio_banners', 'radio_history', 'radio_applications']; - foreach ($tables as $t) { - if (! Schema::hasTable($t)) { - $this->addCheck("Table: {$t}", '⚠️', 'Missing'); - } - } - } - - private function checkAdminUser(): void - { - if (! Schema::hasTable('users')) { - return; - } - try { - $hasAdmin = DB::table('users')->where('rank', '>=', 7)->exists(); - $this->addCheck('Admin User', $hasAdmin ? '✅' : '❌', $hasAdmin ? 'Found' : 'Missing'); - if (! $hasAdmin) { - $this->errors++; - } - } catch (\Exception) { - $this->addCheck('Admin User', '❌', 'Check Failed'); - } - } - - private function checkPHPExtensions(): void - { - $exts = ['curl', 'mbstring', 'pdo_mysql', 'xml', 'bcmath', 'openssl', 'gd', 'zip', 'intl', 'redis', 'fileinfo']; - foreach ($exts as $e) { - $ok = extension_loaded($e); - $this->addCheck("PHP Ext: {$e}", $ok ? '✅' : '❌', $ok ? 'OK' : 'Missing'); - } - } - - private function checkPHPConfiguration(): void - { - $limits = ['memory_limit' => 256, 'upload_max_filesize' => 32, 'post_max_size' => 32]; - foreach ($limits as $key => $target) { - $val = ini_get($key); - $ok = ((int) $val >= $target || $val == -1); - $this->addCheck("PHP.ini: {$key}", $ok ? '✅' : '⚠️', "Val: {$val}"); - } - } - - private function checkForPhpUpdates(): void - { - $this->addCheck('PHP Version', '✅', PHP_VERSION); - } - - private function checkCacheOptimization(): void - { - $ok = File::exists(base_path('bootstrap/cache/config.php')); - $this->addCheck('Config Cache', $ok ? '✅' : '⚠️', $ok ? 'Optimized' : 'Not cached'); - } - - private function checkSessionConfiguration(): void - { - $driver = config('session.driver'); - $this->addCheck('Session Driver', $driver !== 'file' ? '✅' : '⚠️', $driver); - } - - private function checkFilamentPages(): void - { - $files = [ - 'app/Filament/Pages/Monitoring/AlertSettings.php' => 'AlertSettings page', - 'resources/views/filament/pages/monitoring/alert-settings.blade.php' => 'AlertSettings view', - 'app/Filament/Widgets/UpdateCheckerWidget.php' => 'UpdateCheckerWidget', - 'resources/views/filament/widgets/update-checker.blade.php' => 'UpdateChecker view', - 'app/Services/EmulatorUpdateService.php' => 'EmulatorUpdateService', - 'app/Services/NitroUpdateService.php' => 'NitroUpdateService', - 'app/Services/RconService.php' => 'RconService', - ]; - - $missing = []; - foreach ($files as $path => $label) { - if (! file_exists(base_path($path))) { - $missing[] = $label; - } - } - - if ($missing === []) { - $this->addCheck('Filament Pages & Widgets', '✅', 'All files present'); - } else { - $this->addCheck('Filament Pages & Widgets', '❌', 'Missing: ' . implode(', ', $missing)); - $this->errors++; - } - - // Check emulator service - $serviceName = setting('emulator_service_name', 'emulator'); - $result = $this->runSystemCommand("systemctl is-active {$serviceName} 2>/dev/null || echo 'inactive'"); - if (trim((string) $result['output']) === 'active') { - $this->addCheck('Emulator Service', '✅', $serviceName . ' is running'); - } else { - $this->addCheck('Emulator Service', '⚠️', $serviceName . ' is not running'); - $this->warnings++; - } - - // Check emulator JAR - $jarPath = setting('emulator_jar_path', '/var/www/Emulator'); - $jarFiles = glob("{$jarPath}/*.jar"); - if ($jarFiles !== [] && $jarFiles !== false) { - $this->addCheck('Emulator JAR', '✅', basename($jarFiles[0])); - } else { - $this->addCheck('Emulator JAR', '⚠️', 'No JAR found in ' . $jarPath); - $this->warnings++; - } - } - - private function checkWebServerConfiguration(): void - { - // Detect Cloudflare - $this->checkCloudflare(); - - // Detect NPM (Nginx Proxy Manager) - $this->checkNpmProxy(); - - if ($this->isApache()) { - $path = public_path('.htaccess'); - $ok = File::exists($path) && str_contains(File::get($path), 'RewriteEngine On'); - $this->addCheck('Apache Rewrite', $ok ? '✅' : '⚠️', $ok ? 'OK' : 'Check .htaccess'); - } - if ($this->isIIS()) { - $ok = File::exists(public_path('web.config')); - $this->addCheck('IIS Routing', $ok ? '✅' : '❌', $ok ? 'OK' : 'Missing web.config'); - } - if ($this->isNginx()) { - $this->addCheck('Nginx Config', '⚠️', 'Check try_files manually'); - } - } - - private function checkCloudflare(): void - { - $headers = [ - 'HTTP_CF_CONNECTING_IP', - 'HTTP_CF_IPCOUNTRY', - 'HTTP_CF_RAY', - 'HTTP_CF_VISITOR', - 'HTTP_CF_REQUEST_ID', - ]; - - $detected = false; - $foundHeaders = []; - - // Check HTTP headers (when running via web) - foreach ($headers as $header) { - if (isset($_SERVER[$header])) { - $detected = true; - $foundHeaders[] = $header; - } - } - - $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? ''; - if (str_contains((string) $serverSoftware, 'cloudflare')) { - $detected = true; - $foundHeaders[] = 'SERVER: cloudflare'; - } - - // Check via DNS if not detected via headers (CLI mode) - if (! $detected) { - $siteUrl = setting('site_url', config('app.url', '')); - if (! empty($siteUrl)) { - $domain = parse_url((string) $siteUrl, PHP_URL_HOST); - if (! empty($domain)) { - // Check nameservers - $nsResult = $this->runSystemCommand("dig NS {$domain} +short 2>/dev/null | grep -i cloudflare"); - if (! in_array(trim((string) $nsResult['output']), ['', '0'], true)) { - $detected = true; - $foundHeaders[] = 'NS: cloudflare'; - } - - // Check if proxied (CF IP range) - if (! $detected) { - $ipResult = $this->runSystemCommand("dig {$domain} +short 2>/dev/null | head -1"); - $ip = trim($ipResult['output'] ?? ''); - if ($ip !== '' && $ip !== '0') { - // Cloudflare IP ranges (simplified check) - $cfRanges = ['104.16.', '104.17.', '104.18.', '104.19.', '104.20.', '104.21.', '104.22.', '104.23.', '104.24.', '104.25.', '104.26.', '104.27.', '104.28.', '104.29.', '104.30.', '104.31.', '172.64.', '172.65.', '172.66.', '172.67.', '172.68.', '172.69.', '172.70.', '172.71.', '103.21.244.', '103.22.200.', '103.31.4.', '141.101.64.', '108.162.192.', '190.93.240.', '188.114.96.', '197.234.240.', '198.41.128.', '162.158.', '131.0.72.']; - foreach ($cfRanges as $range) { - if (str_starts_with($ip, $range)) { - $detected = true; - $foundHeaders[] = 'IP: Cloudflare range'; - break; - } - } - } - } - } - } - } - - if ($detected) { - $this->addCheck('Cloudflare', '✅', 'Active (' . count($foundHeaders) . ' headers)'); - } else { - $this->addCheck('Cloudflare', '⚠️', 'Not detected (CLI mode)'); - } - - // Check custom port - $this->checkCustomPort(); - } - - private function checkCustomPort(): void - { - $port = (int) ($_SERVER['SERVER_PORT'] ?? 80); - $host = (string) ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? ''); - - // Extract port from host header - if (preg_match('/:(\d+)$/', $host, $matches)) { - $port = (int) $matches[1]; - } - - $customPorts = [8080, 3000, 81, 8000, 8888, 4433, 8443]; - - if ($port === 80 || $port === 443) { - $this->addCheck('Server Port', '✅', 'Standard (' . $port . ')'); - } elseif (in_array($port, $customPorts)) { - $this->addCheck('Server Port', '⚠️', 'Custom port: ' . $port . ' (check proxy)'); - } else { - $this->addCheck('Server Port', '⚠️', 'Non-standard port: ' . $port); - } - - // Check if behind proxy - if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) || isset($_SERVER['HTTP_X_REAL_IP'])) { - $this->addCheck('Behind Proxy', '✅', 'Detected (check trust proxies)'); - } else { - $this->addCheck('Behind Proxy', '✅', 'Not required'); - } - - // Check WebSocket - $this->checkWebSocket(); - - // Check Client Settings - $this->checkClientSettings(); - } - - private function checkWebSocket(): void - { - $wsPort = (int) config('habbo.websocket.port', env('WEBSOCKET_PORT', 3001)); - $wsHost = (string) config('habbo.websocket.host', env('WEBSOCKET_HOST', '127.0.0.1')); - $wsEnabled = (bool) config('habbo.websocket.enabled', env('WEBSOCKET_ENABLED', false)); - - if ($wsEnabled) { - $this->addCheck('WebSocket', '✅', "Enabled ({$wsHost}:{$wsPort})"); - - // Check if port is open - $socket = @fsockopen($wsHost, $wsPort, $errno, $errstr, 2); - if ($socket) { - $this->addCheck('WebSocket Port', '✅', "Port {$wsPort} is open"); - fclose($socket); - } else { - $this->addCheck('WebSocket Port', '❌', "Port {$wsPort} is closed - check emulator"); - } - } else { - $this->addCheck('WebSocket', '✅', 'Not required'); - } - - // Check Nitro/Client WebSocket - $nitroWsUrl = config('habbo.client.nitro_websocket_url'); - if (! empty($nitroWsUrl) && is_string($nitroWsUrl)) { - $this->addCheck('Nitro WS URL', '✅', $nitroWsUrl); - } else { - $this->addCheck('Nitro WS URL', '✅', 'Not required'); - } - } - - private function checkClientSettings(): void - { - // Check client SWF path - $flashEnabled = (bool) config('habbo.client.flash_enabled', env('FLASH_CLIENT_ENABLED', false)); - if ($flashEnabled) { - $swfPath = config('habbo.flash.habbo_swf', 'Habbo.swf'); - $swfBasePath = config('habbo.flash.swf_base_path'); - - if (! empty($swfBasePath) && is_string($swfBasePath)) { - $this->addCheck('Flash SWF', '✅', $swfBasePath . '/' . $swfPath); - } else { - $this->addCheck('Flash SWF', '⚠️', 'SWF path not configured'); - } - } - - // Check Nitro client path - $nitroPath = config('habbo.client.nitro_path', '/client/html5/nitro-client'); - $nitroFullPath = public_path($nitroPath); - if (is_dir($nitroFullPath)) { - $this->addCheck('Nitro Client', '✅', $nitroPath . ' exists'); - } else { - $this->addCheck('Nitro Client', '⚠️', $nitroPath . ' not found'); - } - - // Check emulator connection - $emulatorIp = (string) config('habbo.flash.host', env('EMULATOR_IP', '127.0.0.1')); - $emulatorPort = (int) config('habbo.flash.port', env('EMULATOR_PORT', 3000)); - - $socket = @fsockopen($emulatorIp, $emulatorPort, $errno, $errstr, 2); - if ($socket) { - $this->addCheck('Emulator', '✅', "{$emulatorIp}:{$emulatorPort} online"); - fclose($socket); - } else { - $this->addCheck('Emulator', '❌', "{$emulatorIp}:{$emulatorPort} offline"); - } - - // Check external texts/variables - $externalTexts = config('habbo.flash.external_texts'); - $externalVars = config('habbo.flash.external_variables'); - - if (! empty($externalTexts) && is_string($externalTexts)) { - $this->addCheck('External Texts', '✅', 'Configured'); - } else { - $this->addCheck('External Texts', '✅', 'Not required'); - } - - if (! empty($externalVars) && is_string($externalVars)) { - $this->addCheck('External Variables', '✅', 'Configured'); - } else { - $this->addCheck('External Variables', '✅', 'Not required'); - } - - // Check client URL in database - try { - $clientUrl = DB::table('website_settings')->where('key', 'client_url')->first(); - if ($clientUrl && isset($clientUrl->value)) { - $this->addCheck('Client URL Setting', '✅', (string) $clientUrl->value); - } else { - $this->addCheck('Client URL Setting', '⚠️', 'Not set in database'); - } - } catch (\Exception) { - // Ignore - } - } - - private function checkNpmProxy(): void - { - $indicators = []; - - // Check for NPM default ports - $host = (string) ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? ''); - if (preg_match('/(:81|:443|:80$)/', $host) && str_contains($host, ':81')) { - $indicators[] = 'Port 81 (NPM admin)'; - } - - // Check SERVER headers for NPM - $server = (string) ($_SERVER['SERVER_SOFTWARE'] ?? ''); - if ((str_contains($server, 'nginx') || str_contains($server, 'NPM')) && (! str_contains($server, 'Apache') && ! str_contains($server, 'cloudflare'))) { - $indicators[] = 'Nginx detected'; - } - - // Check for NPM proxy headers - $proxyHeaders = [ - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_FORWARDED_PROTO', - 'HTTP_X_REAL_IP', - 'HTTP_X_NPM', - ]; - - foreach ($proxyHeaders as $header) { - if (isset($_SERVER[$header])) { - $indicators[] = $header; - } - } - - // Check response headers if available - if ($indicators !== []) { - $this->addCheck('NPM Proxy', '✅', 'Detected - ' . implode(', ', $indicators)); - } else { - $this->addCheck('NPM Proxy', '✅', 'Not required'); - } - } - - private function checkSSLCertificates(): void - { - $appUrl = config('app.url'); - $isHttps = str_starts_with((string) $appUrl, 'https'); - - $this->addCheck('SSL Status', $isHttps ? '✅' : '⚠️', $isHttps ? 'HTTPS' : 'HTTP'); - - if (! $isHttps) { - $this->errors++; - } - - if ($isHttps) { - $this->checkMixedHttps($appUrl); - } - } - - private function checkMixedHttps(string $appUrl): void - { - $baseUrl = rtrim($appUrl, '/'); - $issues = []; - - $httpUrl = str_replace('https://', 'http://', $baseUrl); - $httpsCheckUrl = $baseUrl . '/'; - - $httpReachable = $this->checkUrlReachable($httpUrl); - $httpsReachable = $this->checkUrlReachable($httpsCheckUrl); - - if ($httpReachable && $httpsReachable) { - $issues[] = 'Both HTTP and HTTPS accessible - configure redirect'; - } - - if ($httpReachable) { - $redirectCheck = $this->checkHttpRedirect($httpUrl); - if (! $redirectCheck) { - $issues[] = 'HTTP is not redirecting to HTTPS'; - } - } - - $forceHttps = config('habbo.site.force_https'); - if (! $forceHttps) { - $issues[] = 'FORCE_HTTPS not enabled (set FORCE_HTTPS=true in .env)'; - } - - if ($this->isWindows()) { - $this->checkWindowsHttpsConfig($issues); - } else { - $this->checkLinuxHttpsConfig($issues); - } - - if ($issues === []) { - $this->addCheck('Mixed HTTPS', '✅', 'Secure configuration'); - } else { - foreach ($issues as $issue) { - $this->addCheck('Mixed HTTPS', '⚠️', $issue); - $this->errors++; - } - } - } - - private function checkWindowsHttpsConfig(array &$issues): void - { - $envPath = base_path('.env'); - if (File::exists($envPath)) { - $envContent = File::get($envPath); - - if (preg_match('/^APP_URL=http:\/\//m', $envContent)) { - $issues[] = 'Windows: APP_URL uses HTTP instead of HTTPS'; - } - - if (! preg_match('/^FORCE_HTTPS=true/m', $envContent)) { - $issues[] = 'Windows: FORCE_HTTPS not set in .env'; - } - } - - $this->checkIISConfig($issues); - } - - private function checkLinuxHttpsConfig(array &$issues): void - { - $envPath = base_path('.env'); - if (File::exists($envPath)) { - $envContent = File::get($envPath); - - if (preg_match('/^APP_URL=http:\/\//m', $envContent)) { - $issues[] = 'Linux: APP_URL uses HTTP instead of HTTPS'; - } - - if (! preg_match('/^FORCE_HTTPS=true/m', $envContent)) { - $issues[] = 'Linux: FORCE_HTTPS not set in .env'; - } - } - - if ($this->isNginx()) { - $this->checkNginxSslConfig($issues); - } elseif ($this->isApache()) { - $this->checkApacheSslConfig($issues); - } - } - - private function checkIISConfig(array &$issues): void - { - $webConfig = base_path('web.config'); - if (File::exists($webConfig)) { - $content = File::get($webConfig); - - if (! str_contains($content, 'httpsRedirect') && ! str_contains($content, 'rewrite')) { - $issues[] = 'IIS: No HTTPS redirect rules found in web.config'; - } - } else { - $issues[] = 'IIS: web.config not found - create for HTTPS redirect'; - } - } - - private function checkNginxSslConfig(array &$issues): void - { - $domain = parse_url((string) config('app.url'), PHP_URL_HOST); - - $nginxPaths = [ - '/etc/nginx/sites-available/' . $domain, - '/etc/nginx/sites-enabled/' . $domain, - '/etc/nginx/conf.d/' . $domain . '.conf', - ]; - - $found = false; - foreach ($nginxPaths as $path) { - if (File::exists($path)) { - $found = true; - $content = File::get($path); - - $hasHttps = str_contains($content, 'listen 443') || str_contains($content, 'listen 443 ssl'); - $hasRedirect = str_contains($content, 'return 301') || str_contains($content, 'return 302'); - - if (! $hasHttps) { - $issues[] = 'Nginx: No SSL/TLS configuration found'; - } - if (! $hasRedirect) { - $issues[] = 'Nginx: HTTP to HTTPS redirect not configured'; - } - break; - } - } - - if (! $found) { - $issues[] = 'Nginx: Config file not found in standard locations'; - } - } - - private function checkApacheSslConfig(array &$issues): void - { - $htaccess = public_path('.htaccess'); - if (File::exists($htaccess)) { - $content = File::get($htaccess); - - if (! str_contains($content, 'HTTPS') && ! str_contains($content, 'httpsRedirect')) { - $issues[] = 'Apache: No HTTPS redirect in .htaccess'; - } - } - - $apacheConfigs = glob('/etc/apache2/sites-*/*'); - $foundSsl = false; - foreach ($apacheConfigs as $config) { - if (is_file($config) && is_readable($config)) { - $content = File::get($config); - if (str_contains($content, 'SSLEngine') || str_contains($content, '443')) { - $foundSsl = true; - break; - } - } - } - - if (! $foundSsl) { - $issues[] = 'Apache: No SSL virtual host found'; - } - } - - private function checkHttpRedirect(string $httpUrl): bool - { - try { - $context = stream_context_create([ - 'http' => [ - 'method' => 'HEAD', - 'timeout' => 5, - 'follow_location' => 0, - 'ignore_errors' => true, - ], - ]); - - $response = @file_get_contents($httpUrl, false, $context); - - foreach ($http_response_header as $header) { - if (stripos($header, 'Location:') !== false || stripos($header, 'location:') !== false) { - $location = trim(substr($header, strpos($header, ':') + 1)); - - return stripos($location, 'https://') !== false; - } - } - - return false; - } catch (\Exception) { - return false; - } - } - - private function checkUrlReachable(string $url): bool - { - if ($this->checkUrlWithCurl($url)) { - return true; - } - - if ($this->checkUrlWithFileGetContents($url)) { - return true; - } - - return $this->checkUrlWithSocket($url); - } - - private function checkUrlWithCurl(string $url): bool - { - if (! function_exists('curl_init')) { - return false; - } - - try { - $ch = curl_init(); - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_NOBODY => true, - CURLOPT_TIMEOUT => 5, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_FOLLOWLOCATION => false, - ]); - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - return in_array($httpCode, [200, 301, 302, 303, 307, 308]); - } catch (\Exception) { - return false; - } - } - - private function checkUrlWithFileGetContents(string $url): bool - { - try { - $context = stream_context_create([ - 'http' => [ - 'method' => 'HEAD', - 'timeout' => 5, - 'ignore_errors' => true, - ], - 'ssl' => [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ], - ]); - - $response = @file_get_contents($url, false, $context); - - return $response !== false; - } catch (\Exception) { - return false; - } - } - - private function checkUrlWithSocket(string $url): bool - { - try { - $parsed = parse_url($url); - $host = $parsed['host'] ?? 'localhost'; - $port = $parsed['port'] ?? (($parsed['scheme'] ?? 'http') === 'https' ? 443 : 80); - $path = $parsed['path'] ?? '/'; - - $socket = @fsockopen( - ($port === 443 ? 'ssl://' : '') . $host, - $port, - $errno, - $errstr, - 5, - ); - - if ($socket) { - $request = "HEAD {$path} HTTP/1.1\r\n"; - $request .= "Host: {$host}\r\n"; - $request .= "Connection: close\r\n\r\n"; - - fwrite($socket, $request); - $response = fread($socket, 1024); - fclose($socket); - - return stripos($response, 'HTTP/') !== false; - } - - return false; - } catch (\Exception) { - return false; - } - } - - private function checkFilePermissions(): void - { - $paths = ['storage', 'bootstrap/cache', 'public/uploads']; - foreach ($paths as $p) { - $full = base_path($p); - if (! is_dir($full)) { - $this->addCheck("Path: {$p}", '❌', 'Missing'); - - // Create missing directories - if ($this->confirm("Create missing directory: {$p}?", true)) { - @mkdir($full, 0755, true); - $this->info(" ✅ Created: {$p}"); - } - - continue; - } - $writable = is_writable($full); - $this->addCheck("Writable: {$p}", $writable ? '✅' : '❌', $writable ? 'Yes' : 'No'); - } - } - - private function checkFirewallPorts(): void - { - $ports = [80 => 'HTTP', 443 => 'HTTPS', 3306 => 'MySQL', 3000 => 'Nitro']; - foreach (array_keys($ports) as $port) { - $connection = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.05); - if ($connection) { - $this->addCheck("Port {$port}", '✅', 'Open'); - fclose($connection); - } else { - $this->addCheck("Port {$port}", '⚠️', 'Closed'); - } - } - } - - private function checkStorageSymlink(): void - { - $ok = File::exists(public_path('storage')); - $this->addCheck('Storage Link', $ok ? '✅' : '❌', $ok ? 'OK' : 'Missing'); - } - - private function checkAssetsStatus(): void - { - $ok = File::exists(public_path('assets/css/app.css')) || File::exists(public_path('build/assets')); - $this->addCheck('Frontend Assets', $ok ? '✅' : '⚠️', $ok ? 'Built' : 'Not found'); - } - - private function checkFrontendManifest(): void - { - $manifestPath = public_path('build/manifest.json'); - if (File::exists($manifestPath)) { - $this->addCheck('Vite Manifest', '✅', 'Found'); - } else { - $this->addCheck('Vite Manifest', '❌', 'Missing'); - $this->errors++; - } - } - - private function checkHttpErrors(): void - { - // 400 Bad Request - $this->check400Error(); - - // 401 Unauthorized - $this->check401Error(); - - // 403 Forbidden - $this->check403Error(); - - // 404 Not Found - $this->check404Error(); - - // 419 Page Expired (CSRF) - $this->check419Error(); - - // 429 Too Many Requests - $this->check429Error(); - - // 500 Internal Server Error - $this->check500Error(); - - // 502 Bad Gateway - $this->check502Error(); - - // 503 Service Unavailable - $this->check503Error(); - - // 504 Gateway Timeout - $this->check504Error(); - } - - private function check400Error(): void - { - $issues = []; - - // Check for cookie issues - if (config('session.driver') === 'file') { - $sessionPath = storage_path('framework/sessions'); - if (! is_writable($sessionPath)) { - $issues[] = 'Sessions directory not writable'; - } - } - - // Check for large POST data - $postMax = ini_get('post_max_size'); - $uploadMax = ini_get('upload_max_filesize'); - - if ((int) $postMax < 32 || (int) $uploadMax < 32) { - $issues[] = 'POST/upload limits too low'; - } - - if ($issues !== []) { - $this->addCheck('HTTP 400 (Bad Request)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 400 (Bad Request)', '✅', 'OK'); - } - } - - private function check401Error(): void - { - $issues = []; - - // Check .env authentication - if (config('app.env') === 'production' && empty(config('app.key'))) { - $issues[] = 'APP_KEY missing'; - } - - if ($issues !== []) { - $this->addCheck('HTTP 401 (Unauthorized)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 401 (Unauthorized)', '✅', 'OK'); - } - } - - private function check403Error(): void - { - $issues = []; - - // Check public/index.php exists - if (! File::exists(public_path('index.php'))) { - $issues[] = 'index.php missing'; - } - - // Check .htaccess (Apache) - if ($this->isApache() && ! File::exists(public_path('.htaccess'))) { - $issues[] = '.htaccess missing'; - } - - // Check storage permissions - $storagePath = storage_path(); - if (! is_writable($storagePath)) { - $issues[] = 'Storage not writable'; - } - - // Check public permissions - $publicPath = public_path(); - if (! is_readable($publicPath)) { - $issues[] = 'Public not readable'; - } - - // Check CSRF token mismatch - common issue - if (config('session.driver') !== 'file') { - $sessionDriver = config('session.driver'); - if ($sessionDriver === 'redis' && ! extension_loaded('redis')) { - $issues[] = 'Redis extension missing'; - } - } - - if ($issues !== []) { - $this->addCheck('HTTP 403 (Forbidden)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 403 (Forbidden)', '✅', 'OK'); - } - } - - private function check404Error(): void - { - $issues = []; - - // Check routes cache - if (File::exists(base_path('bootstrap/cache/routes.php')) && config('cache.default') !== 'array') { - // Routes might be cached but stale - } - - // Check public directory - if (! is_dir(public_path())) { - $issues[] = 'Public directory missing'; - } - - // Check storage symlink - if (! File::exists(public_path('storage'))) { - $issues[] = 'Storage symlink missing'; - } - - // Check .htaccess for proper rewrite rules - if ($this->isApache()) { - $htaccess = public_path('.htaccess'); - if (File::exists($htaccess)) { - $content = File::get($htaccess); - if (! str_contains($content, 'RewriteRule')) { - $issues[] = 'Missing rewrite rules'; - } - } - } - - // Check nginx config - if ($this->isNginx()) { - $nginxConfig = base_path('atom-nginx.conf'); - if (! File::exists($nginxConfig)) { - $issues[] = 'Nginx config not generated'; - } - } - - if ($issues !== []) { - $this->addCheck('HTTP 404 (Not Found)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 404 (Not Found)', '✅', 'OK'); - } - } - - private function check419Error(): void - { - $issues = []; - - // CSRF token expired - usually session related - if (config('session.lifetime') < 60) { - $issues[] = 'Session lifetime too short'; - } - - if (config('session.driver') === 'file') { - $sessionPath = storage_path('framework/sessions'); - if (! is_writable($sessionPath)) { - $issues[] = 'Sessions not writable'; - } - } - - if ($issues !== []) { - $this->addCheck('HTTP 419 (Page Expired)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 419 (Page Expired)', '✅', 'OK'); - } - } - - private function check429Error(): void - { - $issues = []; - - // Rate limiting issues - if (config('cache.default') === 'file') { - $issues[] = 'File cache for rate limiting (slow)'; - } - - if ($issues !== []) { - $this->addCheck('HTTP 429 (Too Many Requests)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 429 (Too Many Requests)', '✅', 'OK'); - } - } - - private function check500Error(): void - { - $issues = []; - - // Check PHP errors - $errorLog = ini_get('error_log'); - if (! in_array($errorLog, ['', '0', false], true) && File::exists($errorLog)) { - $recentErrors = $this->getRecentErrors($errorLog, 10); - if (count($recentErrors) > 5) { - $issues[] = 'Many PHP errors in log'; - } - } - - // Check debug mode - if (! config('app.debug')) { - // Debug is off - 500 errors won't show details - } - - // Check composer autoload - if (! File::exists(base_path('vendor/autoload.php'))) { - $issues[] = 'Composer dependencies not installed'; - } - - // Check bootstrap/cache - $bootstrapCache = base_path('bootstrap/cache'); - if (! is_writable($bootstrapCache)) { - $issues[] = 'Bootstrap cache not writable'; - } - - // Check storage permissions - if (! is_writable(storage_path())) { - $issues[] = 'Storage not writable'; - } - - // Check database connection - try { - DB::connection()->getPdo(); - } catch (\Exception) { - $issues[] = 'Database connection failed'; - } - - if ($issues !== []) { - $this->addCheck('HTTP 500 (Internal Error)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 500 (Internal Error)', '✅', 'OK'); - } - } - - private function check502Error(): void - { - $issues = []; - $isWindows = DIRECTORY_SEPARATOR === '\\'; - - // Check PHP-FPM (Linux only) - if (! $isWindows && ($this->isNginx() || $this->isApache())) { - $phpFpm = shell_exec('systemctl status php*-fpm 2>/dev/null | head -1'); - if (in_array($phpFpm, ['', '0', false, null], true)) { - $issues[] = 'PHP-FPM may not be running'; - } - } - - // Check socket files - if (! $isWindows) { - $phpVersion = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; - $socketPaths = [ - "/var/run/php/php{$phpVersion}-fpm.sock", - '/var/run/php-fpm.sock', - "/run/php/php{$phpVersion}-fpm.sock", - ]; - $socketExists = array_any($socketPaths, fn ($path) => File::exists($path)); - - if (! $socketExists) { - $issues[] = 'PHP-FPM socket not found'; - } - } else { - // On Windows with nginx, check PHP-CGI - $phpCgiRunning = shell_exec('tasklist 2>nul | findstr php-cgi'); - if (in_array($phpCgiRunning, ['', '0', false, null], true)) { - $issues[] = 'PHP-CGI may not be running'; - } - } - - if ($issues !== []) { - $this->addCheck('HTTP 502 (Bad Gateway)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 502 (Bad Gateway)', '✅', 'OK'); - } - } - - private function check503Error(): void - { - $issues = []; - - // Check maintenance mode - $maintenanceFile = storage_path('framework/maintenance.php'); - if (File::exists($maintenanceFile)) { - $issues[] = 'Maintenance mode enabled'; - } - - // Check queue workers - if (config('queue.default') !== 'sync') { - $result = $this->runSystemCommand('pgrep -f "queue:work"'); - if (in_array(trim((string) $result['output']), ['', '0'], true)) { - $issues[] = 'Queue workers not running'; - } - } - - // Check database connection - try { - DB::connection()->getPdo(); - } catch (\Exception) { - $issues[] = 'Database unavailable'; - } - - // Check Redis - if (config('cache.default') === 'redis') { - try { - Redis::connection()->ping(); - } catch (\Exception) { - $issues[] = 'Redis unavailable'; - } - } - - if ($issues !== []) { - $this->addCheck('HTTP 503 (Unavailable)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 503 (Unavailable)', '✅', 'OK'); - } - } - - private function check504Error(): void - { - $issues = []; - - // Check PHP execution time - $maxExecutionTime = ini_get('max_execution_time'); - if ($maxExecutionTime < 60 && $maxExecutionTime != 0) { - $issues[] = 'Max execution time too low'; - } - - // Check database query time - try { - DB::connection()->getPdo(); - } catch (\Exception) { - $issues[] = 'Database timeout'; - } - - // Check Redis timeout - if (config('cache.default') === 'redis') { - try { - Redis::connection()->ping(); - } catch (\Exception) { - $issues[] = 'Redis timeout'; - } - } - - // Check PHP-FPM timeout - if ($this->isNginx()) { - $nginxConfig = base_path('atom-nginx.conf'); - if (File::exists($nginxConfig)) { - $content = File::get($nginxConfig); - if (! str_contains($content, 'fastcgi_read_timeout')) { - $issues[] = 'Nginx fastcgi timeout not set'; - } - } - } - - if ($issues !== []) { - $this->addCheck('HTTP 504 (Gateway Timeout)', '⚠️', implode(', ', $issues)); - } else { - $this->addCheck('HTTP 504 (Gateway Timeout)', '✅', 'OK'); - } - } - - private function getRecentErrors(string $logFile, int $lines = 10): array - { - if (! File::exists($logFile)) { - return []; - } - - $content = File::get($logFile); - $arr = explode("\n", $content); - - return array_slice($arr, -$lines); - } - - private function checkRedisConnection(): void - { - if (! extension_loaded('redis')) { - return; - } - - $redisClass = '\Redis'; - if (class_exists($redisClass)) { - try { - $redis = new $redisClass; - $host = config('database.redis.default.host', '127.0.0.1'); - $port = config('database.redis.default.port', 6379); - $redis->connect($host, $port); - if ($pass = config('database.redis.default.password')) { - $redis->auth($pass); - } - - if ($redis->ping()) { - $this->addCheck('Redis', '✅', 'Connected'); - } else { - $this->addCheck('Redis', '❌', 'Ping Failed'); - $this->errors++; - } - } catch (\Exception $e) { - $this->addCheck('Redis', '❌', 'Failed: ' . $e->getMessage()); - $this->errors++; - } - } - } - - private function checkCronJobsStatus(): void - { - if ($this->isWindows()) { - return; - } - $cronResult = $this->runSystemCommand('crontab -l'); - $cron = $cronResult['output']; - $ok = str_contains((string) $cron, 'schedule:run'); - $this->addCheck('Laravel Cron', $ok ? '✅' : '⚠️', $ok ? 'Active' : 'Missing'); - } - - private function checkQueueWorkersStatus(): void - { - if ($this->isWindows()) { - $result = $this->runSystemCommand('tasklist /FI "IMAGENAME eq php.exe" /FO CSV'); - $ok = array_filter(explode("\n", (string) $result['output']), fn ($line) => str_contains((string) $line, 'queue:work')) !== []; - } else { - $result = $this->runSystemCommand('pgrep -f "queue:work"'); - $ok = ! in_array(trim((string) $result['output']), ['', '0'], true); - } - - $this->addCheck('Queue Worker', $ok ? '✅' : '⚠️', $ok ? 'Running' : 'Stopped'); - } - - private function checkSupervisorConfig(): void - { - if ($this->isLinux()) { - $path = '/etc/supervisor/conf.d/atom-worker.conf'; - $exists = File::exists($path) || File::exists('/etc/supervisor.d/atom.ini'); - $this->addCheck('Supervisor', $exists ? '✅' : '⚠️', $exists ? 'Found' : 'Missing'); - if (! $exists) { - $this->warnings++; - } - } - } - - private function checkRequiredFiles(): void - { - if (! File::exists(public_path('index.php'))) { - $this->addCheck('public/index.php', '❌', 'Missing'); - } - } - - // --- REPAIRS --- - - private function createAdminUser(?string $username = null, ?string $email = null, ?string $password = null): void - { - if ($username === null) { - $username = $this->ask('Enter Admin Username', 'Admin'); - } - if ($email === null) { - $email = $this->ask('Enter Admin Email', 'admin@example.com'); - } - if ($password === null) { - $password = $this->secret('Enter Admin Password'); - } - - if (empty($password)) { - $this->error('❌ Password is required.'); - - return; - } - - try { - $existingAdmin = DB::table('users')->where('username', $username)->orWhere('rank', '>=', 7)->first(); - - if ($existingAdmin) { - $this->info("✅ Admin user already exists ({$existingAdmin->username})"); - - return; - } - - DB::table('users')->insert([ - 'username' => $username, - 'mail' => $email, - 'password' => Hash::make($password), - 'rank' => 7, - 'look' => 'hr-115-42.hd-190-1.ch-215-66.lg-270-66.sh-300-66', - 'motto' => 'Atom CMS Admin', - 'ip_register' => '127.0.0.1', - 'ip_current' => '127.0.0.1', - 'account_created' => time(), - 'last_login' => time(), - ]); - $this->info("✅ Admin user '{$username}' created."); - } catch (\Exception $e) { - $this->error('❌ Failed: ' . $e->getMessage()); - } - } - - // --- REPAIRS --- - - private function buildFrontendAssets(): void - { - if ($this->skipBuild) { - $this->info('⏭️ Asset building skipped'); - - return; - } - - $isWindows = DIRECTORY_SEPARATOR === '\\'; - - if ($isWindows) { - $this->info('⏭️ Asset building skipped on Windows'); - - return; - } - - $manager = 'npm'; - $cmd = 'npm install && npm run build'; - - if (File::exists(base_path('yarn.lock'))) { - $manager = 'yarn'; - $cmd = 'yarn && yarn build'; - } elseif (File::exists(base_path('pnpm-lock.yaml'))) { - $manager = 'pnpm'; - $cmd = 'pnpm install && pnpm build'; - } - - $this->info("🚀 Building assets with {$manager}..."); - exec("{$cmd} 2>&1", $output, $exitCode); - - if ($exitCode === 0) { - $this->info('✅ Assets built.'); - } else { - $this->warn('⚠️ Build failed - skipped'); - } - } - - private function generateSupervisorConfig(): void - { - if ($this->isWindows()) { - $this->generateWindowsQueueService(); - - return; - } - - $path = base_path('atom-worker.conf'); - $user = $this->webUser; - $root = base_path(); - $content = "[program:atom-worker] -process_name=%(program_name)s_%(process_num)02d -command=php {$root}/artisan queue:work --sleep=3 --tries=3 --max-time=3600 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user={$user} -numprocs=2 -redirect_stderr=true -stdout_logfile={$root}/storage/logs/worker.log -"; - File::put($path, $content); - - $supervisorPath = '/etc/supervisor/conf.d/atom-worker.conf'; - - $isRoot = function_exists('posix_getuid') && posix_getuid() === 0; - - if ($this->isWindows()) { - $this->info('⏭️ Supervisor config not needed on Windows'); - - return; - } - - if ($isRoot) { - @copy($path, $supervisorPath); - $this->runSystemCommand('sudo supervisorctl reread', true); - $this->runSystemCommand('sudo supervisorctl update', true); - $this->runSystemCommand('sudo supervisorctl start atom-worker:*', true); - $this->info("✅ Generated and installed 'atom-worker.conf' to {$supervisorPath}"); - } else { - @copy($path, $supervisorPath); - $this->info("✅ Generated 'atom-worker.conf'. Installed to {$supervisorPath}"); - $this->info(' Run: sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start atom-worker:*'); - } - } - - private function generateWindowsQueueService(): void - { - $root = base_path(); - $batPath = base_path('queue-worker.bat'); - $content = "@echo off -cd /d \"{$root}\" -:start -php artisan queue:work --sleep=3 --tries=3 --max-time=3600 -goto start -"; - File::put($batPath, $content); - - $this->info("✅ Generated 'queue-worker.bat'"); - $this->info(' To run as Windows Service:'); - $this->info(' 1. Download NSSM: https://nssm.cc/download'); - $this->info(" 2. Run: nssm install AtomWorker \"{$batPath}\""); - $this->info(' 3. Or use Task Scheduler: taskschd.msc'); - } - - private function repairApache(): void - { - $path = public_path('.htaccess'); - $rules = "\n \n Options -MultiViews -Indexes\n \n\n RewriteEngine On\n RewriteCond %{REQUEST_FILENAME} !-d\n RewriteCond %{REQUEST_FILENAME} !-f\n RewriteRule ^ index.php [L]\n"; - File::put($path, $rules); - if ($this->isLinux()) { - exec('sudo a2enmod rewrite 2>/dev/null'); - } - } - - private function repairIIS(): void - { - $path = public_path('web.config'); - $data = ''; - File::put($path, $data); - } - - private function repairNginx(): void - { - $phpVer = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; - $template = 'server { - listen 80; - server_name example.com; - root ' . public_path() . "; - add_header X-Frame-Options \"SAMEORIGIN\"; - add_header X-Content-Type-Options \"nosniff\"; - index index.php; - charset utf-8; - location / { - try_files \$uri \$uri/ /index.php?\$query_string; - } - location = /favicon.ico { access_log off; log_not_found off; } - location = /robots.txt { access_log off; log_not_found off; } - error_page 404 /index.php; - location ~ \.php$ { - fastcgi_pass unix:/var/run/php/php{$phpVer}-fpm.sock; - fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name; - include fastcgi_params; - } - location ~ /\.(?!well-known).* { - deny all; - } -}"; - File::put(base_path('atom-nginx.conf'), $template); - $this->info('✅ Template created: atom-nginx.conf'); - } - - private function repairPermissions(): void - { - $isWindows = DIRECTORY_SEPARATOR === '\\'; - - if ($isWindows) { - $paths = [base_path('storage'), base_path('bootstrap/cache'), public_path('uploads')]; - - foreach ($paths as $path) { - if (! file_exists($path)) { - @mkdir($path, 0755, true); - $this->info(" ✅ Created: {$path}"); - - continue; - } - if (is_dir($path)) { - if ($this->isXampp() || $this->isWamp() || $this->isNginx()) { - @chmod($path, 0777); - $this->recursiveChmod($path, 0777); - } else { - @chmod($path, 0755); - $this->recursiveChmod($path, 0755); - } - } - } - $this->info('✅ Windows permissions fixed'); - - return; - } - - // Linux permissions - $paths = [base_path('storage'), base_path('bootstrap/cache'), public_path('uploads')]; - - foreach ($paths as $path) { - if (! file_exists($path)) { - @mkdir($path, 0755, true); - $this->info(" ✅ Created: {$path}"); - - continue; - } - if (is_dir($path)) { - @exec("sudo chown -R {$this->webUser}:{$this->webGroup} {$path} 2>/dev/null"); - @exec("find {$path} -type d -exec chmod 775 {} \; 2>/dev/null"); - @exec("find {$path} -type f -exec chmod 664 {} \; 2>/dev/null"); - } - } - $this->info('✅ Linux permissions fixed'); - } - - private function recursiveChmod(string $dir, int $mode): void - { - if (! is_dir($dir)) { - return; - } - - $files = @array_diff(scandir($dir), ['.', '..']); - if ($files === false) { - return; - } - - foreach ($files as $file) { - $path = $dir . DIRECTORY_SEPARATOR . $file; - if (is_dir($path)) { - @chmod($path, $mode); - $this->recursiveChmod($path, $mode); - } else { - @chmod($path, $mode); - } - } - } - - private function repairLinuxPermissions(): void - { - $this->repairPermissions(); - } - - private function fixKnownCommonErrors(): void - { - $this->info('🔧 Fixing known common errors...'); - // Clear all Laravel caches - $this->call('cache:clear'); - $this->call('view:clear'); - $this->call('route:clear'); - $this->call('config:clear'); - $this->call('event:clear'); - $this->call('clear-compiled'); - $this->info('✅ All caches cleared'); - // Fix storage permissions - $this->repairPermissions(); - $this->info('✅ Storage permissions fixed'); - // Fix bootstrap/cache permissions - $cachePath = base_path('bootstrap/cache'); - if (is_dir($cachePath)) { - if ($this->isWindows()) { - @chmod($cachePath, 0777); - } else { - @chmod($cachePath, 0755); - exec("sudo chown -R {$this->webUser}:{$this->webGroup} {$cachePath} 2>/dev/null"); - } - } - // Fix sessions table if needed - if (Schema::hasTable('sessions')) { - try { - DB::statement('OPTIMIZE TABLE sessions'); - $this->info('✅ Sessions table optimized'); - } catch (\Exception) { - // Ignore - } - } - // Ensure storage directories exist - $directories = [ - storage_path('app'), - storage_path('app/public'), - storage_path('framework/cache'), - storage_path('framework/cache/data'), - storage_path('framework/sessions'), - storage_path('framework/views'), - storage_path('logs'), - ]; - foreach ($directories as $dir) { - if (! is_dir($dir)) { - @mkdir($dir, 0755, true); - } - } - $this->info('✅ Storage directories ensured'); - // Fix radio tables - always run to ensure they exist - $this->createRadioTables(); - // Fix .env if needed - if (! File::exists(base_path('.env'))) { - File::copy(base_path('.env.example'), base_path('.env')); - $this->info('✅ .env file created from example'); - } - // Fix APP_KEY if missing - if (empty(config('app.key'))) { - $this->call('key:generate', ['--force' => true]); - $this->info('✅ APP_KEY generated'); - } - // Fix database if using SQLite (common error) - if (config('database.default') === 'sqlite') { - $dbPath = database_path('database.sqlite'); - if (! File::exists($dbPath)) { - touch($dbPath); - chmod($dbPath, 0755); - $this->info('✅ SQLite database created'); - } - } - // Check and fix common migration issues - try { - $this->call('migrate:status', ['--format' => 'compact']); - } catch (\Exception) { - $this->warn('⚠️ Migration status check failed'); - } - // Fix queue table if using database driver - if (config('queue.default') === 'database') { - if (! Schema::hasTable('jobs')) { - $this->call('queue:table', ['--force' => true]); - $this->info('✅ Queue jobs table created'); - } - if (! Schema::hasTable('failed_jobs')) { - $this->call('queue:failed-table', ['--create' => true]); - $this->info('✅ Failed jobs table created'); - } - } - // Fix broadcasting if using Pusher - if (config('broadcasting.default') === 'pusher' && empty(config('broadcasting.connections.pusher.key'))) { - $this->warn('⚠️ Pusher credentials missing in config'); - } - // Fix Redis connection if configured but not connected - if (config('cache.default') === 'redis') { - try { - Redis::connection()->ping(); - } catch (\Exception) { - $this->warn('⚠️ Redis not connected, falling back to file cache'); - // Can't auto-switch, just warn - } - } - $this->info('✅ Known common errors fixed'); - } - - private function repairPHPConfig(): bool - { - if (! is_writable($this->phpIniPath)) { - return false; - } - $c = File::get($this->phpIniPath); - $c = preg_replace(['/memory_limit\s*=.*/', '/upload_max_filesize\s*=.*/', '/post_max_size\s*=.*/'], ['memory_limit=512M', 'upload_max_filesize=64M', 'post_max_size=64M'], $c); - - return (bool) File::put($this->phpIniPath, $c); - } - - private function repairPHPExtensions(): int - { - $v = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; - $missing = []; - foreach (['gd', 'zip', 'redis', 'intl', 'bcmath', 'xml'] as $ext) { - if (! extension_loaded($ext)) { - $missing[] = $ext; - } - } - if ($missing === []) { return 0; } - if ($this->isWindows()) { - $this->info('Installing PHP extensions on Windows...'); - - // Check for Chocolatey - $chocoExists = shell_exec('where choco 2>nul'); - - if (! in_array($chocoExists, ['', '0', false, null], true)) { - foreach ($missing as $ext) { - $this->info("Installing PHP {$ext} via Chocolatey..."); - // Try to install PHP extension via choco - shell_exec("choco install php{$v}-{$ext} -y 2>nul"); - } - } else { - $this->warn('Cannot auto-install extensions on Windows without Chocolatey'); - $this->warn('Missing extensions: ' . implode(', ', $missing)); - $this->warn('To install manually:'); - $this->warn('1. Download PHP extensions from: https://windows.php.net/downloads/pecl/releases/'); - $this->warn('2. Or enable in php.ini: extension=php_' . implode('.dll, extension=php_', $missing) . '.dll'); - } - - return count($missing); - } - - // Linux - $pm = (shell_exec('which apt 2>/dev/null')) ? 'apt' : ((shell_exec('which yum 2>/dev/null')) ? 'yum' : 'pacman'); - $isWindows = DIRECTORY_SEPARATOR === '\\'; - - foreach ($missing as $ext) { - if ($isWindows) { - $this->warn("⚠️ Cannot install {$ext} on Windows - install manually"); - - continue; - } - - $pkg = ($pm === 'apt') ? "php{$v}-{$ext}" : "php-{$ext}"; - $this->info("Installing {$pkg}..."); - - // Check if we can install without sudo (already root) - $isRoot = function_exists('posix_getuid') && posix_getuid() === 0; - $cmd = $isRoot ? "{$pm} install -y {$pkg}" : "sudo {$pm} install -y {$pkg}"; - exec("{$cmd} 2>/dev/null", $output, $exitCode); - - if ($exitCode !== 0 && ! $isRoot) { - $this->warn("⚠️ Cannot install {$pkg} - run manually: sudo {$pm} install -y {$pkg}"); - } - } - - return count($missing); + return 1; } - private function updatePhpVersion(): int - { - if ($this->isLinux()) { - $this->info('Adding PHP repository...'); - exec('sudo add-apt-repository -y ppa:ondrej/php && sudo apt-get update 2>/dev/null'); - $this->warn("Repository added. Run 'sudo apt upgrade' to finish."); - - return 1; - } - - return 0; - } - - private function createRadioTables(): void - { - $radioTables = [ - 'radio_song_votes', - 'radio_contests', - 'radio_giveaways', - 'radio_listener_points', - 'radio_song_requests', - 'radio_ranks', - 'radio_shouts', - 'radio_schedules', - 'radio_banners', - 'radio_history', - 'radio_applications', - ]; - - $this->info('🔊 Checking radio tables...'); - - $missingTables = []; - foreach ($radioTables as $table) { - if (! Schema::hasTable($table)) { - $missingTables[] = $table; - } - } - - if ($this->isWindows()) { - $mysqlPath = $this->findMysql(); - if ($mysqlPath) { - $mysqlDir = dirname($mysqlPath); - $currentPath = getenv('PATH') ?: ''; - if (! str_contains($currentPath, $mysqlDir)) { - putenv("PATH={$currentPath};{$mysqlDir}"); - } - } - } - - if ($missingTables !== []) { - $this->info('📦 Creating ' . count($missingTables) . ' missing radio tables...'); - - // Run all migrations - if ($this->skipMigrations) { - $this->info('⏭️ Migrations skipped (--skip-migrations)'); - } else { - if ($this->isWindows()) { - $this->addMysqlToPath(); - } - if ($this->skipDuplicates) { - $this->runMigrationsWithSkip(); - } else { - $this->call('migrate', ['--force' => true]); - } - } - } - - // Run all radio seeders - $radioSeeders = [ - RadioSettingsSeeder::class, - RadioContestSeeder::class, - RadioGiveawaySeeder::class, - RadioSongRequestSeeder::class, - RadioSongVoteSeeder::class, - RadioListenerPointSeeder::class, - RadioTestSeeder::class, - ]; - - foreach ($radioSeeders as $seeder) { - if (class_exists($seeder)) { - try { - $this->call('db:seed', ['--class' => $seeder, '--force' => true]); - } catch (\Exception) { - // Ignore individual seeder failures - } - } - } - - // Enable radio in settings - try { - DB::table('website_settings')->updateOrInsert( - ['key' => 'radio_enabled'], - ['value' => '1', 'comment' => 'Radio enabled (0=no, 1=yes)'], - ); - $this->info('✅ Radio enabled'); - } catch (\Exception) { - // Ignore - } - - $this->info('✅ Radio tables and data created'); - } - - private function installRedisIfNeeded(int &$f): void - { - if ($this->isLinux()) { - $this->info('Installing Redis Server (Linux)...'); - exec('sudo apt-get install -y redis-server 2>/dev/null'); - exec('sudo systemctl enable redis-server && sudo systemctl start redis-server 2>/dev/null'); - $f++; - - return; - } - - if ($this->isWindows()) { - $this->info('Installing Redis Server (Windows)...'); - - // Check if Redis is already installed - $redisExists = shell_exec('where redis-server 2>nul') ?: shell_exec('where redis 2>nul'); - - if (in_array($redisExists, ['', '0', false, null], true)) { - // Try to install via Chocolatey - $chocoExists = shell_exec('where choco 2>nul'); - - if (! in_array($chocoExists, ['', '0', false, null], true)) { - $this->info('Installing Redis via Chocolatey...'); - shell_exec('choco install redis-64 -y 2>nul'); - } else { - $this->warn('Redis not installed. To install on Windows:'); - $this->warn('1. Download Redis from: https://github.com/microsoftarchive/redis/releases'); - $this->warn('2. Or install via Chocolatey: choco install redis-64'); - $this->warn('3. Or use Memurai/Redis Windows: https://www.memurai.com/'); - } - } - - // Check if Redis is running - $redisRunning = shell_exec('sc query Redis 2>nul') ?: shell_exec('sc query redis 2>nul'); - - if (in_array($redisRunning, ['', '0', false, null], true)) { - $this->info('Starting Redis service...'); - shell_exec('net start redis 2>nul') ?: shell_exec('sc start redis 2>nul'); - } - - $f++; - - return; - } - } - - private function fixEverything(bool $auto = false): int - { - $fixed = 0; - $errors = []; - $interactive = ! $auto; - - $this->info(''); - $this->info('════════════════════════════════════════════════════════════'); - $this->info(' FIXING EVERYTHING IN ATOMCMS '); - $this->info('════════════════════════════════════════════════════════════'); - $this->info(''); - - // Step 1: Environment - try { - $this->info('[1/11] 🔧 Fixing Environment...'); - - $envExists = File::exists(base_path('.env')); - $appKeySet = ! empty(config('app.key')); - - if ($envExists && $appKeySet && $interactive) { - $this->warn(' ⚠️ Environment looks good already!'); - if (! $this->confirm(' 💡 Weet je zeker dat je dit opnieuw wilt doen?', false)) { - $this->info(' ⏭️ Overgeslagen'); - goto step2; - } - } - - if (! $envExists) { - File::copy(base_path('.env.example'), base_path('.env')); - $this->info(' ✅ .env created'); - } - if (! $appKeySet) { - $this->call('key:generate', ['--force' => true]); - $this->info(' ✅ APP_KEY generated'); - } - if ($envExists && $appKeySet) { - $this->info(' ✅ Environment was already good'); - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Environment: ' . $e->getMessage(); - $this->warn(' ⚠️ Environment error: ' . $e->getMessage()); - } - - step2: - // Step 2: Clear all caches - try { - $this->info('[2/11] 🗑️ Clearing all caches...'); - - if ($interactive && ! $this->confirm(' 💡 Caches legen? (kan helpen bij problemen)', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto step3; - } - - $this->call('cache:clear'); - $this->call('view:clear'); - $this->call('route:clear'); - $this->call('config:clear'); - $this->call('event:clear'); - $this->call('clear-compiled'); - $this->call('optimize:clear'); - $this->info(' ✅ All caches cleared'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Cache clear: ' . $e->getMessage(); - $this->warn(' ⚠️ Cache clear error: ' . $e->getMessage()); - } - - step3: - // Step 3: Fix permissions (with multiple fallbacks) - try { - $this->info('[3/11] 🔐 Fixing permissions...'); - - $storageOk = is_writable(storage_path()) && is_writable(public_path('uploads')); - if ($storageOk && $interactive) { - $this->warn(' ⚠️ Permissions zien er goed uit!'); - if (! $this->confirm(' 💡 Weet je zeker dat je permissions opnieuw wilt fixen?', false)) { - $this->info(' ⏭️ Overgeslagen'); - goto step4; - } - } - - $permissionSuccess = false; - - // Attempt 1: Standard repair - try { - $this->repairPermissions(); - $permissionSuccess = true; - $this->info(' ✅ Permissions fixed'); - } catch (\Exception) { - $this->warn(' ⚠️ Standard repair failed, trying chmod...'); - } - - // Attempt 2: Direct chmod - if (! $permissionSuccess) { - try { - $dirs = [storage_path(), base_path('bootstrap/cache'), public_path('uploads')]; - foreach ($dirs as $dir) { - if (is_dir($dir)) { - @chmod($dir, 0775); - $this->runSystemCommand("chmod -R 775 {$dir} 2>/dev/null"); - } - } - $permissionSuccess = true; - $this->info(' ✅ Permissions fixed (chmod)'); - } catch (\Exception) { - $this->warn(' ⚠️ chmod failed, trying sudo...'); - } - } - - // Attempt 3: sudo chmod - if (! $permissionSuccess && $this->isLinux()) { - try { - $this->runSystemCommand('sudo chmod -R 775 ' . storage_path() . ' 2>/dev/null'); - $this->runSystemCommand('sudo chmod -R 775 ' . base_path('bootstrap/cache') . ' 2>/dev/null'); - $this->runSystemCommand('sudo chmod -R 775 ' . public_path('uploads') . ' 2>/dev/null'); - $permissionSuccess = true; - $this->info(' ✅ Permissions fixed (sudo)'); - } catch (\Exception) { - $this->warn(' ⚠️ All permission attempts failed'); - $errors[] = 'Permissions: All 3 attempts failed'; - } - } - - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Permissions: ' . $e->getMessage(); - $this->warn(' ⚠️ Permissions error: ' . $e->getMessage()); - } - - step4: - // Step 4: Run ALL migrations (with multiple fallbacks) - try { - $this->info('[4/11] 🗄️ Running ALL migrations...'); - - if ($interactive && ! $this->confirm(' 💡 Database migrations draaien?', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto step5; - } - - if ($this->skipMigrations) { - $this->info(' ⏭️ Migrations skipped (--skip-migrations)'); - } else { - if ($this->isWindows()) { - $this->addMysqlToPath(); - } - - $migrationSuccess = false; - - // Attempt 1: Normal migration - try { - $this->call('migrate', ['--force' => true]); - $migrationSuccess = true; - $this->info(' ✅ Migrations complete'); - } catch (\Exception) { - $this->warn(' ⚠️ Normal migration failed, trying with --step...'); - } - - // Attempt 2: Migration with --step (if normal failed) - if (! $migrationSuccess) { - try { - $this->call('migrate', ['--force' => true, '--step' => true]); - $migrationSuccess = true; - $this->info(' ✅ Migrations complete (step mode)'); - } catch (\Exception) { - $this->warn(' ⚠️ Step migration failed, trying rollback + migrate...'); - } - } - - // Attempt 3: Rollback and migrate fresh (if still failed) - if (! $migrationSuccess) { - try { - $this->call('migrate:rollback', ['--force' => true, '--step' => 1]); - $this->call('migrate', ['--force' => true]); - $migrationSuccess = true; - $this->info(' ✅ Migrations complete (rollback + migrate)'); - } catch (\Exception) { - $this->warn(' ⚠️ Rollback failed, trying fresh...'); - } - } - - // Attempt 4: NEVER USE migrate:fresh - it deletes all data! - // Only show warning if all attempts failed - if (! $migrationSuccess) { - $this->warn(' ⚠️ All migration attempts failed'); - $this->warn(' 💡 Run manually: php artisan migrate --force'); - $errors[] = 'Migrations: All attempts failed (manual fix needed)'; - } - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Migrations: ' . $e->getMessage(); - $this->warn(' ⚠️ Migration error: ' . $e->getMessage()); - } - - step5: - // Step 5: Run ALL seeders (with multiple fallbacks) - try { - $this->info('[5/11] 🌱 Running ALL seeders...'); - - if ($interactive && ! $this->confirm(' 💡 Database seeders draaien?', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto step6; - } - - $seederSuccess = false; - - // Attempt 1: Normal seeder - try { - $this->call('db:seed', ['--force' => true]); - $seederSuccess = true; - $this->info(' ✅ All seeders complete'); - } catch (\Exception $e1) { - if (str_contains($e1->getMessage(), 'Duplicate entry') || str_contains($e1->getMessage(), 'already exists')) { - $seederSuccess = true; - $this->info(' ✅ Seeders already applied (duplicates ignored)'); - } else { - $this->warn(' ⚠️ Normal seeder failed, trying individual seeders...'); - } - } - - // Attempt 2: Run individual seeders (if normal failed) - if (! $seederSuccess) { - try { - $seeders = $this->discoverAllSeeders(); - $seededCount = 0; - foreach ($seeders as $seeder) { - try { - $this->call('db:seed', ['--class' => $seeder, '--force' => true]); - $seededCount++; - } catch (\Exception) { - // Skip individual seeder errors - } - } - if ($seededCount > 0) { - $seederSuccess = true; - $this->info(" ✅ {$seededCount} seeders completed"); - } - } catch (\Exception) { - $this->warn(' ⚠️ Individual seeders failed, trying with --class option...'); - } - } - - // Attempt 3: Try with class option - if (! $seederSuccess) { - try { - $this->call('db:seed', ['--class' => 'DatabaseSeeder', '--force' => true]); - $seederSuccess = true; - $this->info(' ✅ Seeders complete (with class option)'); - } catch (\Exception) { - $this->warn(' ⚠️ All seeder attempts failed'); - $errors[] = 'Seeders: All 3 attempts failed'; - } - } - - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Seeders: ' . $e->getMessage(); - $this->warn(' ⚠️ Seeder error: ' . $e->getMessage()); - } - - step6: - // Step 6: Fix storage (with multiple fallbacks) - try { - $this->info('[6/11] 📁 Fixing storage...'); - - $storageLinkExists = File::exists(public_path('storage')); - if ($storageLinkExists && $interactive) { - $this->warn(' ⚠️ Storage symlink bestaat al!'); - if (! $this->confirm(' 💡 Weet je zeker dat je storage opnieuw wilt fixen?', false)) { - $this->info(' ⏭️ Overgeslagen'); - goto step6b; - } - } - - $storageSuccess = false; - - // Attempt 1: Laravel storage:link - try { - if (File::exists(public_path('storage'))) { - @unlink(public_path('storage')); - } - $this->call('storage:link'); - $storageSuccess = true; - $this->info(' ✅ Storage symlink created'); - } catch (\Exception) { - $this->warn(' ⚠️ storage:link failed, trying manual symlink...'); - } - - // Attempt 2: Manual symlink - if (! $storageSuccess) { - try { - if (File::exists(public_path('storage'))) { - @unlink(public_path('storage')); - } - symlink(storage_path('app/public'), public_path('storage')); - $storageSuccess = true; - $this->info(' ✅ Storage symlink created (manual)'); - } catch (\Exception) { - $this->warn(' ⚠️ Manual symlink failed, trying copy...'); - } - } - - // Attempt 3: Copy instead of symlink - if (! $storageSuccess) { - try { - $this->runSystemCommand('cp -r ' . storage_path('app/public') . ' ' . public_path('storage')); - $storageSuccess = true; - $this->info(' ✅ Storage copied (no symlink)'); - } catch (\Exception) { - $this->warn(' ⚠️ All storage attempts failed'); - $errors[] = 'Storage: All 3 attempts failed'; - } - } - - // Create all required directories - $directories = [ - storage_path('app'), - storage_path('app/public'), - storage_path('framework/cache'), - storage_path('framework/cache/data'), - storage_path('framework/sessions'), - storage_path('framework/views'), - storage_path('logs'), - ]; - foreach ($directories as $dir) { - if (! is_dir($dir)) { - @mkdir($dir, 0755, true); - } - } - $this->info(' ✅ Storage directories created'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Storage: ' . $e->getMessage(); - $this->warn(' ⚠️ Storage error: ' . $e->getMessage()); - } - - // Step 5b: Supervisor config - if ($this->isLinux()) { - try { - $this->info('[5b/11] 🐘 Generating Supervisor config...'); - - $supervisorPath = '/etc/supervisor/conf.d/atom-worker.conf'; - $supervisorExists = File::exists($supervisorPath) || File::exists('/etc/supervisor.d/atom.ini'); - - if ($supervisorExists) { - $this->info(' ✅ Supervisor config exists'); - } else { - if ($interactive && ! $this->confirm(' 💡 Supervisor config genereren?', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto step6b; - } - - $this->generateSupervisorConfig(); - $this->info(' ✅ Supervisor config generated'); - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Supervisor: ' . $e->getMessage(); - $this->warn(' ⚠️ Supervisor error: ' . $e->getMessage()); - } - } - - step6b: - // Step 6b: Fix Radio - try { - $this->info('[6c/12] 📻 Fixing Radio tables...'); - - if ($interactive && ! $this->confirm(' 💡 Radio tabellen fixen?', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto step7; - } - - $this->createRadioTables(); - $this->info(' ✅ Radio tables fixed'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Radio: ' . $e->getMessage(); - $this->warn(' ⚠️ Radio error: ' . $e->getMessage()); - } - - step7: - // Step 7: Create admin user if not exists - try { - $this->info('[7/12] 👤 Checking admin user...'); - - $hasAdmin = DB::table('users')->where('rank', '>=', 7)->exists(); - if ($hasAdmin) { - $this->warn(' ⚠️ Admin gebruiker bestaat al!'); - if ($interactive && ! $this->confirm(' 💡 Weet je zeker dat je een nieuwe admin wilt aanmaken?', false)) { - $this->info(' ⏭️ Overgeslagen'); - goto step8; - } - } - - if (! $hasAdmin) { - $this->createAdminUser('Admin', 'admin@atom.local', 'admin123'); - $this->info(' ✅ Admin user created'); - } else { - $this->info(' ✅ Admin user exists (was al goed)'); - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Admin user: ' . $e->getMessage(); - $this->warn(' ⚠️ Admin error: ' . $e->getMessage()); - $this->warn(' 💡 Create admin manually: php artisan make:admin'); - } - - step8: - // Step 8: Fix web server config - try { - $this->info('[8/12] 🌐 Fixing web server config...'); - - if ($interactive && ! $this->confirm(' 💡 Web server config opnieuw genereren?', false)) { - $this->info(' ⏭️ Overgeslagen'); - goto step9; - } - - if ($this->isApache() || $this->isWamp()) { - $this->repairApache(); - $this->info(' ✅ Apache config fixed'); - } elseif ($this->isIIS()) { - $this->repairIIS(); - $this->info(' ✅ IIS config fixed'); - } elseif ($this->isNginx() || $this->isXampp()) { - $this->repairNginx(); - $this->info(' ✅ Nginx config generated'); - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Web server: ' . $e->getMessage(); - $this->warn(' ⚠️ Web server error: ' . $e->getMessage()); - } - - step9: - // Step 9: Fix PHP config - try { - $this->info('[9/12] ⚙️ Optimizing PHP config...'); - - if ($interactive && ! $this->confirm(' 💡 PHP config optimaliseren?', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto step10; - } - - $this->repairPHPConfig(); - $this->info(' ✅ PHP config optimized'); - - $this->repairPHPExtensions(); - $this->info(' ✅ PHP extensions checked'); - - $this->call('config:cache'); - $this->info(' ✅ Config cache created'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'PHP config: ' . $e->getMessage(); - $this->warn(' ⚠️ PHP config error: ' . $e->getMessage()); - } - - step10: - // Step 10: Build assets (with multiple fallbacks) - try { - $this->info('[10/12] 🎨 Building assets...'); - - if ($interactive && ! $this->confirm(' 💡 Frontend assets opnieuw bouwen? (kan lang duren)', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto step11; - } - - $buildSuccess = false; - - // Attempt 1: buildFrontendAssets method - try { - $this->buildFrontendAssets(); - $buildSuccess = true; - $this->info(' ✅ Assets built'); - } catch (\Exception) { - $this->warn(' ⚠️ Standard build failed, trying npm run build...'); - } - - // Attempt 2: npm run build - if (! $buildSuccess) { - try { - $result = $this->runSystemCommand('cd ' . base_path() . ' && npm run build 2>&1'); - if ($result['success'] || str_contains((string) $result['output'], 'built in')) { - $buildSuccess = true; - $this->info(' ✅ Assets built (npm run build)'); - } - } catch (\Exception) { - $this->warn(' ⚠️ npm run build failed, trying npm run build:atom...'); - } - } - - // Attempt 3: npm run build:atom - if (! $buildSuccess) { - try { - $result = $this->runSystemCommand('cd ' . base_path() . ' && npm run build:atom 2>&1'); - if ($result['success'] || str_contains((string) $result['output'], 'built in')) { - $buildSuccess = true; - $this->info(' ✅ Assets built (npm run build:atom)'); - } - } catch (\Exception) { - $this->warn(' ⚠️ All build attempts failed'); - $errors[] = 'Assets: All 3 attempts failed'; - } - } - - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Assets: ' . $e->getMessage(); - $this->warn(' ⚠️ Assets error: ' . $e->getMessage()); - } - - step11: - // Step 11: Fix HTTP errors - try { - $this->info('[11/12] 🌐 Fixing HTTP errors...'); - - if ($interactive && ! $this->confirm(' 💡 HTTP errors controleren en fixen?', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto stepFilament; - } - - $this->fixHttpErrors($auto); - $this->info(' ✅ HTTP errors checked'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'HTTP errors: ' . $e->getMessage(); - $this->warn(' ⚠️ HTTP error check: ' . $e->getMessage()); - } - - stepFilament: - // Step 12: Fix HTTPS / Mixed Content - try { - $this->info('[12/13] 🔒 Fixing HTTPS configuration...'); - - if ($interactive && ! $this->confirm(' 💡 HTTPS en mixed content fixen?', true)) { - $this->info(' ⏭️ Overgeslagen'); - goto finalOpt; - } - - $this->fixHttpsConfiguration(); - $this->info(' ✅ HTTPS configuration fixed'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'HTTPS: ' . $e->getMessage(); - $this->warn(' ⚠️ HTTPS error: ' . $e->getMessage()); - } - - // Step 13: Fix Filament pages and widgets - try { - $this->info('[13/16] 📄 Fixing Filament pages & widgets...'); - $this->fixFilamentPages(); - $this->info(' ✅ Filament files checked'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Filament: ' . $e->getMessage(); - $this->warn(' ⚠️ Filament error: ' . $e->getMessage()); - } - - // Step 14: Fix emulator service (auto-detect + multiple fallbacks) - try { - $this->info('[14/16] 🖥️ Checking emulator service...'); - - // Auto-detect emulator service - $serviceName = $this->detectEmulatorService(); - $this->info(" 🔍 Detected service: {$serviceName}"); - - // Update setting if different - if ($serviceName !== setting('emulator_service_name')) { - WebsiteSetting::updateOrCreate( - ['key' => 'emulator_service_name'], - ['value' => $serviceName], - ); - $this->info(" ✅ Updated setting to: {$serviceName}"); - } - - $result = $this->runSystemCommand("systemctl is-active {$serviceName} 2>/dev/null || echo 'inactive'"); - if (trim((string) $result['output']) !== 'active') { - $this->warn(" ⚠️ Emulator service '{$serviceName}' is not running"); - if ($auto || $this->confirm(' 💡 Emulator service starten?', true)) { - $serviceStarted = false; - - // Attempt 1: systemctl start - try { - $this->runSystemCommand("systemctl start {$serviceName} 2>/dev/null"); - sleep(2); - $check = $this->runSystemCommand("systemctl is-active {$serviceName} 2>/dev/null"); - if (trim((string) $check['output']) === 'active') { - $serviceStarted = true; - $this->info(' ✅ Emulator service started (systemctl)'); - } - } catch (\Exception) { - // Continue to next attempt - } - - // Attempt 2: service start - if (! $serviceStarted) { - try { - $this->runSystemCommand("service {$serviceName} start 2>/dev/null"); - sleep(2); - $serviceStarted = true; - $this->info(' ✅ Emulator service started (service)'); - } catch (\Exception) { - // Continue to next attempt - } - } - - // Attempt 3: Direct java command - if (! $serviceStarted) { - try { - $jarPath = setting('emulator_jar_path', '/var/www/Emulator'); - $jarFiles = glob("{$jarPath}/*.jar"); - if ($jarFiles !== [] && $jarFiles !== false) { - $this->runSystemCommand("cd {$jarPath} && nohup java -jar " . basename($jarFiles[0]) . ' > /dev/null 2>&1 &'); - sleep(3); - $serviceStarted = true; - $this->info(' ✅ Emulator started (direct java)'); - } - } catch (\Exception) { - $this->warn(' ⚠️ All emulator start attempts failed'); - $errors[] = 'Emulator: All start attempts failed'; - } - } - } - } else { - $this->info(" ✅ Emulator service '{$serviceName}' is running"); - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Emulator: ' . $e->getMessage(); - $this->warn(' ⚠️ Emulator error: ' . $e->getMessage()); - } - - // Step 15: Fix database tables - try { - $this->info('[15/16] 🗄️ Checking database tables...'); - $requiredTables = ['users', 'permissions', 'website_settings', 'rooms', 'items', 'catalog_items']; - $missing = []; - foreach ($requiredTables as $table) { - if (! Schema::hasTable($table)) { - $missing[] = $table; - } - } - if ($missing !== []) { - $this->warn(' ⚠️ Missing tables: ' . implode(', ', $missing)); - if ($auto || $this->confirm(' 💡 Database migrations draaien?', true)) { - $this->call('migrate', ['--force' => true]); - $this->info(' ✅ Migrations executed'); - } - } else { - $this->info(' ✅ All required tables exist'); - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Database tables: ' . $e->getMessage(); - $this->warn(' ⚠️ Database error: ' . $e->getMessage()); - } - - // Step 16: Fix file permissions for critical files - try { - $this->info('[16/16] 🔐 Fixing file permissions...'); - $criticalDirs = [ - storage_path(), - base_path('bootstrap/cache'), - public_path('uploads'), - public_path('storage'), - base_path('app/Filament'), - base_path('app/Services'), - base_path('resources/views/filament'), - ]; - foreach ($criticalDirs as $dir) { - if (is_dir($dir)) { - $this->runSystemCommand("chown -R {$this->webUser}:{$this->webGroup} {$dir} 2>/dev/null"); - $this->runSystemCommand("chmod -R 775 {$dir} 2>/dev/null"); - } - } - $this->info(' ✅ File permissions fixed'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Permissions: ' . $e->getMessage(); - $this->warn(' ⚠️ Permission error: ' . $e->getMessage()); - } - - // Step 17: Ensure critical database settings exist - try { - $this->info('[17/17] ⚙️ Ensuring critical settings...'); - $requiredSettings = [ - 'emulator_jar_path' => '/var/www/Emulator', - 'emulator_service_name' => 'emulator', - 'emulator_version' => '4.0.5', - ]; - foreach ($requiredSettings as $key => $default) { - $current = setting($key); - if (empty($current)) { - WebsiteSetting::updateOrCreate( - ['key' => $key], - ['value' => $default], - ); - $this->info(" ✅ Set {$key} = {$default}"); - } - } - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Settings: ' . $e->getMessage(); - $this->warn(' ⚠️ Settings error: ' . $e->getMessage()); - } - - // Step 18: Auto-fix everything else that can go wrong - try { - $this->info('[18/18] 🔧 Ultimate auto-fix...'); - - // 1. Auto-create missing database tables - $this->autoCreateMissingTables(); - - // 2. Auto-fix missing PHP extensions - $this->autoFixPhpExtensions(); - - // 3. Auto-fix file permissions (all platforms) - $this->autoFixAllPermissions(); - - // 4. Auto-fix configuration - $this->autoFixConfiguration(); - - // 5. Auto-restart all services - $this->autoRestartServices(); - - $this->info(' ✅ Ultimate auto-fix complete'); - $fixed++; - } catch (\Exception $e) { - $errors[] = 'Ultimate fix: ' . $e->getMessage(); - $this->warn(' ⚠️ Ultimate fix error: ' . $e->getMessage()); - } - - finalOpt: - // Final optimization - try { - $this->info(''); - $this->info('🚀 Running final optimizations...'); - $this->call('config:cache'); - $this->call('route:cache'); - $this->call('view:cache'); - $this->call('event:cache'); - $this->call('filament:cache-components'); - $this->info(' ✅ Optimizations complete'); - } catch (\Exception $e) { - $errors[] = 'Optimization: ' . $e->getMessage(); - $this->warn(' ⚠️ Optimization error: ' . $e->getMessage()); - } - - // Summary - $this->info(''); - if ($errors === []) { - $this->info('════════════════════════════════════════════════════════════'); - $this->info("🎉 ATOMCMS IS 100% FIXED! ({$fixed}/17 steps completed)"); - $this->info('════════════════════════════════════════════════════════════'); - - if (DIRECTORY_SEPARATOR === '\\') { - $this->info(''); - $this->info('📝 WINDOWS INSTALLATION STEPS:'); - $this->info('-----------------------------------'); - $this->info('1. Install dependencies:'); - $this->info(' composer install'); - $this->info(' npm install'); - $this->info(''); - $this->info('2. Set up database:'); - $this->info(' php artisan migrate --seed'); - $this->info(' php artisan key:generate'); - $this->info(''); - $this->info('3. Build assets:'); - $this->info(' npm run build:atom'); - $this->info(' (For dev: npm run dev:atom)'); - $this->info(''); - $this->info('4. IIS: Point to public folder'); - $this->info(' Grant Full control to IUSR and IIS_IUSRS'); - } - } else { - $this->warn('════════════════════════════════════════════════════════════'); - $this->warn('⚠️ FIXED WITH ' . count($errors) . ' WARNINGS'); - $this->warn('════════════════════════════════════════════════════════════'); - foreach ($errors as $error) { - $this->warn(' - ' . $error); - } - $this->info(''); - $this->info('💡 Most issues are fixed. Restart your server for full effect.'); - $this->info(' - Nginx/Apache: sudo systemctl restart nginx/apache'); - $this->info(' - PHP-FPM: sudo systemctl restart php*-fpm'); - $this->info(' - Queue: php artisan queue:work --daemon'); - } - $this->info(''); - - return 0; - } - - private function fixHttpErrors(bool $auto = false): void - { - $this->info('🔧 Fixing HTTP errors...'); - - // Fix 400 - Ensure proper session config - $this->fix400Errors($auto); - - // Fix 403 - Permissions - $this->fix403Errors(); - - // Fix 404 - Routes and symlinks - $this->fix404Errors(); - - // Fix 419 - Session issues - $this->fix419Errors(); - - // Fix 500 - Common server errors - $this->fix500Errors(); - - // Fix 502 - PHP-FPM - $this->fix502Errors(); - - // Fix 503 - Maintenance and queue - $this->fix503Errors(); - - // Fix 504 - Timeouts - $this->fix504Errors(); - - $this->info('✅ HTTP errors fixed'); - } - - private function fix400Errors(bool $auto): void - { - // Fix sessions directory - $sessionPath = storage_path('framework/sessions'); - if (! is_dir($sessionPath)) { - @mkdir($sessionPath, 0755, true); - } - @chmod($sessionPath, 0755); - - // Fix POST limits - if ($auto) { - $this->repairPHPConfig(); - } - } - - private function fix403Errors(): void - { - // Fix permissions - $this->repairPermissions(); - // Fix .htaccess - if ($this->isApache()) { - $htaccess = public_path('.htaccess'); - if (! File::exists($htaccess)) { - $this->repairApache(); - } - } - // Ensure public is readable - @chmod(public_path(), 0755); - } - - private function fix404Errors(): void - { - // Fix storage symlink - if (! File::exists(public_path('storage'))) { - if (File::exists(public_path('storage'))) { - @unlink(public_path('storage')); - } - $this->call('storage:link'); - } - // Generate nginx config if needed - if ($this->isNginx()) { - $nginxConfig = base_path('atom-nginx.conf'); - if (! File::exists($nginxConfig)) { - $this->repairNginx(); - } - } - // Clear routes cache - $this->call('route:clear'); - } - - private function fix419Errors(): void - { - // Clear sessions - $sessionPath = storage_path('framework/sessions'); - if (is_dir($sessionPath)) { - $files = glob($sessionPath . '/*'); - foreach ($files as $file) { - if (is_file($file)) { - @unlink($file); - } - } - } - // Clear cache - $this->call('cache:clear'); - // Clear session files - $sessionPath = storage_path('framework/sessions'); - if (is_dir($sessionPath)) { - foreach (glob("$sessionPath/*") as $file) { - if (is_file($file)) { - @unlink($file); - } - } - } - } - - private function fix500Errors(): void - { - // Clear all caches - $this->call('cache:clear'); - $this->call('view:clear'); - $this->call('route:clear'); - $this->call('config:clear'); - $this->call('clear-compiled'); - // Fix permissions - $this->repairPermissions(); - // Ensure bootstrap/cache is writable - $bootstrapCache = base_path('bootstrap/cache'); - @chmod($bootstrapCache, 0755); - // Fix database - try { - DB::connection()->getPdo(); - } catch (\Exception) { - // Can't fix database connection automatically - } - } - - private function fix502Errors(): void - { - $phpVersion = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; - // Check if PHP-FPM is running - $isWindows = DIRECTORY_SEPARATOR === '\\'; - if ($isWindows) { - $this->info(' ⚠️ PHP-FPM on Windows - check your PHP setup'); - - return; - } - // Try to restart PHP-FPM - $this->info(' 🔄 Restarting PHP-FPM...'); - // Try different service names - $services = [ - "php{$phpVersion}-fpm", - 'php-fpm', - "php{$phpVersion}-fpm.service", - ]; - $restarted = false; - foreach ($services as $service) { - exec("sudo systemctl restart {$service} 2>/dev/null", $out, $code); - if ($code === 0) { - $this->info(" ✅ Restarted PHP-FPM ({$service})"); - $restarted = true; - break; - } - } - if (! $restarted) { - // Try to start if not running - exec('sudo systemctl start php-fpm 2>/dev/null'); - exec("sudo systemctl start php{$phpVersion}-fpm 2>/dev/null"); - $this->info(' ⚠️ Could not auto-restart PHP-FPM'); - $this->info(' 💡 Try manually: sudo systemctl restart php-fpm'); - } - // Check socket permissions - $socketPaths = [ - "/var/run/php/php{$phpVersion}-fpm.sock", - '/var/run/php-fpm.sock', - "/run/php/php{$phpVersion}-fpm.sock", - ]; - foreach ($socketPaths as $path) { - if (File::exists($path)) { - @chmod($path, 0666); - $this->info(" ✅ Fixed socket: {$path}"); - } - } - } - - private function fix503Errors(): void - { - // Disable maintenance mode - $maintenanceFile = storage_path('framework/maintenance.php'); - if (File::exists($maintenanceFile)) { - @unlink($maintenanceFile); - } - // Restart queue workers - $isWindows = DIRECTORY_SEPARATOR === '\\'; - if (! $isWindows) { - @exec("pkill -f 'queue:work' 2>/dev/null"); - $this->info('Queue workers stopped - restart with supervisor'); - } else { - $this->info('Queue workers managed manually on Windows'); - } - // Check database - try { - DB::connection()->getPdo(); - } catch (\Exception) { - $this->warn('Database unavailable'); - } - } - - private function fix504Errors(): void - { - // Increase PHP timeout - $iniPath = php_ini_loaded_file(); - if (! in_array($iniPath, ['', '0', false], true) && is_writable($iniPath)) { - $content = File::get($iniPath); - if (! str_contains($content, 'max_execution_time')) { - $content .= "\nmax_execution_time = 300"; - File::put($iniPath, $content); - } - } - // Increase PHP-FPM timeout in nginx - if ($this->isNginx()) { - $nginxConfig = base_path('atom-nginx.conf'); - if (File::exists($nginxConfig)) { - $content = File::get($nginxConfig); - if (! str_contains($content, 'fastcgi_read_timeout')) { - $content = str_replace('location ~ \.php$ {', "location ~ \.php$ {\n fastcgi_read_timeout 300;", $content); - File::put($nginxConfig, $content); - } - } - } - } - - private function backupDatabase(): void - { - $this->info('📦 Starting database backup...'); - $filename = 'backup-' . date('Y-m-d-H-i-s') . '.sql'; - $path = storage_path('app/backups'); - if (! File::exists($path)) { - File::makeDirectory($path, 0755, true); - } - - $config = config('database.connections.mysql'); - - if ($this->isWindows()) { - $this->addMysqlToPath(); - } - - if (! $this->checkCommandExists('mysqldump')) { - $this->error('❌ mysqldump is not installed or not in PATH'); - - return; - } - - $passPart = empty($config['password']) ? '' : '--password=' . escapeshellarg((string) $config['password']); - $command = sprintf('mysqldump --user=%s %s --host=%s --port=%s %s > %s/%s', - escapeshellarg((string) $config['username']), $passPart, escapeshellarg((string) $config['host']), - escapeshellarg((string) $config['port']), escapeshellarg((string) $config['database']), - escapeshellarg($path), escapeshellarg($filename), - ); - - $result = $this->runSystemCommand($command); - - if ($result['success']) { - $this->info("✅ Backup created: {$filename}"); - } else { - $this->error('❌ Backup failed: ' . ($result['output'] ?: 'Unknown error')); - } - } - - // --- UTILS --- - private function detectPlatform(): string { - if (DIRECTORY_SEPARATOR === '\\') { - return $this->detectWindowsStack(); + if (PHP_OS_FAMILY === 'Windows') { + return 'windows'; } - $soft = $_SERVER['SERVER_SOFTWARE'] ?? ''; - if (str_contains((string) $soft, 'Nginx')) { - return 'nginx'; + if (file_exists('/etc/nginx')) { + return 'linux-nginx'; } - if (str_contains((string) $soft, 'Apache')) { - return 'apache'; - } - if (str_contains((string) $soft, 'Microsoft-IIS')) { - return 'iis'; + + if (file_exists('/etc/apache2') || file_exists('/etc/httpd')) { + return 'linux-apache'; } return 'linux'; } - private function detectEmulatorService(): string + private function detectWebUserContext(): void { - // Common emulator service names - $possibleNames = [ - 'emulator', - 'arcturus', - 'arcturus-emulator', - 'arcturus-morningstar', - 'arcturus-ms', - 'nitro', - 'hotel', - 'habbo', - 'retro', - ]; + if (function_exists('posix_getpwuid')) { + $user = posix_getpwuid(posix_geteuid()); + $this->webUser = $user['name'] ?? 'unknown'; + } - // Check setting first - $settingName = setting('emulator_service_name', ''); - if (! empty($settingName)) { - $result = $this->runSystemCommand("systemctl list-unit-files {$settingName}.service 2>/dev/null | grep -q {$settingName} && echo 'exists'"); - if (trim((string) $result['output']) === 'exists') { - return $settingName; + if (file_exists('/etc/group')) { + $group = exec('id -gn 2>/dev/null'); + if ($group) { + $this->webGroup = trim($group); } } - - // Check each possible name - foreach ($possibleNames as $name) { - $result = $this->runSystemCommand("systemctl list-unit-files {$name}.service 2>/dev/null | grep -q {$name} && echo 'exists'"); - if (trim((string) $result['output']) === 'exists') { - return $name; - } - } - - // Check for Java processes running emulator JARs - $result = $this->runSystemCommand("ps aux | grep -i 'java.*jar' | grep -v grep | awk '{print $11}' | head -1"); - if (! in_array(trim((string) $result['output']), ['', '0'], true)) { - // Try to find service from running process - $result2 = $this->runSystemCommand("systemctl list-units --type=service --state=running | grep -i 'emul\\|arct\\|nitro\\|hotel' | awk '{print $1}' | sed 's/.service//' | head -1"); - if (! in_array(trim((string) $result2['output']), ['', '0'], true)) { - return trim((string) $result2['output']); - } - } - - // Check all running services for Java-based services - $result = $this->runSystemCommand('systemctl list-units --type=service --state=running 2>/dev/null'); - $lines = explode("\n", $result['output'] ?? ''); - foreach ($lines as $line) { - if (str_contains(strtolower($line), 'java') || str_contains(strtolower($line), 'jar')) { - preg_match('/(\S+)\.service/', $line, $matches); - if (isset($matches[1]) && ($matches[1] !== '' && $matches[1] !== '0')) { - return $matches[1]; - } - } - } - - // Default fallback - return 'emulator'; - } - - private function detectWindowsStack(): string - { - $soft = $_SERVER['SERVER_SOFTWARE'] ?? ''; - - if (str_contains((string) $soft, 'Microsoft-IIS')) { - return 'iis'; - } - - if (str_contains((string) $soft, 'Apache')) { - return 'wamp'; - } - - if (str_contains((string) $soft, 'nginx')) { - return 'nginx'; - } - - $processList = shell_exec('tasklist 2>nul') ?: ''; - - if (str_contains($processList, 'apache')) { - return 'wamp'; - } - - if (str_contains($processList, 'nginx')) { - return 'nginx'; - } - - if (str_contains($processList, 'httpd')) { - return 'wamp'; - } - - return 'windows'; - } - - private function isXampp(): bool - { - return $this->platform === 'xampp'; - } - - private function isWamp(): bool - { - return $this->platform === 'wamp'; - } - - private function autoCreateMissingTables(): void - { - $this->info(' 🔍 Checking for missing tables...'); - - // Run migrations to create missing tables - try { - $this->call('migrate', ['--force' => true]); - $this->info(' ✅ Missing tables created'); - } catch (\Exception) { - // Try with skip duplicates - try { - $this->runMigrationsWithSkip(); - $this->info(' ✅ Missing tables created (with skips)'); - } catch (\Exception) { - // Last resort: create essential tables manually - $this->createEssentialTables(); - } - } - } - - private function createEssentialTables(): void - { - $essentialTables = [ - 'users' => "CREATE TABLE IF NOT EXISTS `users` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `username` varchar(255) NOT NULL, - `password` varchar(255) NOT NULL, - `email` varchar(255) NOT NULL, - `rank` int(11) NOT NULL DEFAULT 1, - `credits` int(11) NOT NULL DEFAULT 0, - `pixels` int(11) NOT NULL DEFAULT 0, - `points` int(11) NOT NULL DEFAULT 0, - `online` enum('0','1','2') NOT NULL DEFAULT '0', - `last_login` int(11) NOT NULL DEFAULT 0, - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `users_username_unique` (`username`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", - - 'permissions' => "CREATE TABLE IF NOT EXISTS `permissions` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `rank_name` varchar(255) NOT NULL, - `rank_level` int(11) NOT NULL DEFAULT 1, - `acc_placefurni` enum('0','1') NOT NULL DEFAULT '0', - `acc_moverotate` enum('0','1') NOT NULL DEFAULT '0', - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", - - 'website_settings' => 'CREATE TABLE IF NOT EXISTS `website_settings` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `key` varchar(255) NOT NULL, - `value` text DEFAULT NULL, - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `website_settings_key_unique` (`key`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci', - ]; - - foreach ($essentialTables as $table => $sql) { - try { - DB::statement($sql); - $this->info(" ✅ Created table: {$table}"); - } catch (\Exception) { - // Table might already exist - } - } - } - - private function autoFixPhpExtensions(): void - { - $this->info(' 🔍 Checking PHP extensions...'); - - $required = ['curl', 'mbstring', 'pdo_mysql', 'xml', 'bcmath', 'openssl', 'gd', 'zip', 'intl', 'redis']; - - foreach ($required as $ext) { - if (! extension_loaded($ext)) { - $this->info(" ⚠️ Missing extension: {$ext}"); - - // Try to install on Linux - if ($this->isLinux()) { - $this->runSystemCommand("sudo apt-get install -y php-{$ext} 2>/dev/null"); - } - } - } - - $this->info(' ✅ PHP extensions checked'); - } - - private function autoFixAllPermissions(): void - { - $this->info(' 🔍 Fixing all permissions...'); - - $dirs = [ - storage_path(), - base_path('bootstrap/cache'), - public_path('uploads'), - public_path('storage'), - ]; - - foreach ($dirs as $dir) { - if (is_dir($dir)) { - if ($this->isWindows()) { - @chmod($dir, 0777); - } else { - $this->runSystemCommand("sudo chmod -R 775 {$dir} 2>/dev/null"); - $this->runSystemCommand("sudo chown -R {$this->webUser}:{$this->webGroup} {$dir} 2>/dev/null"); - } - } - } - - $this->info(' ✅ All permissions fixed'); - } - - private function autoFixConfiguration(): void - { - $this->info(' 🔍 Checking configuration...'); - - // Ensure .env exists - if (! File::exists(base_path('.env')) && File::exists(base_path('.env.example'))) { - File::copy(base_path('.env.example'), base_path('.env')); - $this->call('key:generate', ['--force' => true]); - $this->info(' ✅ .env created'); - } - - // Ensure APP_KEY is set - if (empty(config('app.key'))) { - $this->call('key:generate', ['--force' => true]); - $this->info(' ✅ APP_KEY generated'); - } - - $this->info(' ✅ Configuration checked'); - } - - private function autoRestartServices(): void - { - $this->info(' 🔍 Restarting services...'); - - if ($this->isLinux()) { - // Restart PHP-FPM - $phpVersion = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; - $this->runSystemCommand("sudo systemctl restart php{$phpVersion}-fpm 2>/dev/null"); - - // Restart web server - if ($this->isNginx()) { - $this->runSystemCommand('sudo systemctl restart nginx 2>/dev/null'); - } elseif ($this->isApache()) { - $this->runSystemCommand('sudo systemctl restart apache2 2>/dev/null'); - } - - // Restart emulator - $serviceName = setting('emulator_service_name', 'emulator'); - $this->runSystemCommand("sudo systemctl restart {$serviceName} 2>/dev/null"); - } - - $this->info(' ✅ Services restarted'); - } - - private function detectWebUserContext(): string - { - if ($this->isWindows()) { - if ($this->isXampp() || $this->isWamp()) { - $this->webUser = get_current_user(); - $this->webGroup = 'Administrators'; - } else { - $this->webUser = 'SYSTEM'; - $this->webGroup = 'SYSTEM'; - } - - return $this->webUser; - } - - $candidates = ['www-data', 'nginx', 'apache', 'http', 'www']; - $passwd = @file_get_contents('/etc/passwd') ?: ''; - foreach ($candidates as $user) { - if (str_contains($passwd, $user)) { - $this->webUser = $user; - $this->webGroup = $user; - - return $user; - } - } - - $whoami = $this->runSystemCommand('whoami', true); - - return $this->webUser = $whoami['success'] ? trim((string) $whoami['output']) : 'www-data'; - } - - private function isWindows(): bool - { - return $this->platform === 'windows'; - } - - private function isLinux(): bool - { - return ! $this->isWindows(); - } - - private function isApache(): bool - { - return $this->platform === 'apache' || str_contains($_SERVER['SERVER_SOFTWARE'] ?? '', 'Apache'); - } - - private function isIIS(): bool - { - return str_contains($_SERVER['SERVER_SOFTWARE'] ?? '', 'Microsoft-IIS') || $this->platform === 'iis'; - } - - private function isNginx(): bool - { - return $this->platform === 'nginx' || str_contains($_SERVER['SERVER_SOFTWARE'] ?? '', 'Nginx'); - } - - private function addCheck(string $n, string $s, string $m): void - { - $this->checks[] = [$n, $s, $m]; - if ($s === '❌') { - $this->errors++; - } - if ($s === '⚠️') { - $this->warnings++; - } - } - - private function hasError(string $p): bool - { - return array_any($this->checks, fn ($c) => str_contains((string) $c[0], $p) && $c[1] === '❌'); - } - - private function hasWarning(string $p): bool - { - return array_any($this->checks, fn ($c) => str_contains((string) $c[0], $p) && $c[1] === '⚠️'); - } - - private function displaySummary(): void - { - $this->newLine(); - - // Recount errors from checks (not from internal increments) - $realErrors = 0; - $realWarnings = 0; - foreach ($this->checks as $check) { - if ($check[1] === '❌') { - $realErrors++; - } - if ($check[1] === '⚠️') { - $realWarnings++; - } - } - - $this->table(['Feature', 'Status', 'Message'], $this->checks); - if ($realErrors > 0) { - $this->error("\nFound {$realErrors} errors! Run with --fix."); - } elseif ($realWarnings > 0) { - $this->warn("\nAll OK! ({$realWarnings} warnings)"); - } else { - $this->info("\n🎉 Everything looks perfect!"); - } - } - - private function runSystemCommand(string $command, bool $silent = false): array - { - $output = []; - $exitCode = 0; - - if ($this->isWindows()) { - exec($command . ' 2>NUL', $output, $exitCode); - } else { - exec($command . ' 2>&1', $output, $exitCode); - } - - $result = [ - 'success' => $exitCode === 0, - 'output' => implode("\n", $output), - 'exit_code' => $exitCode, - 'command' => $command, - ]; - - if (! $silent && ! $result['success'] && $output !== []) { - $this->warn("Command failed: {$command}"); - $this->warn('Output: ' . $result['output']); - } - - return $result; - } - - private function runSystemCommandWithRetry(string $command, int $retries = 3, int $delayMs = 500): array - { - for ($i = 0; $i < $retries; $i++) { - $result = $this->runSystemCommand($command, true); - - if ($result['success']) { - return $result; - } - - if ($i < $retries - 1) { - usleep($delayMs * 1000); - } - } - - return $this->runSystemCommand($command, false); - } - - private function checkCommandExists(string $command): bool - { - $which = $this->isWindows() ? 'where' : 'which'; - $result = $this->runSystemCommand("{$which} {$command}", true); - - return $result['success'] && ! in_array(trim((string) $result['output']), ['', '0'], true); - } - - private function findMysqldump(): ?string - { - if (! $this->isWindows()) { - return null; - } - - $possiblePaths = [ - 'C:\xampp\mysql\bin\mysqldump.exe', - 'C:\xampp\mysql\bin\mysqldump', - 'D:\xampp\mysql\bin\mysqldump.exe', - 'D:\xampp\mysql\bin\mysqldump', - 'C:\nginx\mysql\bin\mysqldump.exe', - 'C:\nginx\mysql\bin\mysqldump', - 'D:\nginx\mysql\bin\mysqldump.exe', - 'D:\nginx\mysql\bin\mysqldump', - 'C:\phpmysql\mysqldump.exe', - 'C:\mysql\bin\mysqldump.exe', - 'C:\Program Files\MariaDB 11.8\bin\mysqldump.exe', - 'C:\Program Files\MariaDB 11.8\bin\mariadb-dump.exe', - 'C:\Program Files (x86)\MariaDB 11.8\bin\mysqldump.exe', - getenv('USERPROFILE') . '\xampp\mysql\bin\mysqldump.exe', - getenv('USERPROFILE') . '\xampp\mysql\bin\mysqldump', - getenv('USERPROFILE') . '\nginx\mysql\bin\mysqldump.exe', - 'C:\wamp\bin\mysql\mysql5.7.31\bin\mysqldump.exe', - 'C:\wamp64\bin\mysql\mysql5.7.31\bin\mysqldump.exe', - 'C:\laragon\bin\mysql\mysql-5.7.40-winx64\bin\mysqldump.exe', - 'C:\laragon\bin\mysql\mysql-8.0.30-winx64\bin\mysqldump.exe', - ]; - - foreach ($possiblePaths as $path) { - if (file_exists($path)) { - return $path; - } - } - - return null; - } - - private function findMysql(): ?string - { - if (! $this->isWindows()) { - return null; - } - - $possiblePaths = [ - 'C:\xampp\mysql\bin\mysql.exe', - 'C:\xampp\mysql\bin\mysql', - 'D:\xampp\mysql\bin\mysql.exe', - 'D:\xampp\mysql\bin\mysql', - 'C:\nginx\mysql\bin\mysql.exe', - 'C:\nginx\mysql\bin\mysql', - 'D:\nginx\mysql\bin\mysql.exe', - 'D:\nginx\mysql\bin\mysql', - 'C:\phpmysql\mysql.exe', - 'C:\mysql\bin\mysql.exe', - 'C:\Program Files\MariaDB 11.8\bin\mysql.exe', - 'C:\Program Files\MariaDB 11.8\bin\mariadb.exe', - 'C:\Program Files (x86)\MariaDB 11.8\bin\mysql.exe', - 'C:\Program Files (x86)\MariaDB 11.8\bin\mariadb.exe', - getenv('USERPROFILE') . '\xampp\mysql\bin\mysql.exe', - getenv('USERPROFILE') . '\xampp\mysql\bin\mysql', - getenv('USERPROFILE') . '\nginx\mysql\bin\mysql.exe', - 'C:\wamp\bin\mysql\mysql5.7.31\bin\mysql.exe', - 'C:\wamp64\bin\mysql\mysql5.7.31\bin\mysql.exe', - 'C:\laragon\bin\mysql\mysql-5.7.40-winx64\bin\mysql.exe', - 'C:\laragon\bin\mysql\mysql-8.0.30-winx64\bin\mysql.exe', - ]; - - foreach ($possiblePaths as $path) { - if (file_exists($path)) { - return $path; - } - } - - return null; - } - - private function addMysqlToPath(): bool - { - if (! $this->isWindows()) { - return false; - } - - $mysqlPath = dirname($this->findMysql() ?: $this->findMysqldump() ?: ''); - if (in_array($mysqlPath, ['', '0', '.', '\\'], true)) { - return false; - } - - $currentPath = getenv('PATH') ?: ''; - if (str_contains($currentPath, $mysqlPath)) { - return true; - } - - $newPath = $currentPath . ';' . $mysqlPath; - putenv("PATH={$newPath}"); - - return true; - } - - private function fixEnvFile(): void - { - $this->info('🔧 Fixing .env file...'); - - $envPath = base_path('.env'); - - if (! File::exists($envPath)) { - $this->error('❌ .env file not found!'); - - return; - } - - $currentContent = File::get($envPath); - - // Check if .env is on single line (broken) OR if user wants to regenerate - $isBroken = ! str_contains($currentContent, "\n") && strlen($currentContent) > 500; - - $this->info('Current .env status: ' . ($isBroken ? 'BROKEN (single line)' : 'OK')); - - if ($isBroken || $this->confirm('Regenerate .env file with new settings?', true)) { - if ($isBroken) { - $this->warn('⚠️ .env file is on a single line - fixing...'); - } - - // Ask for site URL - $siteUrl = $this->ask('What is your site URL?', 'http://localhost'); - - // Ask for database credentials - $dbHost = $this->ask('Database host?', '127.0.0.1'); - $dbPort = $this->ask('Database port?', '3306'); - $dbDatabase = $this->ask('Database name?', 'habbo'); - $dbUsername = $this->ask('Database username?', 'root'); - $dbPassword = $this->secret('Database password?'); - - // Ask about Redis - $useRedis = $this->confirm('Use Redis for caching/queue?', false); - - // Ask about session driver - $sessionDriver = $this->ask('Session driver? (database/file)', 'database'); - - // Generate the fixed .env - $newContent = $this->generateFixedEnv($siteUrl, $dbHost, $dbPort, $dbDatabase, $dbUsername, $dbPassword, $sessionDriver, $useRedis); - - File::put($envPath, $newContent); - $this->info('✅ .env file fixed!'); - } else { - $this->info('✅ .env file looks good!'); - } - - // Clear config cache to reload .env - $this->call('config:clear'); - - // Generate key if missing - try { - $key = config('app.key'); - if (empty($key)) { - $this->call('key:generate', ['--force' => true]); - $this->info('✅ App key generated'); - } - } catch (\Exception) { - $this->call('key:generate', ['--force' => true]); - $this->info('✅ App key generated'); - } - } - - private function generateFixedEnv(string $siteUrl, string $dbHost, string $dbPort, string $dbDatabase, string $dbUsername, string $dbPassword, string $sessionDriver, bool $useRedis): string - { - $redisConfig = ''; - $cacheDriver = 'file'; - $broadcastDriver = 'log'; - $queueConnection = 'sync'; - - if ($useRedis) { - $redisConfig = ' -REDIS_CLIENT=phpredis -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 -'; - $cacheDriver = 'redis'; - $broadcastDriver = 'redis'; - $queueConnection = 'redis'; - } - - return "APP_NAME=\"Atom CMS\" -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL={$siteUrl} - -LOG_CHANNEL=stack -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST={$dbHost} -DB_PORT={$dbPort} -DB_DATABASE={$dbDatabase} -DB_USERNAME={$dbUsername} -DB_PASSWORD={$dbPassword} - -BROADCAST_DRIVER={$broadcastDriver} -CACHE_DRIVER={$cacheDriver} -FILESYSTEM_DISK=local -QUEUE_CONNECTION={$queueConnection} -SESSION_DRIVER={$sessionDriver} -SESSION_LIFETIME=120 - -MEMCACHED_HOST=127.0.0.1 - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=smtp -MAIL_HOST=mailpit -MAIL_PORT=1025 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS=\"hello@example.com\" -MAIL_FROM_NAME=\"\${APP_NAME}\" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -PUSHER_APP_ID= -PUSHER_APP_KEY= -PUSHER_APP_SECRET= -PUSHER_HOST= -PUSHER_PORT=443 -PUSHER_SCHEME=https -PUSHER_APP_CLUSTER=mt1 - -VITE_PUSHER_APP_KEY=\"\${PUSHER_APP_KEY}\" -VITE_PUSHER_HOST=\"\${PUSHER_HOST}\" -VITE_PUSHER_PORT=\"\${PUSHER_PORT}\" -VITE_PUSHER_SCHEME=\"\${PUSHER_SCHEME}\" -VITE_PUSHER_APP_CLUSTER=\"\${PUSHER_APP_CLUSTER}\" - -RCON_HOST=127.0.0.1 -RCON_PORT=3001 - -FINDRETROS_NAME= -FINDRETROS_ENABLED=false - -GOOGLE_RECAPTCHA_SITE_KEY= -GOOGLE_RECAPTCHA_SECRET_KEY= - -TURNSTILE_SITE_KEY= -TURNSTILE_SECRET_KEY= - -RENAME_COLLIDING_TABLES=false - -FLASH_CLIENT_ENABLED=false -EMULATOR_IP=127.0.0.1 -EMULATOR_PORT=3000 -SWF_BASE_PATH=client/flash -HABBO_SWF=Habbo.swf -PRODUCTION_FOLDER=gordon/PRODUCTION -EXTERNAL_FURNIDATA=gamedata/furnidata.xml -EXTERNAL_FIGUREMAP=gamedata/figuremap.xml -EXTERNAL_FIGUREDATA=gamedata/figuredata.xml -EXTERNAL_PRODUCTDATA=gamedata/productdata.txt -EXTERNAL_TEXTS=gamedata/external_flash_texts.txt -EXTERNAL_VARIABLES=gamedata/external_variables.txt -EXTERNAL_OVERRIDE_TEXTS=gamedata/override/external_flash_override_texts.txt -EXTERNAL_OVERRIDE_VARIABLES=gamedata/override/external_override_variables.txt - -CONVERT_PASSWORDS=false - -FORCE_HTTPS=false - -APP_LOCALE=en - -PASSWORD_RESET_TOKEN_TIME=15 - -PAYPAL_MODE='sandbox' -PAYPAL_PAYMENT_ACTION='Order' -PAYPAL_CURRENCY='USD' -PAYPAL_NOTIFY_URL= -PAYPAL_LOCALE='en_US' -PAYPAL_VALIDATE_SSL=true - -PAYPAL_SANDBOX_CLIENT_ID= -PAYPAL_SANDBOX_CLIENT_SECRET= -PAYPAL_SANDBOX_APP_ID= - -PAYPAL_LIVE_CLIENT_ID= -PAYPAL_LIVE_CLIENT_SECRET= -PAYPAL_LIVE_APP_ID= - -FORTIFY_PREFIX= -"; - } - - private function getDefaultEnvTemplate(): string - { - return 'APP_NAME="Atom CMS" -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost - -LOG_CHANNEL=stack -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=habbo -DB_USERNAME=root -DB_PASSWORD= - -BROADCAST_DRIVER=log -CACHE_DRIVER=file -FILESYSTEM_DISK=local -QUEUE_CONNECTION=sync -SESSION_DRIVER=database -SESSION_LIFETIME=120 - -MEMCACHED_HOST=127.0.0.1 - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=smtp -MAIL_HOST=mailpit -MAIL_PORT=1025 - -RCON_HOST=127.0.0.1 -RCON_PORT=3001 - -EMULATOR_IP=127.0.0.1 -EMULATOR_PORT=3000 -FLASH_CLIENT_ENABLED=false -SWF_BASE_PATH=client/flash -HABBO_SWF=Habbo.swf -PRODUCTION_FOLDER=gordon/PRODUCTION - -APP_LOCALE=en -FORCE_HTTPS=false - -TURNSTILE_SITE_KEY= -TURNSTILE_SECRET_KEY= -GOOGLE_RECAPTCHA_SITE_KEY= -GOOGLE_RECAPTCHA_SECRET_KEY= - -PAYPAL_MODE= -'; - } - - private function runMigrationsWithSkip(): void - { - $ranMigrations = DB::table('migrations')->pluck('migration')->toArray(); - $migrationFiles = File::files(base_path('database/migrations')); - $pendingMigrations = []; - - foreach ($migrationFiles as $file) { - $migrationName = str_replace('.php', '', $file->getFilename()); - if (! in_array($migrationName, $ranMigrations)) { - $pendingMigrations[] = $migrationName; - } - } - - if ($pendingMigrations === []) { - $this->info('⏭️ No pending migrations (all already ran)'); - } else { - $this->info('📦 Running ' . count($pendingMigrations) . ' new migrations...'); - $this->call('migrate', ['--force' => true]); - } - } - - private function runFullSetup(): int - { - $this->info('════════════════════════════════════════════════════════════'); - $this->info('🚀 RUNNING FULL ATOM CMS SETUP'); - $this->info('════════════════════════════════════════════════════════════'); - $this->info(''); - - $fixed = 0; - $errors = []; - - // Step 1: Fix .env if needed - $this->info('[1/6] 📝 Checking .env file...'); - try { - $envContent = file_get_contents(base_path('.env')); - if (! str_contains($envContent, "\n") && strlen($envContent) > 500) { - $this->warn(' .env is broken - use --fix-env first!'); - $errors[] = '.env file needs fixing (run: atom:check --fix-env)'; - } else { - $this->info(' ✅ .env looks good'); - $fixed++; - } - } catch (\Exception $e) { - $this->error(' ❌ .env error: ' . $e->getMessage()); - $errors[] = '.env: ' . $e->getMessage(); - } - - // Step 2: Clear caches - $this->info('[2/6] 💨 Clearing caches...'); - try { - $this->call('cache:clear'); - $this->call('config:clear'); - $this->call('route:clear'); - $this->call('view:clear'); - $this->info(' ✅ Caches cleared'); - $fixed++; - } catch (\Exception $e) { - $this->error(' ❌ Cache clear failed: ' . $e->getMessage()); - $errors[] = 'Cache: ' . $e->getMessage(); - } - - // Step 3: Run migrations - $this->info('[3/6] 🗄️ Running migrations...'); - try { - $isWindows = DIRECTORY_SEPARATOR === '\\'; - if ($isWindows) { - $this->addMysqlToPath(); - } - $this->call('migrate', ['--force' => true]); - $this->info(' ✅ Migrations complete'); - $fixed++; - } catch (\Exception $e) { - $this->error(' ❌ Migration failed: ' . $e->getMessage()); - $errors[] = 'Migrations: ' . $e->getMessage(); - } - - // Step 4: Run seeders - $this->info('[4/6] 🌱 Running seeders...'); - try { - $this->call('db:seed', ['--force' => true]); - $this->info(' ✅ Seeders complete'); - $fixed++; - } catch (\Exception $e) { - $this->warn(' ⚠️ Seeder warning: ' . $e->getMessage()); - } - - // Step 5: Storage link - $this->info('[5/6] 🔗 Setting up storage...'); - try { - if (File::exists(public_path('storage'))) { - @unlink(public_path('storage')); - } - $this->call('storage:link'); - $this->info(' ✅ Storage linked'); - $fixed++; - } catch (\Exception $e) { - $this->error(' ❌ Storage link failed: ' . $e->getMessage()); - $errors[] = 'Storage: ' . $e->getMessage(); - } - - // Step 6: Build assets - $this->info('[6/6] 🎨 Building assets...'); - $isWindows = DIRECTORY_SEPARATOR === '\\'; - try { - if ($isWindows) { - $this->info(' ⏭️ Skipped on Windows - run manually: npm install && npm run build'); - } else { - $manager = 'npm'; - $cmd = 'npm install && npm run build'; - if (File::exists(base_path('yarn.lock'))) { - $manager = 'yarn'; - $cmd = 'yarn && yarn build'; - } elseif (File::exists(base_path('pnpm-lock.yaml'))) { - $manager = 'pnpm'; - $cmd = 'pnpm install && pnpm build'; - } - $this->info(" Building with {$manager}..."); - exec("{$cmd} 2>&1", $output, $exitCode); - if ($exitCode === 0) { - $this->info(' ✅ Assets built'); - $fixed++; - } else { - $this->warn(' ⚠️ Build failed - try manually'); - } - } - } catch (\Exception $e) { - $this->warn(' ⚠️ Build warning: ' . $e->getMessage()); - } - - // Final optimizations - $this->info(''); - $this->info('🔧 Running optimizations...'); - try { - $this->call('config:cache'); - $this->call('route:cache'); - $this->call('view:cache'); - $this->info(' ✅ Optimizations complete'); - } catch (\Exception) { - // Ignore - } - - // Summary - $this->info(''); - $this->info('════════════════════════════════════════════════════════════'); - if ($errors === []) { - $this->info("🎉 SETUP COMPLETE! ({$fixed}/6 steps)"); - $this->info('════════════════════════════════════════════════════════════'); - $this->info(''); - $this->info('📝 NEXT STEPS:'); - $this->info(' 1. Start your server (nginx/apache/php artisan serve)'); - $this->info(' 2. Visit your site URL'); - $this->info(' 3. Login to Filament admin: /admin'); - } else { - $this->warn('⚠️ SETUP COMPLETED WITH ERRORS:'); - $this->warn('════════════════════════════════════════════════════════════'); - foreach ($errors as $error) { - $this->warn(' - ' . $error); - } - $this->info(''); - $this->info('💡 Try fixing errors and run again'); - } - - return $errors === [] ? 0 : 1; - } - - private function fixHttpsConfiguration(): void - { - $envPath = base_path('.env'); - $envContent = File::get($envPath); - - $appUrl = config('app.url'); - $isHttps = str_starts_with((string) $appUrl, 'https'); - - if (! $isHttps && str_starts_with((string) $appUrl, 'http://')) { - $newAppUrl = str_replace('http://', 'https://', $appUrl); - $envContent = preg_replace( - '/^APP_URL=.*$/m', - 'APP_URL=' . $newAppUrl, - $envContent, - ); - File::put($envPath, $envContent); - $this->info(' ✅ APP_URL updated to HTTPS'); - } - - $forceHttpsPattern = '/^FORCE_HTTPS=.*$/m'; - if (preg_match($forceHttpsPattern, (string) $envContent)) { - $envContent = preg_replace('/^FORCE_HTTPS=.*$/m', 'FORCE_HTTPS=true', (string) $envContent); - } else { - $envContent .= "\nFORCE_HTTPS=true"; - } - File::put($envPath, $envContent); - $this->info(' ✅ FORCE_HTTPS=true added to .env'); - - if ($this->isNginx()) { - $this->fixNginxHttpsRedirect(); - } - - $this->call('config:clear'); - $this->info(' ✅ Configuration cleared'); - } - - private function fixNginxHttpsRedirect(): void - { - $nginxConfigs = glob('/etc/nginx/sites-*/*'); - $domain = parse_url((string) config('app.url'), PHP_URL_HOST); - - foreach ($nginxConfigs as $config) { - if (is_file($config) && is_readable($config)) { - $content = File::get($config); - - if (str_contains($content, $domain)) { - $hasHttpsRedirect = str_contains($content, 'return 301 https://'); - $hasHttpBlock = str_contains($content, 'listen 80'); - - if ($hasHttpBlock && ! $hasHttpsRedirect) { - $this->info(' ⚠️ Found nginx config but no HTTP->HTTPS redirect'); - $this->info(' 💡 Add to your nginx config for port 80:'); - $this->info(' return 301 https://$server_name$request_uri;'); - } - - return; - } - } - } - - $this->info(' ⚠️ Could not find nginx config automatically'); - $this->info(' 💡 Create /etc/nginx/sites-available/your-site with HTTP->HTTPS redirect'); - } - - private function fixFilament(): void - { - $platform = $this->detectPlatform(); - - $this->info(' 🧹 Clearing Filament caches...'); - - // Clear all Laravel caches - $this->call('cache:clear'); - $this->call('config:clear'); - $this->call('route:clear'); - $this->call('view:clear'); - $this->call('event:clear'); - $this->call('optimize:clear'); - - $this->info(' 🎨 Publishing Filament assets...'); - - // Publish and install Filament - try { - $this->call('filament:install', ['--force' => true]); - } catch (\Exception $e) { - $this->warn(' ⚠️ Filament install warning: ' . $e->getMessage()); - } - - // Platform-specific fixes - if ($this->isLinux()) { - $this->info(' 🐧 Applying Linux-specific fixes...'); - - // Fix permissions for storage - $this->fixStoragePermissions(); - - // Clear OPcache if available - if (function_exists('opcache_get_status') && opcache_get_status() !== false) { - @opcache_reset(); - $this->info(' ✅ OPcache cleared'); - } - } elseif ($this->isWindows()) { - $this->info(' 🪟 Applying Windows-specific fixes...'); - - // On Windows, just ensure storage is writable - $this->fixStoragePermissions(); - } - - // Rebuild caches - $this->info(' 💾 Rebuilding caches...'); - $this->call('config:cache'); - $this->call('route:cache'); - $this->call('view:cache'); - $this->call('event:cache'); - $this->call('optimize'); - - // Web server specific fixes - if ($platform === 'nginx') { - $this->info(' 🌐 Fixing Nginx configuration...'); - $this->fixNginxConfig(); - } elseif (in_array($platform, ['apache', 'wamp', 'xampp'], true)) { - $this->info(' 🌐 Fixing Apache configuration...'); - $this->fixApacheConfig(); - } - - $this->info(' ✅ Filament repair complete for ' . strtoupper($platform)); - } - - private function fixStoragePermissions(): void - { - $directories = [ - storage_path('app'), - storage_path('app/public'), - storage_path('framework/cache'), - storage_path('framework/cache/data'), - storage_path('framework/sessions'), - storage_path('framework/views'), - storage_path('logs'), - ]; - - foreach ($directories as $dir) { - if (is_dir($dir)) { - @chmod($dir, 0755); - } - } - } - - private function fixNginxConfig(): void - { - // Check if nginx config has proper fastcgi settings - $nginxConf = '/etc/nginx/nginx.conf'; - if (file_exists($nginxConf)) { - $content = file_get_contents($nginxConf); - if (! str_contains($content, 'fastcgi_buffer_size')) { - $this->warn(' ⚠️ Consider adding fastcgi_buffer_size to nginx.conf for better performance'); - } - } - } - - private function fixApacheConfig(): void - { - // Ensure .htaccess is properly configured - $htaccess = public_path('.htaccess'); - if (file_exists($htaccess)) { - $content = file_get_contents($htaccess); - - // Check for required directives - if (! str_contains($content, 'Options -MultiViews')) { - $this->warn(' ⚠️ Consider adding "Options -MultiViews" to .htaccess'); - } - } - } - - private function fixFilamentPages(): void - { - $this->info('🔧 Checking and fixing missing Filament pages, widgets and views...'); - - // Define ALL expected files: [source path in git, target local path] - $files = [ - // AlertSettings page - [ - 'git' => 'app/Filament/Pages/Monitoring/AlertSettings.php', - 'local' => base_path('app/Filament/Pages/Monitoring/AlertSettings.php'), - ], - [ - 'git' => 'resources/views/filament/pages/monitoring/alert-settings.blade.php', - 'local' => base_path('resources/views/filament/pages/monitoring/alert-settings.blade.php'), - ], - // UpdateCheckerWidget - [ - 'git' => 'app/Filament/Widgets/UpdateCheckerWidget.php', - 'local' => base_path('app/Filament/Widgets/UpdateCheckerWidget.php'), - ], - [ - 'git' => 'resources/views/filament/widgets/update-checker.blade.php', - 'local' => base_path('resources/views/filament/widgets/update-checker.blade.php'), - ], - // Services - [ - 'git' => 'app/Services/EmulatorUpdateService.php', - 'local' => base_path('app/Services/EmulatorUpdateService.php'), - ], - [ - 'git' => 'app/Services/NitroUpdateService.php', - 'local' => base_path('app/Services/NitroUpdateService.php'), - ], - [ - 'git' => 'app/Services/RconService.php', - 'local' => base_path('app/Services/RconService.php'), - ], - [ - 'git' => 'app/Services/AlertService.php', - 'local' => base_path('app/Services/AlertService.php'), - ], - [ - 'git' => 'app/Services/AlertSettings.php', - 'local' => base_path('app/Services/AlertSettings.php'), - ], - ]; - - foreach ($files as $file) { - if (! file_exists($file['local'])) { - $this->info(" Looking for {$file['git']} in git history..."); - $content = null; - $result = $this->runSystemCommand("git show HEAD:{$file['git']}"); - if ($result['success'] && ! empty($result['output'])) { - $content = $result['output']; - } else { - $this->warn(" Could not find {$file['git']} in git HEAD. Skipping."); - - continue; - } - - if ($content !== null) { - $dir = dirname($file['local']); - if (! is_dir($dir)) { - mkdir($dir, 0755, true); - $this->info(" Created directory: {$dir}"); - } - - if (file_put_contents($file['local'], $content) !== false) { - $this->info(" ✅ Restored {$file['local']}"); - } else { - $this->error(" ❌ Failed to write {$file['local']}"); - } - } - } else { - $this->info(" ✅ {$file['git']} exists"); - } - } - - // Check PHP syntax of all important files - $this->info("\n🔍 Checking PHP syntax of critical files..."); - $phpFiles = [ - 'app/Filament/Pages/Monitoring/AlertSettings.php', - 'app/Filament/Widgets/UpdateCheckerWidget.php', - 'app/Services/EmulatorUpdateService.php', - 'app/Services/NitroUpdateService.php', - 'app/Services/RconService.php', - 'app/Services/AlertService.php', - 'app/Services/AlertSettings.php', - ]; - - foreach ($phpFiles as $phpFile) { - $fullPath = base_path($phpFile); - if (file_exists($fullPath)) { - $result = $this->runSystemCommand("php -l {$fullPath} 2>&1"); - if (str_contains((string) $result['output'], 'Errors parsing')) { - $this->error(" ❌ Syntax error in {$phpFile}: {$result['output']}"); - } else { - $this->info(" ✅ {$phpFile} syntax OK"); - } - } - } - - // Check file permissions - $this->info("\n🔍 Checking file permissions..."); - $dirs = [ - storage_path(), - base_path('bootstrap/cache'), - public_path('uploads'), - public_path('storage'), - ]; - - foreach ($dirs as $dir) { - if (is_dir($dir)) { - if (is_writable($dir)) { - $this->info(" ✅ {$dir} is writable"); - } else { - $this->error(" ❌ {$dir} is NOT writable"); - } - } else { - $this->warn(" ⚠️ {$dir} does not exist"); - } - } - - // Check database connection - $this->info("\n🔍 Checking database connection..."); - try { - DB::connection()->getPdo(); - $this->info(' ✅ Database connection OK'); - } catch (\Exception $e) { - $this->error(' ❌ Database connection failed: ' . $e->getMessage()); - } - - // Check required tables - $this->info("\n🔍 Checking required tables..."); - $requiredTables = ['users', 'permissions', 'website_settings', 'rooms', 'items']; - foreach ($requiredTables as $table) { - if (Schema::hasTable($table)) { - $count = DB::table($table)->count(); - $this->info(" ✅ {$table} exists ({$count} rows)"); - } else { - $this->error(" ❌ {$table} does NOT exist"); - } - } - - // Check RCON connection - $this->info("\n🔍 Checking RCON connection..."); - try { - $rcon = new RconService; - if ($rcon->isConnected()) { - $this->info(' ✅ RCON connection OK'); - } else { - $this->warn(' ⚠️ RCON not connected (emulator might be offline)'); - } - } catch (\Exception $e) { - $this->warn(' ⚠️ RCON check failed: ' . $e->getMessage()); - } - - // Check emulator service - $this->info("\n🔍 Checking emulator service..."); - $serviceName = setting('emulator_service_name', 'emulator'); - $result = $this->runSystemCommand("systemctl is-active {$serviceName} 2>/dev/null || echo 'inactive'"); - if (trim((string) $result['output']) === 'active') { - $this->info(" ✅ Emulator service '{$serviceName}' is running"); - } else { - $this->warn(" ⚠️ Emulator service '{$serviceName}' is not running"); - } - - // Check emulator JAR - $this->info("\n🔍 Checking emulator JAR..."); - $jarPath = setting('emulator_jar_path', '/var/www/Emulator'); - $jarFiles = glob("{$jarPath}/*.jar"); - if ($jarFiles !== [] && $jarFiles !== false) { - $this->info(' ✅ Found JAR: ' . basename($jarFiles[0])); - } else { - $this->warn(" ⚠️ No JAR found in {$jarPath}"); - } - - // Check Nitro client - $this->info("\n🔍 Checking Nitro client..."); - $nitroPath = setting('nitro_client_path', '/var/www/atomcms/nitro-client'); - if (is_dir($nitroPath)) { - $this->info(' ✅ Nitro client directory exists'); - if (file_exists("{$nitroPath}/package.json")) { - $this->info(' ✅ package.json exists'); - } else { - $this->warn(' ⚠️ package.json missing'); - } - } else { - $this->warn(' ⚠️ Nitro client directory not found'); - } - - // Check webroot - $this->info("\n🔍 Checking webroot..."); - $webroot = setting('nitro_webroot', '/var/www/Client'); - if (is_dir($webroot)) { - $this->info(" ✅ Webroot exists: {$webroot}"); - $indexFile = "{$webroot}/index.html"; - if (file_exists($indexFile)) { - $this->info(' ✅ index.html exists'); - } else { - $this->warn(' ⚠️ index.html missing in webroot'); - } - } else { - $this->warn(" ⚠️ Webroot not found: {$webroot}"); - } - - $this->info("\n✅ All checks complete!"); } } diff --git a/app/Filament/Pages/Monitoring/Commandocentrum.php b/app/Filament/Pages/Monitoring/Commandocentrum.php index 777d832..467a4e1 100755 --- a/app/Filament/Pages/Monitoring/Commandocentrum.php +++ b/app/Filament/Pages/Monitoring/Commandocentrum.php @@ -9,6 +9,7 @@ use App\Models\Miscellaneous\WebsitePermission; use App\Models\StaffActivity; use App\Services\AutoDetectService; use App\Services\CatalogService; +use App\Services\Diagnostics\DiagnosticRunner; use App\Services\EmulatorUpdateService; use App\Services\RconService; use App\Services\SettingsService; @@ -61,9 +62,13 @@ final class Commandocentrum extends Page implements HasForms public string $catalogSyncUrl = ''; + /** @var array<\App\Services\Diagnostics\DiagnosticResult> */ + public array $diagnostics = []; + public function mount(): void { $this->fillForm(); + $this->runDiagnostics(); } protected function fillForm(): void @@ -147,6 +152,22 @@ final class Commandocentrum extends Page implements HasForms ->content(fn (): HtmlString => $this->renderServerInfo()), ]), + Section::make('🩺 Systeem Gezondheid') + ->description('Automatische systeem diagnostiek') + ->icon('heroicon-o-heart') + ->afterHeader([ + Action::make('refresh_diagnostics') + ->label('Vernieuwen') + ->icon('heroicon-o-arrow-path') + ->color('info') + ->action('refreshDiagnostics'), + ]) + ->schema([ + Placeholder::make('diagnostics') + ->label('') + ->content(fn (): HtmlString => $this->renderDiagnostics()), + ]), + Section::make('🏨 Hotel Status') ->description('Emulator en Nitro status') ->icon('heroicon-o-building-office') @@ -1941,4 +1962,111 @@ final class Commandocentrum extends Page implements HasForms ->color($color) ->send(); } + + public function refreshDiagnostics(): void + { + $this->runDiagnostics(); + $this->notify('Success', 'Diagnostiek vernieuwd', 'success'); + } + + private function runDiagnostics(): void + { + $runner = app(DiagnosticRunner::class); + $this->diagnostics = $runner->runAll(); + } + + private function renderDiagnostics(): HtmlString + { + if ($this->diagnostics === []) { + $this->runDiagnostics(); + } + + $errors = array_filter($this->diagnostics, fn ($r) => $r->status === 'error'); + $warnings = array_filter($this->diagnostics, fn ($r) => $r->status === 'warning'); + $ok = array_filter($this->diagnostics, fn ($r) => $r->status === 'ok'); + + $errorCount = count($errors); + $warningCount = count($warnings); + $okCount = count($ok); + + $overallStatus = $errorCount > 0 ? 'error' : ($warningCount > 0 ? 'warning' : 'ok'); + $overallColor = match ($overallStatus) { + 'error' => '#ef4444', + 'warning' => '#f59e0b', + default => '#22c55e', + }; + $overallLabel = match ($overallStatus) { + 'error' => 'Kritieke Problemen', + 'warning' => 'Waarschuwingen', + default => 'Gezond', + }; + + $html = '
'; + + // Summary cards + $html .= '
'; + $html .= $this->getSummaryCardHtml('Gezond', $okCount, '#22c55e', 'heroicon-o-check-circle'); + $html .= $this->getSummaryCardHtml('Waarschuwingen', $warningCount, '#f59e0b', 'heroicon-o-exclamation-triangle'); + $html .= $this->getSummaryCardHtml('Fouten', $errorCount, '#ef4444', 'heroicon-o-x-circle'); + $html .= '
'; + + // Overall status banner + $html .= '
'; + $html .= '
'; + $html .= 'Systeem Status: {$overallLabel}'; + $html .= '
'; + + // Detailed results + if ($errorCount > 0 || $warningCount > 0) { + $html .= '
'; + foreach ($this->diagnostics as $result) { + if ($result->status === 'ok') { + continue; + } + $color = $result->status === 'error' ? '#ef4444' : '#f59e0b'; + $icon = $result->status === 'error' + ? '' + : ''; + + $html .= '
'; + $html .= '
' . $icon . '
'; + $html .= '
'; + $html .= '
' . e($result->name) . '
'; + $html .= '
' . e($result->message) . '
'; + if ($result->fix) { + $html .= '
💡 ' . e($result->fix) . '
'; + } + $html .= '
'; + } + $html .= '
'; + } + + $html .= '
'; + + return new HtmlString($html); + } + + private function getSummaryCardHtml(string $label, int $count, string $color, string $icon): string + { + $iconSvg = match ($icon) { + 'heroicon-o-check-circle' => '', + 'heroicon-o-exclamation-triangle' => '', + 'heroicon-o-x-circle' => '', + default => '', + }; + + return << +
+
+ + {$iconSvg} + +
+ {$label} +
+
{$count}
+ + HTML; + } } diff --git a/app/Services/Diagnostics/DatabaseDiagnostic.php b/app/Services/Diagnostics/DatabaseDiagnostic.php new file mode 100755 index 0000000..96ef753 --- /dev/null +++ b/app/Services/Diagnostics/DatabaseDiagnostic.php @@ -0,0 +1,124 @@ + + */ + public function runAll(): array + { + return [ + $this->checkConnection(), + $this->checkMigrations(), + $this->checkRequiredTables(), + $this->checkRadioTables(), + ]; + } + + public function checkConnection(): DiagnosticResult + { + try { + DB::connection()->getPdo(); + + return DiagnosticResult::ok('Database Connection', 'Connected to ' . DB::connection()->getDatabaseName()); + } catch (\Exception $e) { + return DiagnosticResult::error('Database Connection', $e->getMessage(), 'Check DB credentials in .env'); + } + } + + public function checkMigrations(): DiagnosticResult + { + try { + $pending = DB::table('migrations')->exists() + ? count($this->getPendingMigrations()) + : 'unknown'; + + if ($pending === 'unknown') { + return DiagnosticResult::warning('Migrations', 'Migrations table not found'); + } + + if ($pending > 0) { + return DiagnosticResult::warning('Migrations', "{$pending} pending migrations", 'Run: php artisan migrate'); + } + + return DiagnosticResult::ok('Migrations', 'All migrations up to date'); + } catch (\Exception $e) { + return DiagnosticResult::error('Migrations', $e->getMessage()); + } + } + + public function checkRequiredTables(): DiagnosticResult + { + $requiredTables = [ + 'users', 'permissions', 'website_settings', 'website_articles', + 'website_shop_articles', 'website_shop_categories', + ]; + + $missing = []; + foreach ($requiredTables as $table) { + if (! Schema::hasTable($table)) { + $missing[] = $table; + } + } + + if ($missing !== []) { + return DiagnosticResult::error( + 'Required Tables', + 'Missing: ' . implode(', ', $missing), + 'Run: php artisan migrate' + ); + } + + return DiagnosticResult::ok('Required Tables', 'All required tables exist'); + } + + public function checkRadioTables(): DiagnosticResult + { + $radioTables = [ + 'radio_ranks', 'radio_banners', 'radio_schedules', + 'radio_shouts', 'radio_history', + ]; + + $missing = []; + foreach ($radioTables as $table) { + if (! Schema::hasTable($table)) { + $missing[] = $table; + } + } + + if ($missing !== []) { + return DiagnosticResult::warning( + 'Radio Tables', + 'Missing: ' . implode(', ', $missing), + 'Run radio migration seeder' + ); + } + + return DiagnosticResult::ok('Radio Tables', 'All radio tables exist'); + } + + /** + * @return array + */ + private function getPendingMigrations(): array + { + $migrated = DB::table('migrations')->pluck('migration')->toArray(); + $allMigrations = []; + + $migrationPath = database_path('migrations'); + if (is_dir($migrationPath)) { + foreach (scandir($migrationPath) as $file) { + if (str_ends_with($file, '.php')) { + $allMigrations[] = pathinfo($file, PATHINFO_FILENAME); + } + } + } + + return array_diff($allMigrations, $migrated); + } +} diff --git a/app/Services/Diagnostics/DiagnosticCheck.php b/app/Services/Diagnostics/DiagnosticCheck.php new file mode 100755 index 0000000..8890634 --- /dev/null +++ b/app/Services/Diagnostics/DiagnosticCheck.php @@ -0,0 +1,10 @@ + + */ + private array $results = []; + + /** + * @return array + */ + public function runAll(): array + { + $this->results = []; + + $diagnostics = [ + new DatabaseDiagnostic(), + new SecurityDiagnostic(), + new SystemDiagnostic(), + new HttpDiagnostic(), + ]; + + foreach ($diagnostics as $diagnostic) { + $this->results = array_merge($this->results, $diagnostic->runAll()); + } + + return $this->results; + } + + public function hasErrors(): bool + { + return array_any($this->results, fn ($r) => $r->status === 'error'); + } + + public function hasWarnings(): bool + { + return array_any($this->results, fn ($r) => $r->status === 'warning'); + } + + public function getErrors(): array + { + return array_filter($this->results, fn ($r) => $r->status === 'error'); + } + + public function getWarnings(): array + { + return array_filter($this->results, fn ($r) => $r->status === 'warning'); + } + + public function getOk(): array + { + return array_filter($this->results, fn ($r) => $r->status === 'ok'); + } +} diff --git a/app/Services/Diagnostics/HttpDiagnostic.php b/app/Services/Diagnostics/HttpDiagnostic.php new file mode 100755 index 0000000..33a0dd7 --- /dev/null +++ b/app/Services/Diagnostics/HttpDiagnostic.php @@ -0,0 +1,96 @@ + + */ + public function runAll(): array + { + return [ + $this->checkAppUrl(), + $this->checkSslCertificate(), + $this->checkHttpRedirect(), + ]; + } + + public function checkAppUrl(): DiagnosticResult + { + $appUrl = config('app.url'); + + if (empty($appUrl) || $appUrl === 'http://localhost') { + return DiagnosticResult::warning( + 'App URL', + 'APP_URL not configured properly', + 'Set APP_URL in .env to your domain' + ); + } + + if (! str_starts_with($appUrl, 'https://') && app()->environment('production')) { + return DiagnosticResult::warning( + 'App URL', + 'Not using HTTPS in production', + 'Configure SSL and update APP_URL' + ); + } + + return DiagnosticResult::ok('App URL', $appUrl); + } + + public function checkSslCertificate(): DiagnosticResult + { + $appUrl = config('app.url'); + + if (! str_starts_with($appUrl, 'https://')) { + return DiagnosticResult::warning('SSL', 'Not using HTTPS'); + } + + $host = parse_url($appUrl, PHP_URL_HOST); + if (! $host) { + return DiagnosticResult::warning('SSL', 'Could not parse host from APP_URL'); + } + + try { + $response = Http::timeout(5)->get($appUrl); + + if ($response->successful()) { + return DiagnosticResult::ok('SSL', 'Certificate valid'); + } + + return DiagnosticResult::warning('SSL', 'HTTPS endpoint returned ' . $response->status()); + } catch (\Exception $e) { + return DiagnosticResult::error('SSL', $e->getMessage(), 'Check SSL certificate configuration'); + } + } + + public function checkHttpRedirect(): DiagnosticResult + { + $appUrl = config('app.url'); + + if (! str_starts_with($appUrl, 'https://')) { + return DiagnosticResult::ok('HTTP Redirect', 'Not applicable (no HTTPS)'); + } + + $httpUrl = str_replace('https://', 'http://', $appUrl); + + try { + $response = Http::timeout(5)->withoutRedirecting()->get($httpUrl); + + if (in_array($response->status(), [301, 302])) { + return DiagnosticResult::ok('HTTP Redirect', 'HTTP redirects to HTTPS'); + } + + return DiagnosticResult::warning( + 'HTTP Redirect', + "HTTP returns {$response->status()} instead of redirect", + 'Configure web server to redirect HTTP to HTTPS' + ); + } catch (\Exception $e) { + return DiagnosticResult::warning('HTTP Redirect', 'Could not test: ' . $e->getMessage()); + } + } +} diff --git a/app/Services/Diagnostics/SecurityDiagnostic.php b/app/Services/Diagnostics/SecurityDiagnostic.php new file mode 100755 index 0000000..62d17dc --- /dev/null +++ b/app/Services/Diagnostics/SecurityDiagnostic.php @@ -0,0 +1,90 @@ + + */ + public function runAll(): array + { + return [ + $this->checkAppKey(), + $this->checkDebugMode(), + $this->checkEnvFile(), + $this->checkFilePermissions(), + ]; + } + + public function checkAppKey(): DiagnosticResult + { + $key = config('app.key'); + + if (empty($key)) { + return DiagnosticResult::error('App Key', 'No application key set', 'Run: php artisan key:generate'); + } + + return DiagnosticResult::ok('App Key', 'Application key is set'); + } + + public function checkDebugMode(): DiagnosticResult + { + if (config('app.debug') && app()->environment('production')) { + return DiagnosticResult::error( + 'Debug Mode', + 'Debug mode is enabled in production', + 'Set APP_DEBUG=false in .env' + ); + } + + return DiagnosticResult::ok('Debug Mode', 'Debug mode is ' . (config('app.debug') ? 'enabled (dev)' : 'disabled')); + } + + public function checkEnvFile(): DiagnosticResult + { + $envPath = base_path('.env'); + + if (! File::exists($envPath)) { + return DiagnosticResult::error('.env File', '.env file not found', 'Copy .env.example to .env'); + } + + $content = File::get($envPath); + if (! str_contains($content, "\n") && strlen($content) > 500) { + return DiagnosticResult::warning( + '.env File', + 'File appears to be on a single line', + 'Ensure .env has proper line breaks' + ); + } + + return DiagnosticResult::ok('.env File', 'File exists and is properly formatted'); + } + + public function checkFilePermissions(): DiagnosticResult + { + $directories = [ + storage_path(), + bootstrap_path('cache'), + ]; + + $issues = []; + foreach ($directories as $dir) { + if (! is_writable($dir)) { + $issues[] = $dir; + } + } + + if ($issues !== []) { + return DiagnosticResult::error( + 'File Permissions', + 'Not writable: ' . implode(', ', $issues), + 'Run: chmod -R 775 storage bootstrap/cache' + ); + } + + return DiagnosticResult::ok('File Permissions', 'All directories are writable'); + } +} diff --git a/app/Services/Diagnostics/SystemDiagnostic.php b/app/Services/Diagnostics/SystemDiagnostic.php new file mode 100755 index 0000000..414c504 --- /dev/null +++ b/app/Services/Diagnostics/SystemDiagnostic.php @@ -0,0 +1,100 @@ + + */ + public function runAll(): array + { + return [ + $this->checkPhpExtensions(), + $this->checkPhpVersion(), + $this->checkCache(), + $this->checkSession(), + $this->checkRedis(), + ]; + } + + public function checkPhpExtensions(): DiagnosticResult + { + $required = ['pdo_mysql', 'curl', 'json', 'mbstring', 'xml', 'zip', 'bcmath']; + $missing = array_filter($required, fn ($ext) => ! extension_loaded($ext)); + + if ($missing !== []) { + return DiagnosticResult::error( + 'PHP Extensions', + 'Missing: ' . implode(', ', $missing), + 'Install: sudo apt install php-' . implode(' php-', $missing) + ); + } + + return DiagnosticResult::ok('PHP Extensions', 'All required extensions loaded'); + } + + public function checkPhpVersion(): DiagnosticResult + { + $current = PHP_VERSION_ID; + $min = 80100; // PHP 8.1 + + if ($current < $min) { + return DiagnosticResult::error( + 'PHP Version', + 'Current: ' . PHP_VERSION . ' (minimum: 8.1)', + 'Upgrade PHP to 8.1 or higher' + ); + } + + return DiagnosticResult::ok('PHP Version', PHP_VERSION); + } + + public function checkCache(): DiagnosticResult + { + try { + Cache::put('diagnostic_test', 'ok', 10); + $value = Cache::get('diagnostic_test'); + + if ($value === 'ok') { + return DiagnosticResult::ok('Cache', 'Working (' . config('cache.default') . ')'); + } + + return DiagnosticResult::warning('Cache', 'Cache returned unexpected value'); + } catch (\Exception $e) { + return DiagnosticResult::error('Cache', $e->getMessage()); + } + } + + public function checkSession(): DiagnosticResult + { + $driver = config('session.driver'); + + if ($driver === 'file' && app()->environment('production')) { + return DiagnosticResult::warning( + 'Session', + 'Using file sessions in production', + 'Consider using redis or database sessions' + ); + } + + return DiagnosticResult::ok('Session', "Driver: {$driver}"); + } + + public function checkRedis(): DiagnosticResult + { + if (config('redis.default.host') === '127.0.0.1') { + try { + Redis::ping(); + + return DiagnosticResult::ok('Redis', 'Connected'); + } catch (\Exception $e) { + return DiagnosticResult::warning('Redis', 'Not reachable: ' . $e->getMessage()); + } + } + + return DiagnosticResult::ok('Redis', 'Not configured (optional)'); + } +} diff --git a/app/Services/Emulator/EmulatorBackupService.php b/app/Services/Emulator/EmulatorBackupService.php new file mode 100755 index 0000000..33fc72c --- /dev/null +++ b/app/Services/Emulator/EmulatorBackupService.php @@ -0,0 +1,80 @@ +loadConfiguration(); + } + + public function getList(): array + { + $backups = []; + + try { + $backupDir = $this->jarPath . '/backup'; + if (! is_dir($backupDir)) { + return $backups; + } + + $result = Process::timeout(5)->run("ls -1t {$backupDir}/*.jar 2>/dev/null"); + if ($result->successful()) { + $lines = array_filter(explode("\n", trim($result->output()))); + foreach ($lines as $line) { + $line = trim($line); + if ($line === '' || $line === '0' || ! str_contains($line, '.jar')) { + continue; + } + + $filename = basename($line); + $version = $this->extractVersionFromFilename($filename); + + preg_match('/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})/', $line, $dateMatches); + $date = $dateMatches[1] ?? date('Y-m-d_H-i'); + + $backups[] = [ + 'name' => $filename, + 'jar' => $filename, + 'date' => $date, + 'version' => $version, + ]; + } + } + } catch (\Exception) { + } + + return $backups; + } + + public function restore(string $backupName): array + { + try { + $backupDir = $this->jarPath . '/backup'; + $backupPath = $backupDir . '/' . $backupName; + $targetPath = $this->jarPath . '/' . $backupName; + + if (! file_exists($backupPath)) { + return ['success' => false, 'error' => 'Backup niet gevonden']; + } + + if (file_exists($targetPath)) { + unlink($targetPath); + } + + copy($backupPath, $targetPath); + chmod($targetPath, 0755); + + return ['success' => true, 'message' => 'Backup hersteld: ' . $backupName]; + } catch (\Exception $e) { + return ['success' => false, 'error' => $e->getMessage()]; + } + } +} diff --git a/app/Services/Emulator/EmulatorBuildService.php b/app/Services/Emulator/EmulatorBuildService.php new file mode 100644 index 0000000..f518061 --- /dev/null +++ b/app/Services/Emulator/EmulatorBuildService.php @@ -0,0 +1,273 @@ +loadConfiguration(); + } + + public function buildFromSource(bool $force = false): array + { + $repo = $this->sourceRepo ?: $this->githubRepo; + $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; + + if (! $repo) { + return ['success' => false, 'error' => 'Geen source repo geconfigureerd']; + } + + $sourcePath = $this->emulatorSourcePath; + $serviceName = $this->emulatorService; + + Log::info('[EmulatorBuild] Starting source build', [ + 'repo' => $repo, + 'branch' => $branch, + 'path' => $sourcePath, + ]); + + $this->ensureJavaInstalled(); + + Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $sourcePath))); + Process::timeout(10)->run('mkdir -p ' . escapeshellarg((string) $this->jarPath)); + Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath))); + + $maxRetries = 2; + $lastError = ''; + + for ($retry = 0; $retry <= $maxRetries; $retry++) { + Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)) . ' 2>/dev/null || true'); + Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || true'); + + if ($retry > 0) { + Log::info('[EmulatorBuild] Retry attempt', ['attempt' => $retry]); + Process::timeout(30)->run('rm -rf ' . escapeshellarg((string) $sourcePath)); + } + + try { + $existsCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . " ] && echo 'exists'"); + $sourceExists = $existsCheck->successful() && trim($existsCheck->output()) === 'exists'; + + $gitCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . "/.git ] && echo 'git'"); + $isGitRepo = $gitCheck->successful() && trim($gitCheck->output()) === 'git'; + + if ($sourceExists && $isGitRepo) { + Log::info('[EmulatorBuild] Pulling latest changes'); + $commands = [ + 'cd ' . escapeshellarg((string) $sourcePath) . ' && git fetch origin', + 'cd ' . escapeshellarg((string) $sourcePath) . ' && git checkout ' . escapeshellarg($branch), + 'cd ' . escapeshellarg((string) $sourcePath) . ' && git pull origin ' . escapeshellarg($branch), + ]; + } else { + Log::info('[EmulatorBuild] Cloning repository'); + $commands = [ + 'sudo rm -rf ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || rm -rf ' . escapeshellarg((string) $sourcePath), + 'mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)), + 'git clone --branch ' . escapeshellarg($branch) . ' --depth 1 https://github.com/' . escapeshellarg($repo) . '.git ' . escapeshellarg((string) $sourcePath), + 'chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath), + ]; + } + + $command = implode(' && ', $commands); + $result = Process::timeout(300)->run($command); + + if ($result->failed()) { + $lastError = 'Git clone/pull failed: ' . substr($result->errorOutput(), 0, 300); + Log::warning('[EmulatorBuild] Git operation failed', ['error' => $lastError, 'attempt' => $retry]); + + continue; + } + + $buildCommands = $this->getBuildCommands($sourcePath); + + Log::info('[EmulatorBuild] Running build', ['command' => $buildCommands]); + $buildResult = Process::timeout(600)->run('cd ' . escapeshellarg((string) $sourcePath) . ' && ' . $buildCommands); + + $hasSignalError = str_contains($buildResult->errorOutput(), 'signal') || str_contains($buildResult->output(), 'signal'); + + if ($hasSignalError) { + Log::warning('[EmulatorBuild] Build process received signal, checking if JAR was built anyway'); + } + + $jarPath = $this->findBuiltJar($sourcePath); + + if ($jarPath) { + Log::info('[EmulatorBuild] JAR found despite build status', ['jar' => $jarPath]); + } elseif ($buildResult->failed() && ! $hasSignalError) { + $lastError = 'Build failed: ' . substr($buildResult->errorOutput(), 0, 500); + Log::warning('[EmulatorBuild] Build failed', ['error' => $lastError, 'attempt' => $retry]); + + if ($retry < $maxRetries) { + $cleanCommands = [ + 'cd ' . escapeshellarg((string) $sourcePath) . ' && mvn clean 2>/dev/null || ./gradlew clean 2>/dev/null || true', + ]; + Process::timeout(60)->run(implode(' && ', $cleanCommands)); + + continue; + } + + continue; + } + + if (! $jarPath) { + $lastError = 'Build succeeded but JAR not found. Check build output.'; + Log::warning('[EmulatorBuild] JAR not found', ['attempt' => $retry]); + + continue; + } + + return $this->deployJar($jarPath, $serviceName); + + } catch (\Exception $e) { + $lastError = $e->getMessage(); + Log::error('[EmulatorBuild] Source build exception', ['error' => $lastError, 'attempt' => $retry]); + } + } + + return [ + 'success' => false, + 'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError, + ]; + } + + public function getBuildCommands(string $sourcePath): string + { + $pomPath = $sourcePath; + $pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'"); + + if (! $pomCheck->successful() || trim($pomCheck->output()) !== 'pom') { + $pomPath = $sourcePath . '/Emulator'; + $pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'"); + } + + $gradlewPath = $sourcePath . '/gradlew'; + $gradlewCheck = Process::timeout(5)->run("[ -f {$gradlewPath} ] && echo 'gradlew'"); + + $gradlePath = $sourcePath . '/build.gradle'; + $gradleCheck = Process::timeout(5)->run("[ -f {$gradlePath} ] && echo 'gradle'"); + + if ($pomCheck->successful() && trim($pomCheck->output()) === 'pom') { + return "cd {$pomPath} && mvn clean package -DskipTests 2>&1"; + } + + if ($gradlewCheck->successful() && trim($gradlewCheck->output()) === 'gradlew') { + return "cd {$sourcePath} && chmod +x gradlew && ./gradlew clean build -x test 2>&1"; + } + + if ($gradleCheck->successful() && trim($gradleCheck->output()) === 'gradle') { + return "cd {$sourcePath} && gradle clean build -x test 2>&1"; + } + + return "cd {$sourcePath} && ls -la"; + } + + public function findBuiltJar(string $sourcePath): ?string + { + $patterns = [ + $sourcePath . '/target/*.jar', + $sourcePath . '/build/libs/*.jar', + $sourcePath . '/*/*.jar', + $sourcePath . '/*.jar', + $sourcePath . '/Emulator/target/*.jar', + $sourcePath . '/Emulator/build/libs/*.jar', + $sourcePath . '/Emulator/*/*.jar', + ]; + + foreach ($patterns as $pattern) { + $result = Process::timeout(10)->run('ls -t ' . $pattern . ' 2>/dev/null | head -1'); + if ($result->successful()) { + $jarPath = trim($result->output()); + if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) { + return $jarPath; + } + } + } + + return null; + } + + private function deployJar(string $jarPath, string $serviceName): array + { + $jarName = basename($jarPath); + $version = $this->extractVersionFromFilename($jarName); + if ($version === '' || $version === '0') { + $version = date('Y.m.d'); + } + + $deployCommands = [ + 'mkdir -p ' . escapeshellarg((string) $this->jarPath), + 'chown -R www-data:www-data ' . escapeshellarg((string) $this->jarPath), + 'if ls ' . escapeshellarg((string) $this->jarPath) . '/*.jar 1>/dev/null 2>&1; then mkdir -p ' . escapeshellarg((string) $this->jarPath) . '/backup && mv ' . escapeshellarg((string) $this->jarPath) . '/*.jar ' . escapeshellarg((string) $this->jarPath) . '/backup/; fi', + 'cp -f ' . escapeshellarg($jarPath) . ' ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName), + 'chown www-data:www-data ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName), + 'chmod 755 ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName), + 'ls -la ' . escapeshellarg((string) $this->jarPath) . '/', + 'systemctl restart ' . escapeshellarg((string) $serviceName) . ' 2>&1 || service ' . escapeshellarg((string) $serviceName) . ' restart 2>&1 || true', + ]; + + $deployCommand = implode(' && ', $deployCommands); + + Log::info('[EmulatorBuild] Deploying jar', [ + 'source' => $jarPath, + 'destination' => "{$this->jarPath}/{$jarName}", + ]); + + $deployResult = Process::timeout(60)->run($deployCommand); + + if (! $deployResult->successful()) { + return [ + 'success' => false, + 'error' => 'Deploy failed: ' . substr($deployResult->errorOutput(), 0, 300), + ]; + } + + $sourceService = new EmulatorSourceService; + $sourceInfo = $sourceService->checkForUpdates(); + $installedDate = $sourceInfo['latest_timestamp'] ?? time(); + + $this->settings->set('emulator_version', $version); + $this->settings->set('emulator_jar_installed_date', (string) $installedDate); + $this->settings->set('emulator_source_commit', $sourceInfo['latest_sha'] ?? 'unknown'); + $this->settings->set('emulator_source_date', (string) $installedDate); + + $currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; + $this->settings->set('emulator_installed_branch', $currentBranch); + + $sqlService = new EmulatorSqlService; + $sqlService->runUpdates(); + + Log::info('[EmulatorBuild] Source build successful'); + + return [ + 'success' => true, + 'version' => $version, + 'jar' => $jarName, + 'built' => true, + 'message' => "✅ Emulator vanaf source gebouwd!\n📦 {$jarName}\n🔄 Service herstart", + ]; + } + + private function ensureJavaInstalled(): void + { + $javaCheck = Process::timeout(5)->run('java -version 2>&1'); + if (! $javaCheck->successful()) { + Log::warning('[EmulatorBuild] Java not found, attempting to install'); + Process::timeout(180)->run('apt-get update && apt-get install -y default-jdk 2>&1'); + } + + $mavenCheck = Process::timeout(5)->run('which mvn'); + if (! $mavenCheck->successful()) { + Log::warning('[EmulatorBuild] Maven not found, attempting to install'); + Process::timeout(180)->run('apt-get install -y maven 2>&1'); + } + } +} diff --git a/app/Services/Emulator/EmulatorConfiguration.php b/app/Services/Emulator/EmulatorConfiguration.php new file mode 100755 index 0000000..6e6ad83 --- /dev/null +++ b/app/Services/Emulator/EmulatorConfiguration.php @@ -0,0 +1,197 @@ +settings = app(SettingsService::class); + $this->settings->clearInstanceCache(); + $this->githubUrl = $this->settings->getOrDefault('emulator_github_url', ''); + $this->jarDirectUrl = $this->settings->getOrDefault('emulator_jar_direct_url', ''); + + $basePath = $this->detectBasePath(); + $this->jarPath = $this->resolveEmulatorPath($this->settings->getOrDefault('emulator_jar_path', $basePath . '/Emulator')); + $this->emulatorService = $this->settings->getOrDefault('emulator_service_name', $this->detectEmulatorService()); + $this->emulatorSourcePath = $this->resolveEmulatorPath($this->settings->getOrDefault('emulator_source_path', $basePath . '/emulator-source')); + $this->sourceRepo = $this->settings->getOrDefault('emulator_source_repo', ''); + + $this->parseGitHubUrl($this->githubUrl); + $this->parseSourceRepo($this->sourceRepo); + $this->ensureGitSafeDirectories(); + + $settingBranch = $this->settings->getOrDefault('emulator_github_branch', null); + if ($settingBranch) { + $this->githubBranch = strtolower((string) $settingBranch); + $this->sourceBranch = strtolower((string) $settingBranch); + } + } + + private function detectBasePath(): string + { + $possiblePaths = [ + base_path(), + '/var/www/atomcms', + '/var/www/html', + '/var/www', + dirname(base_path()), + ]; + + foreach ($possiblePaths as $path) { + if (is_dir($path)) { + return $path; + } + } + + return '/var/www'; + } + + private function resolveEmulatorPath(string $path): string + { + if (str_starts_with($path, '/')) { + return $path; + } + + return $this->detectBasePath() . '/' . ltrim($path, '/'); + } + + private function detectEmulatorService(): string + { + $possibleServices = ['emulator', 'arcturus', 'morningstar', 'habbo', 'hotel', 'game']; + + foreach ($possibleServices as $service) { + $result = Process::timeout(5)->run("systemctl list-unit-files {$service}.service 2>/dev/null | grep -q '{$service}.service' && echo 'found'"); + if ($result->successful() && trim($result->output()) === 'found') { + return $service; + } + + $result = Process::timeout(5)->run("systemctl list-units --type=service --all 2>/dev/null | grep -q '{$service}.service' && echo 'found'"); + if ($result->successful() && trim($result->output()) === 'found') { + return $service; + } + } + + return 'emulator'; + } + + private function ensureGitSafeDirectories(): void + { + $directories = [ + base_path(), + '/var/www/atomcms', + '/var/www', + $this->emulatorSourcePath, + $this->jarPath, + dirname((string) $this->emulatorSourcePath), + dirname((string) $this->jarPath), + ]; + + foreach (array_unique($directories) as $dir) { + if (! empty($dir) && $this->isDirAccessible($dir)) { + Process::timeout(5)->run('git config --global --add safe.directory ' . escapeshellarg($dir) . ' 2>/dev/null || true'); + } + } + + Process::timeout(5)->run("git config --global --add safe.directory '*' 2>/dev/null || true"); + } + + private function isDirAccessible(string $path): bool + { + if ($path === '' || $path === '0') { + return false; + } + + $result = Process::timeout(5)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"'); + + return trim($result->output()) === 'yes'; + } + + private function parseGitHubUrl(string $url): void + { + $parsed = $this->parseGithubRepoUrl($url); + $this->githubRepo = $parsed['repo']; + $this->githubBranch = $parsed['branch']; + } + + private function parseSourceRepo(string $url): void + { + $parsed = $this->parseGithubRepoUrl($url); + $this->sourceRepo = $parsed['repo']; + $this->sourceBranch = $parsed['branch']; + } + + private function parseGithubRepoUrl(string $url): array + { + if ($url === '' || $url === '0') { + return ['repo' => null, 'branch' => 'main']; + } + + if (preg_match('/github\.com\/([^\/]+)\/([^\/\?#]+)/', $url, $matches)) { + $repo = $matches[1] . '/' . $matches[2]; + $branch = 'main'; + + if (preg_match('/\/tree\/([^\/]+)/', $url, $branchMatch)) { + $branch = $branchMatch[1]; + } + + return ['repo' => $repo, 'branch' => $branch]; + } + + return ['repo' => null, 'branch' => 'main']; + } + + protected function extractVersionFromFilename(string $filename): string + { + $filename = basename($filename, '.jar'); + + if (preg_match('/[\d]+\.[\d]+\.[\d]+/', $filename, $matches)) { + return $matches[0]; + } + + if (preg_match('/v?([\d]+)/', $filename, $matches)) { + return $matches[1]; + } + + return ''; + } + + protected function commandDirExists(string $path): bool + { + $result = Process::timeout(5)->run('[ -d ' . escapeshellarg($path) . ' ] && echo "1" || echo "0"'); + + return trim($result->output()) === '1'; + } + + protected function commandFileExists(string $path): bool + { + $result = Process::timeout(5)->run('ls ' . $path . ' 2>/dev/null | head -1'); + + return $result->successful() && trim($result->output()) !== ''; + } +} diff --git a/app/Services/Emulator/EmulatorJarService.php b/app/Services/Emulator/EmulatorJarService.php new file mode 100644 index 0000000..18b831a --- /dev/null +++ b/app/Services/Emulator/EmulatorJarService.php @@ -0,0 +1,510 @@ +loadConfiguration(); + } + + public function isConfigured(): bool + { + return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true); + } + + public function checkForUpdates(): array + { + if (! $this->isConfigured()) { + return [ + 'update_available' => false, + 'error' => 'Configureer een GitHub URL of directe .jar URL', + ]; + } + + $sourceService = new EmulatorSourceService; + $sourceInfo = $sourceService->checkForUpdates(); + $hasSourceUpdates = $sourceInfo && $sourceInfo['has_update']; + + if (! in_array($this->jarDirectUrl, [null, '', '0'], true)) { + return $this->checkDirectUrlUpdates($sourceInfo, $hasSourceUpdates); + } + + return $this->checkGitHubFolderUpdates($sourceInfo, $hasSourceUpdates); + } + + public function performUpdate(array $check): array + { + $jarUrl = $check['jar_url']; + $jarName = $check['jar_name']; + $version = $check['latest_version']; + $serviceName = $this->emulatorService; + + $tempDir = '/tmp/emulator-update-' . Str::random(8); + $tempJar = $tempDir . '/' . $jarName; + + $updateScript = $this->buildUpdateScript($jarUrl, $jarName, $tempDir, $tempJar, $serviceName); + + $scriptPath = '/tmp/emulator_update_' . uniqid() . '.sh'; + file_put_contents($scriptPath, $updateScript); + chmod($scriptPath, 0755); + + Log::info('[EmulatorJar] Starting update', [ + 'version' => $version, + 'jar' => $jarName, + 'url' => $jarUrl, + ]); + + try { + $result = Process::timeout(600)->run('bash ' . $scriptPath . ' 2>&1'); + @unlink($scriptPath); + + if ($result->exitCode() !== 0) { + Log::error('[EmulatorJar] Update failed', [ + 'output' => $result->output(), + 'error' => $result->errorOutput(), + ]); + + return [ + 'success' => false, + 'error' => 'Update mislukt: ' . substr($result->output(), 0, 300), + ]; + } + + $this->storeUpdateInfo($version, $check); + + Log::info('[EmulatorJar] Update successful'); + + return [ + 'success' => true, + 'version' => $version, + 'jar' => $jarName, + 'message' => "✅ Emulator geüpdatet naar v{$version}!\n📦 {$jarName}\n🔄 Service herstart", + ]; + } catch (\Exception $e) { + Log::error('[EmulatorJar] Exception', ['error' => $e->getMessage()]); + + return [ + 'success' => false, + 'error' => $e->getMessage(), + ]; + } + } + + public function findLatestJar(): ?array + { + if (! $this->githubRepo) { + return null; + } + + $branch = $this->githubBranch ?: 'main'; + + $commonNames = ['arcturus.jar', 'Arcturus.jar', 'emulator.jar', 'habbo.jar', 'hotel.jar']; + + foreach ($commonNames as $name) { + try { + $apiUrl = "https://api.github.com/repos/{$this->githubRepo}/contents/Latest_Compiled_Version/{$name}?ref={$branch}"; + $response = Http::timeout(10) + ->withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'AtomCMS-Emulator-Updater', + ]) + ->get($apiUrl); + + if (! $response->successful()) { + continue; + } + + $data = json_decode($response->body(), true); + + if (! isset($data['sha']) || ! isset($data['download_url'])) { + continue; + } + + $commitDate = null; + $commitSha = $data['sha']; + + try { + $commitsResponse = Http::timeout(10) + ->withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'AtomCMS-Emulator-Updater', + ]) + ->get("https://api.github.com/repos/{$this->githubRepo}/commits", [ + 'path' => "Latest_Compiled_Version/{$name}", + 'sha' => $branch, + 'per_page' => 1, + ]); + + if ($commitsResponse->successful()) { + $commits = $commitsResponse->json(); + if (! empty($commits) && isset($commits[0]['commit']['committer']['date'])) { + $commitDate = strtotime($commits[0]['commit']['committer']['date']); + $commitSha = $commits[0]['sha'] ?? $data['sha']; + } + } + } catch (\Exception) { + Log::debug('[EmulatorJar] Could not fetch commit date for ' . $name); + } + + $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); + $installedJarCommit = $this->settings->getOrDefault('emulator_jar_commit', null); + + $isUpdate = false; + if ($installedJarCommit !== null && $commitSha !== null) { + $isUpdate = $installedJarCommit !== $commitSha; + } elseif ($installedDate !== null && $commitDate) { + $isUpdate = (int) $installedDate < $commitDate; + } elseif ($installedDate === null && $commitDate) { + $isUpdate = true; + } + + $version = $this->extractVersionFromFilename($name); + if ($version === '' || $version === '0') { + $version = $commitDate ? date('Y.m.d', $commitDate) : date('Y.m.d'); + } + + return [ + 'name' => $name, + 'url' => $data['download_url'], + 'version' => $version, + 'commit' => $commitSha, + 'commit_date' => $commitDate, + 'is_update' => $isUpdate, + 'installed_date' => $installedDate, + ]; + } catch (\Exception $e) { + Log::warning('[EmulatorJar] Error checking JAR file ' . $name, ['error' => $e->getMessage()]); + } + } + + return null; + } + + private function checkDirectUrlUpdates(?array $sourceInfo, bool $hasSourceUpdates): array + { + $jarInfo = $this->validateDirectUrl($this->jarDirectUrl); + + if ($jarInfo) { + if (! empty($jarInfo['version'])) { + $currentVersion = $this->settings->getOrDefault('emulator_version', '0.0.0'); + if ($jarInfo['version'] !== $currentVersion) { + $this->settings->set('emulator_version', $jarInfo['version']); + } + } + + $hasJarUpdates = $jarInfo['is_update'] ?? false; + + if ($hasSourceUpdates && ! $hasJarUpdates) { + return [ + 'update_available' => true, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $jarInfo['version'], + 'release_name' => 'Source Update', + 'jar_url' => null, + 'jar_name' => null, + 'jar_size' => 'Onbekend', + 'type' => 'source_build', + 'source_info' => $sourceInfo, + 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', + ]; + } + + return [ + 'update_available' => $hasJarUpdates || $hasSourceUpdates, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'latest_version' => $jarInfo['version'], + 'release_name' => $hasSourceUpdates ? 'Source Update' : 'Direct URL', + 'jar_url' => $this->jarDirectUrl, + 'jar_name' => $jarInfo['name'], + 'jar_size' => 'Onbekend', + 'type' => $hasSourceUpdates ? 'source_build' : 'direct_url', + 'commit' => $jarInfo['commit_sha'] ?? null, + 'source_info' => $sourceInfo, + 'has_source_updates' => $hasSourceUpdates, + ]; + } + + if ($hasSourceUpdates) { + return [ + 'update_available' => true, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend', + 'release_name' => 'Source Update', + 'jar_url' => null, + 'jar_name' => null, + 'jar_size' => 'Onbekend', + 'type' => 'source_build', + 'source_info' => $sourceInfo, + 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', + ]; + } + + return [ + 'update_available' => false, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'error' => 'Directe .jar URL is niet bereikbaar', + ]; + } + + private function checkGitHubFolderUpdates(?array $sourceInfo, bool $hasSourceUpdates): array + { + $jarInfo = $this->findLatestJar(); + + if (! $jarInfo) { + if ($hasSourceUpdates) { + return [ + 'update_available' => true, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend', + 'release_name' => 'Source Update', + 'jar_url' => null, + 'jar_name' => null, + 'jar_size' => 'Onbekend', + 'type' => 'source_build', + 'source_info' => $sourceInfo, + 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', + ]; + } + + return [ + 'update_available' => false, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'latest_version' => 'Onbekend', + 'error' => 'Kon geen .jar bestand vinden', + 'type' => 'not_found', + 'source_available' => $sourceInfo !== null, + ]; + } + + $version = $jarInfo['version']; + $hasJarUpdates = $jarInfo['is_update'] ?? false; + + if ($hasSourceUpdates && ! $hasJarUpdates) { + return [ + 'update_available' => true, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version, + 'release_name' => 'Source Update', + 'jar_url' => null, + 'jar_name' => null, + 'jar_size' => 'Onbekend', + 'type' => 'source_build', + 'source_info' => $sourceInfo, + 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', + ]; + } + + return [ + 'update_available' => $hasJarUpdates || $hasSourceUpdates, + 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), + 'latest_version' => $hasSourceUpdates ? ($sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version) : $version, + 'release_name' => $hasSourceUpdates ? 'Source Update' : 'Latest from GitHub', + 'jar_url' => $jarInfo['url'], + 'jar_name' => $jarInfo['name'], + 'jar_size' => 'Onbekend', + 'type' => $hasSourceUpdates ? 'source_build' : 'github_folder', + 'commit' => $jarInfo['commit'] ?? null, + 'commit_date' => $jarInfo['commit_date'] ?? null, + 'source_info' => $sourceInfo, + 'has_source_updates' => $hasSourceUpdates, + ]; + } + + private function validateDirectUrl(string $url): ?array + { + if ($url === '' || $url === '0' || ! str_ends_with(strtolower($url), '.jar')) { + return null; + } + + try { + $jarName = basename(parse_url($url, PHP_URL_PATH)); + $version = $this->extractVersionFromFilename($jarName); + $storedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0'); + + $lastModified = null; + $commitSha = null; + $gitHubInfoAvailable = false; + + if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)/', $url, $matches)) { + $owner = $matches[1]; + $repo = $matches[2]; + $branch = $matches[3]; + $path = $matches[4]; + + try { + $apiResponse = Http::timeout(10) + ->withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'AtomCMS-Emulator-Updater', + ]) + ->get("https://api.github.com/repos/{$owner}/{$repo}/contents/{$path}", [ + 'ref' => $branch, + ]); + + if ($apiResponse->successful()) { + $data = $apiResponse->json(); + + if (isset($data['sha']) && ! isset($data['message'])) { + $gitHubInfoAvailable = true; + $commitSha = $data['sha']; + + if (isset($data['commit']['committer']['date'])) { + $lastModified = strtotime($data['commit']['committer']['date']); + } + } + } + } catch (\Exception) { + Log::debug('[EmulatorJar] Could not fetch GitHub commit info for direct URL'); + } + } + + if ($lastModified === null) { + $response = Http::timeout(10)->head($url); + if ($response->successful()) { + $modifiedSince = $response->header('Last-Modified'); + if ($modifiedSince) { + $lastModified = strtotime($modifiedSince); + if ($lastModified === false) { + $lastModified = null; + } + } + } + } + + $isUpdate = false; + $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); + + $storedData = $this->settings->getOrDefault('emulator_direct_url_info_' . md5($url)); + if ($storedData !== null && is_string($storedData)) { + $storedDataArray = json_decode($storedData, true); + if (is_array($storedDataArray)) { + if ($gitHubInfoAvailable && $commitSha !== null && isset($storedDataArray['commit_sha'])) { + $isUpdate = $commitSha !== $storedDataArray['commit_sha']; + } elseif ($lastModified !== null && $installedDate !== null) { + $isUpdate = (int) $installedDate < $lastModified; + } elseif ($lastModified !== null && isset($storedDataArray['last_modified'])) { + $isUpdate = $lastModified > $storedDataArray['last_modified']; + } elseif (! in_array($version, ['', '0', $storedVersion], true)) { + $isUpdate = version_compare($version, $storedVersion) > 0; + } + } + } else { + $isUpdate = true; + } + + $infoToStore = [ + 'last_checked' => time(), + 'version' => $version, + ]; + if ($lastModified) { + $infoToStore['last_modified'] = $lastModified; + } + if ($commitSha) { + $infoToStore['commit_sha'] = $commitSha; + } + $this->settings->set('emulator_direct_url_info_' . md5($url), json_encode($infoToStore)); + + return [ + 'name' => $jarName, + 'version' => $version, + 'last_modified' => $lastModified, + 'commit_sha' => $commitSha, + 'is_update' => $isUpdate, + 'gitHub_rate_limited' => ! $gitHubInfoAvailable && preg_match('/github\.com/', $url), + ]; + } catch (\Exception $e) { + Log::warning('[EmulatorJar] Direct URL not reachable', ['error' => $e->getMessage()]); + } + + return null; + } + + private function buildUpdateScript(string $jarUrl, string $jarName, string $tempDir, string $tempJar, string $serviceName): string + { + return <</dev/null 2>&1; then + mkdir -p "\$JAR_PATH/backup" + mv "\$JAR_PATH"/*.jar "\$JAR_PATH/backup/" 2>/dev/null || true +fi + +download_success=false +for attempt in 1 2 3; do + if curl -L --max-time 300 --retry 3 --retry-delay 5 -o "\$TEMP_JAR" "\$JAR_URL" 2>&1; then + FILE_TYPE=\$(file -b "\$TEMP_JAR" 2>/dev/null) + JAR_SIZE=\$(stat -c%s "\$TEMP_JAR" 2>/dev/null || echo 0) + + if echo "\$FILE_TYPE" | grep -qi "zip\|jar\|archive" && [ "\$JAR_SIZE" -gt 1000 ]; then + download_success=true + break + else + rm -f "\$TEMP_JAR" 2>/dev/null || true + sleep 3 + fi + else + sleep 5 + fi +done + +if [ "\$download_success" = false ]; then + if ls "\$JAR_PATH/backup"/*.jar 1>/dev/null 2>&1; then + mv "\$JAR_PATH/backup"/*.jar "\$JAR_PATH/" 2>/dev/null || true + fi + rm -rf "\$TEMP_DIR" + exit 1 +fi + +mv "\$TEMP_JAR" "\$JAR_PATH/" +chown -R www-data:www-data "\$JAR_PATH" +chmod 755 "\$JAR_PATH/\$JAR_NAME" + +systemctl restart "\$SERVICE" 2>&1 || service "\$SERVICE" restart 2>&1 || true + +rm -rf "\$TEMP_DIR" 2>/dev/null || true +BASH; + } + + private function storeUpdateInfo(string $version, array $check): void + { + setting('emulator_version', $version); + + $commitDate = $check['commit_date'] ?? time(); + $this->settings->set('emulator_jar_installed_date', (string) $commitDate); + $this->settings->set('emulator_jar_commit', $check['commit'] ?? null); + + $sourceSha = $check['source_info']['latest_sha'] ?? $check['commit'] ?? null; + $sourceDate = $check['source_info']['latest_timestamp'] ?? ($check['commit_date'] ?? time()); + if ($sourceSha) { + $this->settings->set('emulator_source_commit', $sourceSha); + } + if ($sourceDate) { + $this->settings->set('emulator_source_date', (string) $sourceDate); + } + + $currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; + $this->settings->set('emulator_installed_branch', $currentBranch); + } +} diff --git a/app/Services/Emulator/EmulatorSourceService.php b/app/Services/Emulator/EmulatorSourceService.php new file mode 100755 index 0000000..ae6a7ec --- /dev/null +++ b/app/Services/Emulator/EmulatorSourceService.php @@ -0,0 +1,273 @@ +loadConfiguration(); + } + + public function checkForUpdates(): ?array + { + $repo = $this->sourceRepo ?: $this->githubRepo; + $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; + + if (! $repo) { + return null; + } + + $localCheck = $this->checkLocalSourceUpdates(); + if ($localCheck !== null) { + return $localCheck; + } + + return $this->checkRemoteSourceUpdates($repo, $branch); + } + + public function isSourceBuildAvailable(): bool + { + $hasRepo = ! in_array($this->sourceRepo, [null, '', '0'], true) || ! in_array($this->githubRepo, [null, '', '0'], true); + $hasPath = ! in_array($this->emulatorSourcePath, [null, '', '0'], true); + + if (! $hasRepo || ! $hasPath) { + return false; + } + + try { + $result = Process::timeout(5)->run("[ -d {$this->emulatorSourcePath} ] && echo 'exists' || echo 'not_exists'"); + + return trim($result->output()) === 'exists'; + } catch (\Exception) { + return false; + } + } + + private function checkLocalSourceUpdates(): ?array + { + $sourcePath = $this->emulatorSourcePath; + + $existsCheck = Process::timeout(5)->run("[ -d {$sourcePath} ] && echo 'exists'"); + if (! $existsCheck->successful() || trim($existsCheck->output()) !== 'exists') { + return $this->checkSourceByCloning(); + } + + try { + $gitCheck = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse --is-inside-work-tree 2>/dev/null"); + if (! $gitCheck->successful()) { + return $this->checkSourceByCloning(); + } + + $currentCommitResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse HEAD"); + if (! $currentCommitResult->successful()) { + return null; + } + $currentSha = trim($currentCommitResult->output()); + + $fetchResult = Process::timeout(30)->run("cd {$sourcePath} && git fetch origin 2>&1"); + if ($fetchResult->failed()) { + Log::debug('[EmulatorSource] Git fetch failed, trying local check only'); + } + + $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; + $latestResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse origin/{$branch} 2>/dev/null || git rev-parse HEAD"); + if (! $latestResult->successful()) { + return null; + } + $latestSha = trim($latestResult->output()); + + $dateResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%ci' {$latestSha}"); + $latestDate = null; + $latestTimestamp = time(); + if ($dateResult->successful()) { + $latestDate = trim($dateResult->output(), "'"); + if ($latestDate !== '' && $latestDate !== '0') { + $latestTimestamp = strtotime($latestDate); + } + } + + $this->persistSourceCommitInfo($latestSha, $latestTimestamp); + + $msgResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%s' {$latestSha}"); + $latestMessage = $msgResult->successful() ? trim($msgResult->output()) : ''; + + $authorResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%an' {$latestSha}"); + $latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : ''; + + $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); + $storedDate = $this->settings->getOrDefault('emulator_source_date', null); + + $isUpdate = $storedSha !== null && $storedSha !== $latestSha; + if ($storedSha === null) { + $isUpdate = true; + } + + return [ + 'has_update' => $isUpdate, + 'latest_sha' => $latestSha, + 'latest_date' => $latestDate, + 'latest_timestamp' => $latestTimestamp, + 'latest_message' => $latestMessage, + 'latest_author' => $latestAuthor, + 'stored_sha' => $storedSha, + 'stored_date' => $storedDate, + 'source' => 'local', + ]; + } catch (\Exception $e) { + Log::debug('[EmulatorSource] Local source check failed: ' . $e->getMessage()); + + return null; + } + } + + private function checkSourceByCloning(): ?array + { + $repo = $this->sourceRepo ?: $this->githubRepo; + $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; + + if (! $repo) { + return null; + } + + $repo = preg_replace('#/tree/[^/]+/.*$#', '', $repo); + $repo = str_replace(['https://github.com/', 'http://github.com/'], '', $repo); + $repo = rtrim($repo, '/'); + + try { + $tempDir = '/tmp/emulator-source-check-' . Str::random(8); + + $cloneResult = Process::timeout(120)->run( + "git clone --branch {$branch} --depth 1 https://github.com/{$repo}.git {$tempDir} 2>&1", + ); + + if ($cloneResult->failed()) { + return null; + } + + $shaResult = Process::timeout(5)->run("cd {$tempDir} && git rev-parse HEAD"); + $latestSha = $shaResult->successful() ? trim($shaResult->output()) : ''; + + $dateResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%ci'"); + $latestDate = null; + $latestTimestamp = time(); + if ($dateResult->successful()) { + $latestDate = trim($dateResult->output()); + if ($latestDate !== '' && $latestDate !== '0') { + $latestTimestamp = strtotime($latestDate); + } + } + + $this->persistSourceCommitInfo($latestSha, $latestTimestamp); + + $msgResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%s'"); + $latestMessage = $msgResult->successful() ? trim($msgResult->output()) : ''; + + $authorResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%an'"); + $latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : ''; + + Process::timeout(10)->run("rm -rf {$tempDir}"); + + $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); + $storedDate = $this->settings->getOrDefault('emulator_source_date', null); + + $isUpdate = $storedSha !== null && $storedSha !== $latestSha; + if ($storedSha === null) { + $isUpdate = true; + } + + return [ + 'has_update' => $isUpdate, + 'latest_sha' => $latestSha, + 'latest_date' => $latestDate, + 'latest_timestamp' => $latestTimestamp, + 'latest_message' => $latestMessage, + 'latest_author' => $latestAuthor, + 'stored_sha' => $storedSha, + 'stored_date' => $storedDate, + 'source' => 'cloned', + ]; + } catch (\Exception $e) { + Log::debug('[EmulatorSource] Source clone check failed: ' . $e->getMessage()); + + return null; + } + } + + private function checkRemoteSourceUpdates(string $repo, string $branch): ?array + { + try { + $response = Http::timeout(10) + ->withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'AtomCMS-Emulator-Updater', + ]) + ->get("https://api.github.com/repos/{$repo}/commits", [ + 'sha' => $branch, + 'per_page' => 1, + ]); + + if (! $response->successful()) { + return null; + } + + $commits = $response->json(); + + if (empty($commits) || ! isset($commits[0]['sha'])) { + return null; + } + + $latestCommit = $commits[0]; + $latestSha = $latestCommit['sha']; + $latestDate = $latestCommit['commit']['committer']['date'] ?? null; + $latestTimestamp = $latestDate ? strtotime((string) $latestDate) : time(); + + $this->persistSourceCommitInfo($latestSha, $latestTimestamp); + + $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); + $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); + $storedDate = $this->settings->getOrDefault('emulator_source_date', null); + + if ($storedSha !== null && $storedSha === $latestSha) { + $isUpdate = false; + } elseif ($storedSha !== null && $storedSha !== $latestSha) { + $isUpdate = true; + } elseif ($installedDate !== null) { + $installedTimestamp = is_numeric($installedDate) ? (int) $installedDate : strtotime((string) $installedDate); + $isUpdate = $installedTimestamp < $latestTimestamp; + } else { + $isUpdate = false; + } + + return [ + 'has_update' => $isUpdate, + 'latest_sha' => $latestSha, + 'latest_date' => $latestDate, + 'latest_timestamp' => $latestTimestamp, + 'latest_message' => $latestCommit['commit']['message'] ?? '', + 'latest_author' => $latestCommit['commit']['author']['name'] ?? '', + 'stored_sha' => $storedSha, + 'stored_date' => $storedDate, + ]; + } catch (\Exception $e) { + Log::warning('[EmulatorSource] Could not check source updates via GitHub API', ['error' => $e->getMessage()]); + + return null; + } + } + + private function persistSourceCommitInfo(string $sha, int $timestamp): void + { + $this->settings->set('emulator_source_commit', $sha); + $this->settings->set('emulator_source_date', (string) $timestamp); + } +} diff --git a/app/Services/Emulator/EmulatorSqlService.php b/app/Services/Emulator/EmulatorSqlService.php new file mode 100755 index 0000000..aba1cc9 --- /dev/null +++ b/app/Services/Emulator/EmulatorSqlService.php @@ -0,0 +1,510 @@ +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)); + } +} diff --git a/app/Services/Emulator/EmulatorStatusService.php b/app/Services/Emulator/EmulatorStatusService.php new file mode 100755 index 0000000..cef6326 --- /dev/null +++ b/app/Services/Emulator/EmulatorStatusService.php @@ -0,0 +1,144 @@ +loadConfiguration(); + } + + public function getStatus(): array + { + $rconService = new RconService; + + return [ + 'is_connected' => $rconService->isConnected(), + 'service_running' => $this->isServiceRunning(), + 'jar_exists' => $this->commandFileExists($this->jarPath . '/*.jar'), + 'jar_files' => $this->getJarFiles(), + 'source_exists' => $this->commandDirExists($this->emulatorSourcePath), + 'emulator_db_connected' => $this->testEmulatorDbConnection(), + 'jar_path' => $this->jarPath, + 'source_path' => $this->emulatorSourcePath, + 'service_name' => $this->emulatorService, + ]; + } + + public function isServiceRunning(): bool + { + try { + $result = Process::timeout(5)->run('systemctl is-active ' . escapeshellarg((string) $this->emulatorService) . ' 2>/dev/null'); + + return trim($result->output()) === 'active'; + } catch (\Exception) { + return false; + } + } + + public function testEmulatorDbConnection(): bool + { + try { + $host = $this->settings->getOrDefault('emulator_database_host', config('database.connections.mysql.host', '127.0.0.1')); + $port = $this->settings->getOrDefault('emulator_database_port', config('database.connections.mysql.port', '3306')); + $name = $this->settings->getOrDefault('emulator_database_name', ''); + $username = $this->settings->getOrDefault('emulator_database_username', ''); + $password = $this->settings->getOrDefault('emulator_database_password', ''); + + if (empty($name) || empty($username)) { + return false; + } + + $result = Process::timeout(5)->run( + 'mysql -h ' . escapeshellarg((string) $host) . + ' -P ' . escapeshellarg((string) $port) . + ' -u ' . escapeshellarg((string) $username) . + ' -p' . escapeshellarg((string) $password) . + ' -e "SELECT 1" ' . escapeshellarg((string) $name) . ' 2>/dev/null | head -1', + ); + + return $result->successful(); + } catch (\Exception) { + return false; + } + } + + public function getJarFiles(): array + { + $result = Process::timeout(5)->run('ls -1 ' . escapeshellarg((string) $this->jarPath) . '/*.jar 2>/dev/null | head -5'); + if ($result->successful()) { + $files = array_filter(explode("\n", trim($result->output()))); + + return array_map(basename(...), $files); + } + + return []; + } + + public function getInstalledVersion(): string + { + return setting('emulator_version', 'Onbekend'); + } + + public function getInstalledJar(): ?string + { + try { + $result = Process::timeout(5)->run("ls -1 {$this->jarPath}/*.jar 2>/dev/null | head -1"); + + if ($result->successful()) { + $jarPath = trim($result->output()); + if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) { + return basename($jarPath); + } + } + } catch (\Exception) { + } + + return setting('emulator_version', 'Onbekend') !== 'Onbekend' + ? 'Emulator v' . setting('emulator_version') + : null; + } + + public function getInstalledJarInfo(): array + { + $files = []; + + try { + $result = Process::timeout(5)->run("ls -1lh {$this->jarPath}/*.jar 2>/dev/null"); + if ($result->successful()) { + $lines = array_filter(explode("\n", trim($result->output()))); + foreach ($lines as $line) { + $line = trim($line); + if ($line === '' || $line === '0') { + continue; + } + $parts = preg_split('/\s+/', $line); + $filename = end($parts); + if (str_contains((string) $filename, '.jar')) { + $size = $parts[4] ?? '?'; + $files[] = [ + 'name' => basename((string) $filename), + 'size' => $size, + ]; + } + } + } + } catch (\Exception) { + } + + return $files; + } + + public function isConfigured(): bool + { + return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true); + } +} diff --git a/app/Services/EmulatorUpdateService.php b/app/Services/EmulatorUpdateService.php index e3299c4..154d452 100755 --- a/app/Services/EmulatorUpdateService.php +++ b/app/Services/EmulatorUpdateService.php @@ -4,262 +4,49 @@ declare(strict_types=1); namespace App\Services; -use App\Models\Miscellaneous\WebsiteSetting; +use App\Services\Emulator\EmulatorBackupService; +use App\Services\Emulator\EmulatorBuildService; +use App\Services\Emulator\EmulatorJarService; +use App\Services\Emulator\EmulatorSourceService; +use App\Services\Emulator\EmulatorSqlService; +use App\Services\Emulator\EmulatorStatusService; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Process; -use Illuminate\Support\Facades\Schema; -use Illuminate\Support\Str; class EmulatorUpdateService { - private const string SQL_TABLE = 'emulator_sql_updates'; - - private readonly ?SettingsService $settings; - - private readonly ?string $githubUrl; - - private readonly ?string $jarDirectUrl; - - private ?string $githubRepo; - - private ?string $githubBranch; - - private readonly ?string $jarPath; - - private readonly ?string $emulatorService; - - private readonly ?string $databaseHost; - - private readonly ?string $databasePort; - - private readonly ?string $databaseName; - - private readonly ?string $databaseUsername; - - private readonly ?string $databasePassword; - - private readonly ?string $emulatorSourcePath; - - private ?string $sourceRepo; - - private ?string $sourceBranch; + private readonly EmulatorStatusService $statusService; + private readonly EmulatorJarService $jarService; + private readonly EmulatorSourceService $sourceService; + private readonly EmulatorBuildService $buildService; + private readonly EmulatorSqlService $sqlService; + private readonly EmulatorBackupService $backupService; + private readonly SettingsService $settings; public function __construct() { + $this->statusService = new EmulatorStatusService; + $this->jarService = new EmulatorJarService; + $this->sourceService = new EmulatorSourceService; + $this->buildService = new EmulatorBuildService; + $this->sqlService = new EmulatorSqlService; + $this->backupService = new EmulatorBackupService; $this->settings = app(SettingsService::class); - $this->settings->clearInstanceCache(); - $this->githubUrl = $this->settings->getOrDefault('emulator_github_url', ''); - $this->jarDirectUrl = $this->settings->getOrDefault('emulator_jar_direct_url', ''); - - $basePath = $this->detectBasePath(); - $this->jarPath = $this->resolveEmulatorPath($this->settings->getOrDefault('emulator_jar_path', $basePath . '/Emulator')); - $this->emulatorService = $this->settings->getOrDefault('emulator_service_name', $this->detectEmulatorService()); - $this->databaseHost = config('database.connections.mysql.host'); - $this->databasePort = config('database.connections.mysql.port', '3306'); - $this->databaseName = config('database.connections.mysql.database'); - $this->databaseUsername = config('database.connections.mysql.username'); - $this->databasePassword = config('database.connections.mysql.password'); - $this->emulatorSourcePath = $this->resolveEmulatorPath($this->settings->getOrDefault('emulator_source_path', $basePath . '/emulator-source')); - $this->sourceRepo = $this->settings->getOrDefault('emulator_source_repo', ''); - - $this->parseGitHubUrl($this->githubUrl); - $this->parseSourceRepo($this->sourceRepo); - $this->ensureGitSafeDirectories(); - - $settingBranch = $this->settings->getOrDefault('emulator_github_branch', null); - if ($settingBranch) { - $this->githubBranch = strtolower((string) $settingBranch); - $this->sourceBranch = strtolower((string) $settingBranch); - } - } - - private function detectBasePath(): string - { - $possiblePaths = [ - base_path(), - '/var/www/atomcms', - '/var/www/html', - '/var/www', - dirname(base_path()), - ]; - - foreach ($possiblePaths as $path) { - if (is_dir($path)) { - return $path; - } - } - - return '/var/www'; - } - - private function resolveEmulatorPath(string $path): string - { - if (str_starts_with($path, '/')) { - return $path; - } - - return $this->detectBasePath() . '/' . ltrim($path, '/'); - } - - private function detectEmulatorService(): string - { - $possibleServices = ['emulator', 'arcturus', 'morningstar', 'habbo', 'hotel', 'game']; - - foreach ($possibleServices as $service) { - $result = Process::timeout(5)->run("systemctl list-unit-files {$service}.service 2>/dev/null | grep -q '{$service}.service' && echo 'found'"); - if ($result->successful() && trim($result->output()) === 'found') { - return $service; - } - - $result = Process::timeout(5)->run("systemctl list-units --type=service --all 2>/dev/null | grep -q '{$service}.service' && echo 'found'"); - if ($result->successful() && trim($result->output()) === 'found') { - return $service; - } - } - - return 'emulator'; - } - - private function ensureGitSafeDirectories(): void - { - $this->detectBasePath(); - - $directories = [ - base_path(), - '/var/www/atomcms', - '/var/www', - $this->emulatorSourcePath, - $this->jarPath, - dirname((string) $this->emulatorSourcePath), - dirname((string) $this->jarPath), - ]; - - foreach (array_unique($directories) as $dir) { - if (! empty($dir) && $this->isDirAccessible($dir)) { - Process::timeout(5)->run('git config --global --add safe.directory ' . escapeshellarg($dir) . ' 2>/dev/null || true'); - } - } - - Process::timeout(5)->run("git config --global --add safe.directory '*' 2>/dev/null || true"); - - $this->ensureSudoAccess(); - } - - private function isDirAccessible(string $path): bool - { - if ($path === '' || $path === '0') { - return false; - } - - $result = Process::timeout(5)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"'); - - return trim($result->output()) === 'yes'; - } - - private function ensureSudoAccess(): void - { - $sudoersFile = '/etc/sudoers.d/atomcms-emulator'; - $webUser = 'www-data'; - - $content = "# Auto-generated by AtomCMS - Do not edit\n"; - $content .= "{$webUser} ALL=(ALL) NOPASSWD: /bin/systemctl start *, /bin/systemctl stop *, /bin/systemctl restart *, /usr/bin/systemctl start *, /usr/bin/systemctl stop *, /usr/bin/systemctl restart *\n"; - - // Check if file exists using command line (avoids open_basedir) - $checkResult = Process::timeout(5)->run("test -f {$sudoersFile} && echo 'exists' || echo 'missing'"); - $fileExists = trim($checkResult->output()) === 'exists'; - - if (! $fileExists) { - Process::timeout(5)->run("echo '{$content}' | sudo tee {$sudoersFile} > /dev/null 2>&1"); - Process::timeout(5)->run("sudo chmod 0440 {$sudoersFile} 2>/dev/null"); - } - } - - private function ensureJavaInstalled(): array - { - $actions = []; - $errors = []; - - $javaCheck = Process::timeout(5)->run('java -version 2>&1'); - if (! $javaCheck->successful()) { - $errors[] = 'Java niet geïnstalleerd'; - Log::warning('[EmulatorUpdate] Java not found, attempting to install'); - - $installResult = Process::timeout(180)->run('apt-get update && apt-get install -y default-jdk 2>&1'); - if ($installResult->successful()) { - $actions[] = 'Java (laatste versie) geïnstalleerd'; - } else { - $errors[] = 'Kon Java niet installeren'; - } - } else { - $javaVersion = explode("\n", $javaCheck->errorOutput())[0] ?? 'Java gevonden'; - $actions[] = trim($javaVersion); - } - - $mavenCheck = Process::timeout(5)->run('which mvn'); - if (! $mavenCheck->successful()) { - Log::warning('[EmulatorUpdate] Maven not found, attempting to install'); - $installResult = Process::timeout(180)->run('apt-get install -y maven 2>&1'); - if ($installResult->successful()) { - $actions[] = 'Maven geïnstalleerd'; - } - } else { - $actions[] = 'Maven gevonden'; - } - - $gradleCheck = Process::timeout(5)->run('which gradle'); - if (! $gradleCheck->successful()) { - Log::warning('[EmulatorUpdate] Gradle not found, will use wrapper if available'); - } else { - $actions[] = 'Gradle gevonden'; - } - - return [ - 'actions' => $actions, - 'errors' => $errors, - ]; } public function isConfigured(): bool { - return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true); + return $this->statusService->isConfigured(); } public function getStatus(): array { - $rconService = new RconService; + $status = $this->statusService->getStatus(); + $updateCheck = $this->jarService->checkForUpdates(); + $sourceInfo = $this->sourceService->checkForUpdates(); - // Check basic status - $isConnected = $rconService->isConnected(); - - // Check for updates - $updateCheck = $this->checkForUpdates(); - - // Check JAR file - $jarExists = $this->commandFileExists($this->jarPath . '/*.jar'); - $jarFiles = $this->getJarFiles(); - - // Check source - $sourceExists = $this->commandDirExists($this->emulatorSourcePath); - - // Check service - $serviceRunning = $this->isServiceRunning(); - - // Check emulator database - $emulatorDbConnected = $this->testEmulatorDbConnection(); - - // Get commits - $sourceInfo = $this->checkForSourceUpdates(); - - return [ - 'is_connected' => $isConnected, - 'service_running' => $serviceRunning, - 'jar_exists' => $jarExists, - 'jar_files' => $jarFiles, - 'source_exists' => $sourceExists, - 'source_info' => $sourceInfo, + return array_merge($status, [ 'update_available' => $updateCheck['update_available'] ?? false, 'current_version' => $updateCheck['current_version'] ?? setting('emulator_version', 'N/A'), 'latest_version' => $updateCheck['latest_version'] ?? 'N/A', @@ -271,409 +58,28 @@ class EmulatorUpdateService 'latest_date' => $sourceInfo['latest_date'] ?? null, 'stored_sha' => $sourceInfo['stored_sha'] ?? null, 'stored_date' => $sourceInfo['stored_date'] ?? null, - 'jar_path' => $this->jarPath, - 'source_path' => $this->emulatorSourcePath, - 'service_name' => $this->emulatorService, - 'emulator_db_connected' => $emulatorDbConnected, - ]; - } - - private function testEmulatorDbConnection(): bool - { - try { - $result = Process::timeout(5)->run('mysql -h ' . escapeshellarg((string) $this->databaseHost) . ' -P ' . escapeshellarg((string) $this->databasePort) . ' -u ' . escapeshellarg((string) $this->databaseUsername) . ' -p' . escapeshellarg((string) $this->databasePassword) . ' -e "SELECT 1" ' . escapeshellarg((string) $this->databaseName) . ' 2>/dev/null | head -1'); - - return $result->successful(); - } catch (\Exception) { - return false; - } - } - - private function isServiceRunning(): bool - { - try { - $result = Process::timeout(5)->run('systemctl is-active ' . escapeshellarg((string) $this->emulatorService) . ' 2>/dev/null'); - - return trim($result->output()) === 'active'; - } catch (\Exception) { - return false; - } - } - - private function getJarFiles(): array - { - $result = Process::timeout(5)->run('ls -1 ' . escapeshellarg((string) $this->jarPath) . '/*.jar 2>/dev/null | head -5'); - if ($result->successful()) { - $files = array_filter(explode("\n", trim($result->output()))); - - return array_map(basename(...), $files); - } - - return []; + 'source_info' => $sourceInfo, + ]); } public function checkForUpdates(): array { - if (! $this->isConfigured()) { - return [ - 'update_available' => false, - 'error' => 'Configureer een GitHub URL of directe .jar URL', - ]; - } - - // Check for source updates FIRST (independent of JAR method) - $sourceInfo = $this->checkForSourceUpdates(); - $hasSourceUpdates = $sourceInfo && $sourceInfo['has_update']; - - if (! in_array($this->jarDirectUrl, [null, '', '0'], true)) { - $jarInfo = $this->validateDirectUrl($this->jarDirectUrl); - - if ($jarInfo) { - // Update the stored version if we're checking a direct URL - if (! empty($jarInfo['version'])) { - $currentVersion = $this->settings->getOrDefault('emulator_version', '0.0.0'); - if ($jarInfo['version'] !== $currentVersion) { - $this->settings->set('emulator_version', $jarInfo['version']); - } - } - - // Check if source has updates even if JAR doesn't - $hasJarUpdates = $jarInfo['is_update'] ?? false; - - if ($hasSourceUpdates && ! $hasJarUpdates) { - return [ - 'update_available' => true, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $jarInfo['version'], - 'release_name' => 'Source Update', - 'jar_url' => null, - 'jar_name' => null, - 'jar_size' => 'Onbekend', - 'type' => 'source_build', - 'source_info' => $sourceInfo, - 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', - ]; - } - - return [ - 'update_available' => $hasJarUpdates || $hasSourceUpdates, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'latest_version' => $jarInfo['version'], - 'release_name' => $hasSourceUpdates ? 'Source Update' : 'Direct URL', - 'jar_url' => $this->jarDirectUrl, - 'jar_name' => $jarInfo['name'], - 'jar_size' => 'Onbekend', - 'type' => $hasSourceUpdates ? 'source_build' : 'direct_url', - 'commit' => $jarInfo['commit_sha'] ?? null, - 'source_info' => $sourceInfo, - 'has_source_updates' => $hasSourceUpdates, - ]; - } - - // URL not reachable, but check source updates - if ($hasSourceUpdates) { - return [ - 'update_available' => true, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend', - 'release_name' => 'Source Update', - 'jar_url' => null, - 'jar_name' => null, - 'jar_size' => 'Onbekend', - 'type' => 'source_build', - 'source_info' => $sourceInfo, - 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', - ]; - } - - return [ - 'update_available' => false, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'error' => 'Directe .jar URL is niet bereikbaar', - ]; - } - - $jarInfo = $this->findLatestJar(); - - if (! $jarInfo) { - if ($hasSourceUpdates) { - return [ - 'update_available' => true, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend', - 'release_name' => 'Source Update', - 'jar_url' => null, - 'jar_name' => null, - 'jar_size' => 'Onbekend', - 'type' => 'source_build', - 'source_info' => $sourceInfo, - 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', - ]; - } - - return [ - 'update_available' => false, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'latest_version' => 'Onbekend', - 'error' => 'Kon geen .jar bestand vinden', - 'type' => 'not_found', - 'source_available' => $sourceInfo !== null, - ]; - } - - $version = $jarInfo['version']; - $commitDate = $jarInfo['commit_date'] ?? null; - - $hasJarUpdates = $jarInfo['is_update'] ?? false; - - if ($hasSourceUpdates && ! $hasJarUpdates) { - return [ - 'update_available' => true, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version, - 'release_name' => 'Source Update', - 'jar_url' => null, - 'jar_name' => null, - 'jar_size' => 'Onbekend', - 'type' => 'source_build', - 'source_info' => $sourceInfo, - 'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig', - ]; - } - - return [ - 'update_available' => $hasJarUpdates || $hasSourceUpdates, - 'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'), - 'latest_version' => $hasSourceUpdates ? ($sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version) : $version, - 'release_name' => $hasSourceUpdates ? 'Source Update' : 'Latest from GitHub', - 'jar_url' => $jarInfo['url'], - 'jar_name' => $jarInfo['name'], - 'jar_size' => 'Onbekend', - 'type' => $hasSourceUpdates ? 'source_build' : 'github_folder', - 'commit' => $jarInfo['commit'] ?? null, - 'commit_date' => $commitDate, - 'source_info' => $sourceInfo, - 'has_source_updates' => $hasSourceUpdates, - ]; + return $this->jarService->checkForUpdates(); } public function checkForSqlUpdates(bool $recentOnly = true): array { - if (! $this->githubRepo) { - return [ - 'has_updates' => false, - 'error' => 'Geen GitHub repo geconfigureerd', - ]; - } - - $this->ensureSqlTableExists(); - - $result = $this->fetchSqlFilesFromGitHub($recentOnly); - - // Check if result is an error response - 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), - ]; - } - - private function ensureSqlTableExists(): void - { - $tableName = self::SQL_TABLE; - - if (Schema::hasTable($tableName)) { - return; - } - - Schema::create($tableName, 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 getAppliedSqlFiles(): array - { - $this->ensureSqlTableExists(); - - return DB::table(self::SQL_TABLE) - ->orderBy('applied_at', 'desc') - ->get() - ->toArray(); + return $this->sqlService->checkForUpdates($recentOnly); } public function runSqlUpdates(): array { - $sqlCheck = $this->checkForSqlUpdates(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 (isset($results['errors']) && $results['errors'] !== []) { - $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!'; - } - - if (isset($results['files_run']) && $results['files_run'] !== [] && $this->restartEmulator()) { - $results['message'] .= ' Emulator herstart.'; - } - - return $results; - } - - public function restartEmulator(): bool - { - $serviceName = $this->emulatorService; - - try { - Log::info('[EmulatorUpdate] Restarting emulator service: ' . $serviceName); - - $commands = [ - "sudo systemctl restart {$serviceName} 2>&1", - "sudo service {$serviceName} restart 2>&1", - "sudo /etc/init.d/{$serviceName} restart 2>&1", - "sudo systemctl stop {$serviceName} && sudo systemctl start {$serviceName} 2>&1", - ]; - - foreach ($commands as $cmd) { - $output = shell_exec($cmd); - if (! str_contains($output ?? '', 'error') && ! str_contains($output ?? '', 'failed')) { - Log::info('[EmulatorUpdate] Emulator restarted successfully'); - - return true; - } - } - - Log::warning('[EmulatorUpdate] All restart methods failed, trying pgrep'); - - $pidCheck = shell_exec("pgrep -f '{$serviceName}'"); - if (! in_array(trim($pidCheck ?? ''), ['', '0'], true)) { - $pids = explode("\n", trim($pidCheck)); - foreach ($pids as $pid) { - shell_exec("kill -9 {$pid} 2>&1"); - } - sleep(2); - } - - $startCommands = [ - "sudo systemctl start {$serviceName} 2>&1", - "sudo service {$serviceName} start 2>&1", - ]; - - foreach ($startCommands as $cmd) { - $output = shell_exec($cmd); - if (! str_contains($output ?? '', 'error') && ! str_contains($output ?? '', 'failed')) { - Log::info('[EmulatorUpdate] Emulator started successfully'); - - return true; - } - } - - return false; - } catch (\Exception $e) { - Log::error('[EmulatorUpdate] Failed to restart emulator', ['error' => $e->getMessage()]); - - return false; - } - } - - 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(), - ], - ); + return $this->sqlService->runUpdates(); } public function getAppliedSqlUpdates(): array { - $this->ensureSqlTableExists(); - - return $this->getAppliedSqlFiles(); + return $this->sqlService->getAppliedUpdates(); } public function updateEmulator(): array @@ -692,10 +98,9 @@ class EmulatorUpdateService return ['success' => false, 'error' => 'Emulator is al up-to-date']; } - // Build from source if there are source updates (regardless of type) $hasSourceUpdates = ($check['has_source_updates'] ?? false) || ($check['type'] ?? '') === 'source_build'; - if ($hasSourceUpdates && $this->isSourceBuildAvailable()) { + if ($hasSourceUpdates && $this->sourceService->isSourceBuildAvailable()) { return $this->buildFromSource(); } @@ -707,7 +112,7 @@ class EmulatorUpdateService return ['success' => false, 'error' => 'Geen .jar gevonden']; } - $result = $this->performUpdate($check); + $result = $this->jarService->performUpdate($check); if ($result['success']) { $this->runSqlUpdates(); @@ -721,1363 +126,61 @@ class EmulatorUpdateService return $result; } - private function downloadAndRunSql(array $file): array - { - try { - $response = Http::timeout(60)->get($file['url']); - - if (! $response->successful()) { - return ['success' => false, 'error' => 'Download mislukt']; - } - - $sql = $response->body(); - - $sql = $this->cleanSql($sql); - - if (in_array(trim($sql), ['', '0'], true)) { - return ['success' => true, 'message' => 'Lege SQL file overgeslagen']; - } - - // Get database credentials from settings or .env - $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. Vul de database gegevens in bij de instellingen.']; - } - - $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)); - } - - private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array - { - if (! $this->githubRepo) { - return []; - } - - $branch = $this->githubBranch ?: 'main'; - - // Try multiple possible folder names (with URL encoding) - $folderNames = [ - 'Database%20Updates', - 'Database Updates', - 'database_updates', - 'database/updates', - 'sql/updates', - 'sql', - 'updates', - ]; - - // Known SQL files in Arcturus-Morningstar-Extended repository - // We check raw GitHub content which has no rate limit - $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 = []; - - // Check each known file exists via raw GitHub (no rate limit) - 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; // Found in this folder, move to next file - } - } - } - - if ($sqlFiles !== []) { - usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name'])); - - return $sqlFiles; - } - - return []; - } catch (\Exception $e) { - Log::warning('[EmulatorUpdate] Could not fetch SQL files', ['error' => $e->getMessage()]); - - return []; - } - } - - private function fetchRecentSqlFiles(string $branch): array - { - $weekAgo = now()->subDays(7)->toIso8601String(); - - // Get GitHub token from settings if available - $githubToken = setting('github_token', ''); - $headers = [ - 'Accept' => 'application/vnd.github+json', - 'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0', - ]; - if (! empty($githubToken)) { - $headers['Authorization'] = 'Bearer ' . $githubToken; - } - - // Try multiple folder names - $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'])); - Log::info('[EmulatorUpdate] Found recent SQL files in folder: ' . $folderName, ['count' => count($sqlFiles)]); - - return $sqlFiles; - } - } - } - - return []; - } catch (\Exception $e) { - Log::warning('[EmulatorUpdate] Could not fetch recent SQL files', ['error' => $e->getMessage()]); - - return []; - } - } - public function performUpdate(array $check): array { - $jarUrl = $check['jar_url']; - $jarName = $check['jar_name']; - $version = $check['latest_version']; - $serviceName = $this->emulatorService; - - $tempDir = '/tmp/emulator-update-' . Str::random(8); - $tempJar = $tempDir . '/' . $jarName; - - // Build update script with comprehensive fallbacks - $updateScript = <</dev/null | tail -1 | awk '{print \$4}') -if [ "\$FREE_MB" -lt 500 ] 2>/dev/null; then - echo "WARNING: Low disk space (\${FREE_MB}MB), cleaning temp..." - rm -rf /tmp/emulator-update-* /tmp/nitro_* /tmp/nitro-switch-* 2>/dev/null || true -fi - -mkdir -p "\$TEMP_DIR" "\$JAR_PATH" - -# Backup existing JAR -if ls "\$JAR_PATH"/*.jar 1>/dev/null 2>&1; then - mkdir -p "\$JAR_PATH/backup" - mv "\$JAR_PATH"/*.jar "\$JAR_PATH/backup/" 2>/dev/null || true - echo "Existing JAR backed up" -fi - -# Download with retries -download_success=false -for attempt in 1 2 3; do - echo "Download attempt \$attempt/3..." - if curl -L --max-time 300 --retry 3 --retry-delay 5 -o "\$TEMP_JAR" "\$JAR_URL" 2>&1; then - # Validate it's actually a JAR - FILE_TYPE=\$(file -b "\$TEMP_JAR" 2>/dev/null) - JAR_SIZE=\$(stat -c%s "\$TEMP_JAR" 2>/dev/null || echo 0) - echo "Downloaded: \$JAR_SIZE bytes, type: \$FILE_TYPE" - - if echo "\$FILE_TYPE" | grep -qi "zip\|jar\|archive" && [ "\$JAR_SIZE" -gt 1000 ]; then - download_success=true - break - else - echo "Invalid file (not a JAR), retrying..." - rm -f "\$TEMP_JAR" 2>/dev/null || true - sleep 3 - fi - else - echo "Curl failed, retrying in 5s..." - sleep 5 - fi -done - -if [ "\$download_success" = false ]; then - echo "ERROR: Download failed after 3 attempts" - # Restore backup - if ls "\$JAR_PATH/backup"/*.jar 1>/dev/null 2>&1; then - mv "\$JAR_PATH/backup"/*.jar "\$JAR_PATH/" 2>/dev/null || true - echo "Backup restored" - fi - rm -rf "\$TEMP_DIR" - exit 1 -fi - -# Move JAR to target -mv "$TEMP_JAR" "$JAR_PATH/" -chown -R www-data:www-data "$JAR_PATH" -chmod 755 "$JAR_PATH/$JAR_NAME" -echo "JAR installed: $JAR_PATH/$JAR_NAME" - -# Restart service (try all methods) -service_restarted=false -for method in "systemctl restart \"\$SERVICE\"" "service \"\$SERVICE\" restart" "/etc/init.d/\$SERVICE restart" "kill -HUP \$(pgrep -f \"\$SERVICE\")" ; do - echo "Trying: \$method" - eval "\$method" 2>/dev/null && { service_restarted=true; break; } || true -done -if [ "\$service_restarted" = false ]; then - echo "WARNING: Service restart may have failed (check manually)" >&2 - exit 1 -fi - -# Cleanup -rm -rf "\$TEMP_DIR" 2>/dev/null || true -echo "" -echo "=== Update complete ===" -BASH; - - $scriptPath = '/tmp/emulator_update_' . uniqid() . '.sh'; - file_put_contents($scriptPath, $updateScript); - chmod($scriptPath, 0755); - - Log::info('[EmulatorUpdate] Starting update', [ - 'version' => $version, - 'jar' => $jarName, - 'url' => $jarUrl, - ]); - - try { - $result = Process::timeout(600)->run('bash ' . $scriptPath . ' 2>&1'); - @unlink($scriptPath); - - Log::info('[EmulatorUpdate] Update exitCode', ['exitCode' => $result->exitCode()]); - - if ($result->exitCode() !== 0) { - Log::error('[EmulatorUpdate] Update failed', [ - 'output' => $result->output(), - 'error' => $result->errorOutput(), - ]); - - return [ - 'success' => false, - 'error' => 'Update mislukt: ' . substr($result->output(), 0, 300), - ]; - } - - setting('emulator_version', $version); - - // Store the commit date as installed date - $commitDate = $check['commit_date'] ?? time(); - $this->settings->set('emulator_jar_installed_date', (string) $commitDate); - $this->settings->set('emulator_jar_commit', $check['commit'] ?? null); - - // Also track source commit info for update detection - $sourceSha = $check['source_info']['latest_sha'] ?? $check['commit'] ?? null; - $sourceDate = $check['source_info']['latest_timestamp'] ?? $commitDate ?? null; - if ($sourceSha) { - $this->settings->set('emulator_source_commit', $sourceSha); - } - if ($sourceDate) { - $this->settings->set('emulator_source_date', (string) $sourceDate); - } - - // Track which branch the update came from - $currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; - $this->settings->set('emulator_installed_branch', $currentBranch); - - Log::info('[EmulatorUpdate] Update successful'); - - return [ - 'success' => true, - 'version' => $version, - 'jar' => $jarName, - 'message' => "✅ Emulator geüpdatet naar v{$version}!\n📦 {$jarName}\n🔄 Service herstart", - ]; - } catch (\Exception $e) { - Log::error('[EmulatorUpdate] Exception', ['error' => $e->getMessage()]); - - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - } - } - - private function validateDirectUrl(string $url): ?array - { - if ($url === '' || $url === '0' || ! str_ends_with(strtolower($url), '.jar')) { - return null; - } - - try { - $jarName = basename(parse_url($url, PHP_URL_PATH)); - $version = $this->extractVersionFromFilename($jarName); - $storedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0'); - - // Try to get commit info from GitHub API for GitHub raw URLs - $lastModified = null; - $commitSha = null; - $gitHubInfoAvailable = false; - - if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)/', $url, $matches)) { - $owner = $matches[1]; - $repo = $matches[2]; - $branch = $matches[3]; - $path = $matches[4]; - - try { - $apiResponse = Http::timeout(10) - ->withHeaders([ - 'Accept' => 'application/vnd.github.v3+json', - 'User-Agent' => 'AtomCMS-Emulator-Updater', - ]) - ->get("https://api.github.com/repos/{$owner}/{$repo}/contents/{$path}", [ - 'ref' => $branch, - ]); - - if ($apiResponse->successful()) { - $data = $apiResponse->json(); - - // Check if we got valid data (not a rate limit error) - if (isset($data['sha']) && ! isset($data['message'])) { - $gitHubInfoAvailable = true; - $commitSha = $data['sha']; - - // Try to get commit date - if (isset($data['commit']['committer']['date'])) { - $lastModified = strtotime($data['commit']['committer']['date']); - } - } - } - } catch (\Exception) { - Log::debug('[EmulatorUpdate] Could not fetch GitHub commit info for direct URL'); - } - } - - // Fallback: try HEAD request - if ($lastModified === null) { - $response = Http::timeout(10)->head($url); - if ($response->successful()) { - $modifiedSince = $response->header('Last-Modified'); - if ($modifiedSince) { - $lastModified = strtotime($modifiedSince); - // Verify strtotime succeeded - if ($lastModified === false) { - $lastModified = null; - } - } - } - } - - // Check if we have stored info and if this is actually newer - $isUpdate = false; - $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); - - $storedData = $this->settings->getOrDefault('emulator_direct_url_info_' . md5($url)); - if ($storedData !== null && is_string($storedData)) { - $storedDataArray = json_decode($storedData, true); - if (is_array($storedDataArray)) { - // Compare by SHA (most reliable when GitHub API available) - if ($gitHubInfoAvailable && $commitSha !== null && isset($storedDataArray['commit_sha'])) { - $isUpdate = $commitSha !== $storedDataArray['commit_sha']; - } - // Fallback to date comparison - elseif ($lastModified !== null && $installedDate !== null) { - $isUpdate = (int) $installedDate < $lastModified; - } elseif ($lastModified !== null && isset($storedDataArray['last_modified'])) { - $isUpdate = $lastModified > $storedDataArray['last_modified']; - } - // Fallback to version comparison from filename - elseif (! in_array($version, ['', '0', $storedVersion], true)) { - $isUpdate = version_compare($version, $storedVersion) > 0; - } - } - } else { - // First time check - $isUpdate = true; - } - - // Always update our stored info - $infoToStore = [ - 'last_checked' => time(), - 'version' => $version, - ]; - if ($lastModified) { - $infoToStore['last_modified'] = $lastModified; - } - if ($commitSha) { - $infoToStore['commit_sha'] = $commitSha; - } - $this->settings->set('emulator_direct_url_info_' . md5($url), json_encode($infoToStore)); - - Log::info('[EmulatorUpdate] Direct URL check: jar=' . $jarName . ', version=' . $version . ', stored=' . $storedVersion . ', sha=' . ($commitSha ?? 'N/A') . ', github=' . ($gitHubInfoAvailable ? 'yes' : 'no') . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); - - return [ - 'name' => $jarName, - 'version' => $version, - 'last_modified' => $lastModified, - 'commit_sha' => $commitSha, - 'is_update' => $isUpdate, - 'gitHub_rate_limited' => ! $gitHubInfoAvailable && preg_match('/github\.com/', $url), - ]; - } catch (\Exception $e) { - Log::warning('[EmulatorUpdate] Direct URL not reachable', ['error' => $e->getMessage()]); - } - - return null; - } - - private function findLatestJar(): ?array - { - if (! $this->githubRepo) { - return null; - } - - $branch = $this->githubBranch ?: 'main'; - - $commonNames = ['arcturus.jar', 'Arcturus.jar', 'emulator.jar', 'habbo.jar', 'hotel.jar']; - - foreach ($commonNames as $name) { - try { - // First get the file info for download URL - $apiUrl = "https://api.github.com/repos/{$this->githubRepo}/contents/Latest_Compiled_Version/{$name}?ref={$branch}"; - $response = Http::timeout(10) - ->withHeaders([ - 'Accept' => 'application/vnd.github.v3+json', - 'User-Agent' => 'AtomCMS-Emulator-Updater', - ]) - ->get($apiUrl); - - if (! $response->successful()) { - continue; - } - - $data = json_decode($response->body(), true); - - if (! isset($data['sha']) || ! isset($data['download_url'])) { - continue; - } - - // Get the commit date by fetching the last commit that touched this file - $commitDate = null; - $commitSha = $data['sha']; - - try { - $commitsResponse = Http::timeout(10) - ->withHeaders([ - 'Accept' => 'application/vnd.github.v3+json', - 'User-Agent' => 'AtomCMS-Emulator-Updater', - ]) - ->get("https://api.github.com/repos/{$this->githubRepo}/commits", [ - 'path' => "Latest_Compiled_Version/{$name}", - 'sha' => $branch, - 'per_page' => 1, - ]); - - if ($commitsResponse->successful()) { - $commits = $commitsResponse->json(); - if (! empty($commits) && isset($commits[0]['commit']['committer']['date'])) { - $commitDate = strtotime($commits[0]['commit']['committer']['date']); - $commitSha = $commits[0]['sha'] ?? $data['sha']; - } - } - } catch (\Exception $e) { - Log::debug('[EmulatorUpdate] Could not fetch commit date for ' . $name); - } - - // Get the installed date (set when actually installed) - $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); - - // Also get the last checked date (for debugging) - $lastCheckedDate = $this->settings->getOrDefault("emulator_jar_{$name}_date", null); - - // Update the last checked date - if ($commitDate) { - $this->settings->set("emulator_jar_{$name}_date", (string) $commitDate); - } - - // Extract version from filename or use commit date - $version = $this->extractVersionFromFilename($name); - if ($version === '' || $version === '0') { - $version = $commitDate ? date('Y.m.d', $commitDate) : date('Y.m.d'); - } - - // Get the installed JAR commit SHA for comparison - $installedJarCommit = $this->settings->getOrDefault('emulator_jar_commit', null); - - // Check if this is actually an update - $isUpdate = false; - $installedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0'); - - // First, try to compare by SHA (most reliable) - if ($installedJarCommit !== null && $commitSha !== null) { - $isUpdate = $installedJarCommit !== $commitSha; - } - // If SHA comparison isn't possible, fall back to date comparison - elseif ($installedDate !== null && $commitDate) { - // Compare installed date with latest commit date - $isUpdate = (int) $installedDate < $commitDate; - } elseif ($installedDate === null && $commitDate) { - // Never installed - this is a first install - $isUpdate = true; - } - - Log::info('[EmulatorUpdate] Checking update for ' . $name . ': installed=' . ($installedDate ?? 'null') . ', latest=' . ($commitDate ?? 'null') . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); - - return [ - 'name' => $name, - 'url' => $data['download_url'], - 'version' => $version, - 'commit' => $commitSha, - 'commit_date' => $commitDate, - 'is_update' => $isUpdate, - 'installed_date' => $installedDate, - ]; - } catch (\Exception $e) { - Log::warning('[EmulatorUpdate] Error checking JAR file {$name}', ['error' => $e->getMessage()]); - } - } - - return null; - } - - public function checkForSourceUpdates(): ?array - { - $repo = $this->sourceRepo ?: $this->githubRepo; - $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; - - if (! $repo) { - return null; - } - - // Try local git check first if source is already cloned - $localCheck = $this->checkLocalSourceUpdates(); - if ($localCheck !== null) { - return $localCheck; - } - - // Fallback to GitHub API - try { - $response = Http::timeout(10) - ->withHeaders([ - 'Accept' => 'application/vnd.github.v3+json', - 'User-Agent' => 'AtomCMS-Emulator-Updater', - ]) - ->get("https://api.github.com/repos/{$repo}/commits", [ - 'sha' => $branch, - 'per_page' => 1, - ]); - - if (! $response->successful()) { - return null; - } - - $commits = $response->json(); - - if (empty($commits) || ! isset($commits[0]['sha'])) { - return null; - } - - $latestCommit = $commits[0]; - $latestSha = $latestCommit['sha']; - $latestDate = $latestCommit['commit']['committer']['date'] ?? null; - $latestTimestamp = $latestDate ? strtotime((string) $latestDate) : time(); - - $this->persistSourceCommitInfo($latestSha, $latestTimestamp); - - $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); - $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); - $storedDate = $this->settings->getOrDefault('emulator_source_date', null); - - // Compare SHAs first - if they're the same, no update - if ($storedSha !== null && $storedSha === $latestSha) { - $isUpdate = false; - } elseif ($storedSha !== null && $storedSha !== $latestSha) { - $isUpdate = true; - } elseif ($installedDate !== null) { - // Fallback to date comparison - $installedTimestamp = is_numeric($installedDate) ? (int) $installedDate : strtotime((string) $installedDate); - $isUpdate = $installedTimestamp < $latestTimestamp; - } else { - // Never installed - this is a first install - $isUpdate = false; - } - - Log::info('[EmulatorUpdate] Source update check (GitHub API): repo=' . $repo . ', installed=' . ($installedDate ?? 'null') . ', latest=' . $latestTimestamp . ' (' . $latestDate . '), isUpdate=' . ($isUpdate ? 'true' : 'false')); - - return [ - 'has_update' => $isUpdate, - 'latest_sha' => $latestSha, - 'latest_date' => $latestDate, - 'latest_timestamp' => $latestTimestamp, - 'latest_message' => $latestCommit['commit']['message'] ?? '', - 'latest_author' => $latestCommit['commit']['author']['name'] ?? '', - 'stored_sha' => $storedSha, - 'stored_date' => $storedDate, - ]; - } catch (\Exception $e) { - Log::warning('[EmulatorUpdate] Could not check source updates via GitHub API', ['error' => $e->getMessage()]); - - return null; - } - } - - private function checkLocalSourceUpdates(): ?array - { - $sourcePath = $this->emulatorSourcePath; - - // Use command line to check if directory exists (bypasses open_basedir) - $existsCheck = Process::timeout(5)->run("[ -d {$sourcePath} ] && echo 'exists'"); - if (! $existsCheck->successful() || trim($existsCheck->output()) !== 'exists') { - // Try to clone the repo first - return $this->checkSourceByCloning(); - } - - try { - // Check if it's a git repo - $gitCheck = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse --is-inside-work-tree 2>/dev/null"); - if (! $gitCheck->successful()) { - // Not a git repo - try to clone fresh - Log::info('[EmulatorUpdate] Source path exists but not a git repo, will clone fresh'); - - return $this->checkSourceByCloning(); - } - - // Get current commit - $currentCommitResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse HEAD"); - if (! $currentCommitResult->successful()) { - return null; - } - $currentSha = trim($currentCommitResult->output()); - - // Get latest commit from remote - $fetchResult = Process::timeout(30)->run("cd {$sourcePath} && git fetch origin 2>&1"); - if ($fetchResult->failed()) { - Log::debug('[EmulatorUpdate] Git fetch failed, trying local check only'); - } - - // Get latest commit on current branch - $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; - $latestResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse origin/{$branch} 2>/dev/null || git rev-parse HEAD"); - if (! $latestResult->successful()) { - return null; - } - $latestSha = trim($latestResult->output()); - - // Get commit date - $dateResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%ci' {$latestSha}"); - $latestDate = null; - $latestTimestamp = time(); - if ($dateResult->successful()) { - $latestDate = trim($dateResult->output(), "'"); - if ($latestDate !== '' && $latestDate !== '0') { - $latestTimestamp = strtotime($latestDate); - } - } - - $this->persistSourceCommitInfo($latestSha, $latestTimestamp); - - $msgResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%s' {$latestSha}"); - $latestMessage = ''; - if ($msgResult->successful()) { - $latestMessage = trim($msgResult->output()); - } - - // Get author - $authorResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%an' {$latestSha}"); - $latestAuthor = ''; - if ($authorResult->successful()) { - $latestAuthor = trim($authorResult->output()); - } - - $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); - $storedDate = $this->settings->getOrDefault('emulator_source_date', null); - $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); - - // Check if there's an update - compare stored SHA with latest SHA - $isUpdate = false; - if ($storedSha !== null && $storedSha !== $latestSha) { - $isUpdate = true; - } elseif ($storedSha === null) { - // First time - consider it an update - $isUpdate = true; - } - - Log::info('[EmulatorUpdate] Source update check (local git): stored=' . $storedSha . ', latest=' . $latestSha . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); - - return [ - 'has_update' => $isUpdate, - 'latest_sha' => $latestSha, - 'latest_date' => $latestDate, - 'latest_timestamp' => $latestTimestamp, - 'latest_message' => $latestMessage, - 'latest_author' => $latestAuthor, - 'stored_sha' => $storedSha, - 'stored_date' => $storedDate, - 'source' => 'local', - ]; - } catch (\Exception $e) { - Log::debug('[EmulatorUpdate] Local source check failed: ' . $e->getMessage()); - - return null; - } - } - - private function checkSourceByCloning(): ?array - { - $repo = $this->sourceRepo ?: $this->githubRepo; - $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; - - if (! $repo) { - Log::debug('[EmulatorUpdate] No repo configured for source check'); - - return null; - } - - // Clean up repo URL if it contains tree/branch info - $repo = preg_replace('#/tree/[^/]+/.*$#', '', $repo); - $repo = str_replace(['https://github.com/', 'http://github.com/'], '', $repo); - $repo = rtrim($repo, '/'); - - Log::info('[EmulatorUpdate] Checking source updates by cloning', ['repo' => $repo, 'branch' => $branch]); - - try { - $tempDir = '/tmp/emulator-source-check-' . Str::random(8); - - // Clone with depth 1 - $cloneResult = Process::timeout(120)->run( - "git clone --branch {$branch} --depth 1 https://github.com/{$repo}.git {$tempDir} 2>&1", - ); - - if ($cloneResult->failed()) { - Log::debug('[EmulatorUpdate] Could not clone source for checking: ' . $cloneResult->errorOutput()); - - return null; - } - - // Get latest commit info - $shaResult = Process::timeout(5)->run("cd {$tempDir} && git rev-parse HEAD"); - $latestSha = $shaResult->successful() ? trim($shaResult->output()) : ''; - - $dateResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%ci'"); - $latestDate = null; - $latestTimestamp = time(); - if ($dateResult->successful()) { - $latestDate = trim($dateResult->output()); - if ($latestDate !== '' && $latestDate !== '0') { - $latestTimestamp = strtotime($latestDate); - } - } - - $this->persistSourceCommitInfo($latestSha, $latestTimestamp); - - $msgResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%s'"); - $latestMessage = $msgResult->successful() ? trim($msgResult->output()) : ''; - - $authorResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%an'"); - $latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : ''; - - // Cleanup - Process::timeout(10)->run("rm -rf {$tempDir}"); - - $storedSha = $this->settings->getOrDefault('emulator_source_commit', null); - $storedDate = $this->settings->getOrDefault('emulator_source_date', null); - $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); - - // Check if there's an update - compare stored SHA with latest SHA - $isUpdate = false; - if ($storedSha !== null && $storedSha !== $latestSha) { - $isUpdate = true; - } elseif ($storedSha === null) { - // First time - consider it an update - $isUpdate = true; - } - - Log::info('[EmulatorUpdate] Source update check (cloned): stored=' . $storedSha . ', latest=' . $latestSha . ', isUpdate=' . ($isUpdate ? 'true' : 'false')); - - return [ - 'has_update' => $isUpdate, - 'latest_sha' => $latestSha, - 'latest_date' => $latestDate, - 'latest_timestamp' => $latestTimestamp, - 'latest_message' => $latestMessage, - 'latest_author' => $latestAuthor, - 'stored_sha' => $storedSha, - 'stored_date' => $storedDate, - 'source' => 'cloned', - ]; - } catch (\Exception $e) { - Log::debug('[EmulatorUpdate] Source clone check failed: ' . $e->getMessage()); - - return null; - } + return $this->jarService->performUpdate($check); } public function buildFromSource(bool $force = false): array { - $repo = $this->sourceRepo ?: $this->githubRepo; - $branch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; - - if (! $repo) { - return ['success' => false, 'error' => 'Geen source repo geconfigureerd']; - } - - $sourcePath = $this->emulatorSourcePath; - $serviceName = $this->emulatorService; - - Log::info('[EmulatorUpdate] Starting source build', [ - 'repo' => $repo, - 'branch' => $branch, - 'path' => $sourcePath, - ]); - - $this->ensureJavaInstalled(); - - // Pre-flight: Ensure directories exist and fix permissions - Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $sourcePath))); - Process::timeout(10)->run('mkdir -p ' . escapeshellarg((string) $this->jarPath)); - Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath))); - - // Auto-retry logic - $maxRetries = 2; - $lastError = ''; - - for ($retry = 0; $retry <= $maxRetries; $retry++) { - // Always fix permissions before any operation (handles root-owned files from previous builds) - Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)) . ' 2>/dev/null || true'); - Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || true'); - - if ($retry > 0) { - Log::info('[EmulatorUpdate] Retry attempt', ['attempt' => $retry]); - // Force re-clone on retry - Process::timeout(30)->run('rm -rf ' . escapeshellarg((string) $sourcePath)); - } - - try { - // Use command line to check if directory exists and is a git repo (bypasses open_basedir) - $existsCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . " ] && echo 'exists'"); - $sourceExists = $existsCheck->successful() && trim($existsCheck->output()) === 'exists'; - - // Also check if it's a valid git repository - $gitCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . "/.git ] && echo 'git'"); - $isGitRepo = $gitCheck->successful() && trim($gitCheck->output()) === 'git'; - - if ($sourceExists && $isGitRepo) { - Log::info('[EmulatorUpdate] Pulling latest changes'); - $commands = [ - 'cd ' . escapeshellarg((string) $sourcePath) . ' && git fetch origin', - 'cd ' . escapeshellarg((string) $sourcePath) . ' && git checkout ' . escapeshellarg($branch), - 'cd ' . escapeshellarg((string) $sourcePath) . ' && git pull origin ' . escapeshellarg($branch), - ]; - } else { - Log::info('[EmulatorUpdate] Cloning repository'); - $commands = [ - 'sudo rm -rf ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || rm -rf ' . escapeshellarg((string) $sourcePath), - 'mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)), - 'git clone --branch ' . escapeshellarg($branch) . ' --depth 1 https://github.com/' . escapeshellarg($repo) . '.git ' . escapeshellarg((string) $sourcePath), - 'chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath), - ]; - } - - $command = implode(' && ', $commands); - $result = Process::timeout(300)->run($command); - - if ($result->failed()) { - $lastError = 'Git clone/pull failed: ' . substr($result->errorOutput(), 0, 300); - Log::warning('[EmulatorUpdate] Git operation failed', ['error' => $lastError, 'attempt' => $retry]); - - continue; // Retry - } - - $buildCommands = $this->getBuildCommands($sourcePath); - - Log::info('[EmulatorUpdate] Running build', ['command' => $buildCommands]); - $buildResult = Process::timeout(600)->run('cd ' . escapeshellarg((string) $sourcePath) . ' && ' . $this->getBuildCommands($sourcePath)); - - $hasSignalError = str_contains($buildResult->errorOutput(), 'signal') || str_contains($buildResult->output(), 'signal'); - - // Check for signal errors - still try to find JAR - if ($hasSignalError) { - Log::warning('[EmulatorUpdate] Build process received signal, checking if JAR was built anyway'); - } - - // Even if build "failed", check if JAR exists (signal errors may be misleading) - $jarPath = $this->findBuiltJar($sourcePath); - - if ($jarPath) { - Log::info('[EmulatorUpdate] JAR found despite build status', ['jar' => $jarPath]); - } elseif ($buildResult->failed() && ! $hasSignalError) { - $lastError = 'Build failed: ' . substr($buildResult->errorOutput(), 0, 500); - Log::warning('[EmulatorUpdate] Build failed', ['error' => $lastError, 'attempt' => $retry]); - - // Try to clean and rebuild - if ($retry < $maxRetries) { - $cleanCommands = [ - 'cd ' . escapeshellarg((string) $sourcePath) . ' && mvn clean 2>/dev/null || ./gradlew clean 2>/dev/null || true', - ]; - Process::timeout(60)->run(implode(' && ', $cleanCommands)); - - continue; // Retry - } - - continue; - } - - if (! $jarPath) { - $lastError = 'Build succeeded but JAR not found. Check build output.'; - Log::warning('[EmulatorUpdate] JAR not found', ['attempt' => $retry]); - - continue; - } - - $jarName = basename($jarPath); - $version = $this->extractVersionFromFilename($jarName); - if ($version === '' || $version === '0') { - $version = date('Y.m.d'); - } - - $deployCommands = [ - 'mkdir -p ' . escapeshellarg((string) $this->jarPath), - 'chown -R www-data:www-data ' . escapeshellarg((string) $this->jarPath), - 'if ls ' . escapeshellarg((string) $this->jarPath) . '/*.jar 1>/dev/null 2>&1; then mkdir -p ' . escapeshellarg((string) $this->jarPath) . '/backup && mv ' . escapeshellarg((string) $this->jarPath) . '/*.jar ' . escapeshellarg((string) $this->jarPath) . '/backup/; fi', - 'cp -f ' . escapeshellarg($jarPath) . ' ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName), - 'chown www-data:www-data ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName), - 'chmod 755 ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName), - 'ls -la ' . escapeshellarg((string) $this->jarPath) . '/', - '# Restart service (try all methods) - service_restarted=false - for method in "systemctl restart ' . escapeshellarg((string) $serviceName) . '" "service ' . escapeshellarg((string) $serviceName) . ' restart" "/etc/init.d/' . escapeshellarg((string) $serviceName) . ' restart" "kill -HUP \$(pgrep -f ' . escapeshellarg((string) $serviceName) . ')" ; do - echo "Trying: $method" - eval "$method" 2>/dev/null && { service_restarted=true; break; } || true - done - if [ "$service_restarted" = false ]; then - echo "WARNING: Service restart may have failed (check manually)" >&2 - exit 1 - fi', - ]; - - $deployCommand = implode(' && ', $deployCommands); - - Log::info('[EmulatorUpdate] Deploying jar', [ - 'source' => $jarPath, - 'destination' => "{$this->jarPath}/{$jarName}", - ]); - - $deployResult = Process::timeout(60)->run($deployCommand); - - Log::info('[EmulatorUpdate] Deploy result', [ - 'output' => $deployResult->output(), - 'error' => $deployResult->errorOutput(), - 'successful' => $deployResult->successful(), - ]); - - if (! $deployResult->successful()) { - $lastError = 'Deploy failed: ' . substr($deployResult->errorOutput(), 0, 300); - Log::warning('[EmulatorUpdate] Deploy failed', ['error' => $lastError, 'attempt' => $retry]); - - if ($retry < $maxRetries) { - continue; // Retry - } - - // All retries exhausted - return [ - 'success' => false, - 'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError, - ]; - } - - $sourceInfo = $this->checkForSourceUpdates(); - $installedDate = $sourceInfo['latest_timestamp'] ?? time(); - - $this->settings->set('emulator_version', $version); - $this->settings->set('emulator_jar_installed_date', (string) $installedDate); - $this->settings->set('emulator_source_commit', $sourceInfo['latest_sha'] ?? 'unknown'); - $this->settings->set('emulator_source_date', (string) $installedDate); - - $currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main'; - $this->settings->set('emulator_installed_branch', $currentBranch); - - $this->runSqlUpdates(); - - Log::info('[EmulatorUpdate] Source build successful'); - - return [ - 'success' => true, - 'version' => $version, - 'jar' => $jarName, - 'built' => true, - 'message' => "✅ Emulator vanaf source gebouwd!\n📦 {$jarName}\n🔄 Service herstart", - ]; - - } catch (\Exception $e) { - $lastError = $e->getMessage(); - Log::error('[EmulatorUpdate] Source build exception', ['error' => $lastError, 'attempt' => $retry]); - } - } - - // All retries exhausted - return [ - 'success' => false, - 'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError, - ]; + return $this->buildService->buildFromSource($force); } - private function getBuildCommands(string $sourcePath): string + public function restartEmulator(): bool { - // Check for pom.xml in both sourcePath and sourcePath/Emulator - $pomPath = $sourcePath; - $pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'"); + $serviceName = $this->settings->getOrDefault('emulator_service_name', 'emulator'); - if (! $pomCheck->successful() || trim($pomCheck->output()) !== 'pom') { - $pomPath = $sourcePath . '/Emulator'; - $pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'"); - } - - $gradlewPath = $sourcePath . '/gradlew'; - $gradlewCheck = Process::timeout(5)->run("[ -f {$gradlewPath} ] && echo 'gradlew'"); - - $gradlePath = $sourcePath . '/build.gradle'; - $gradleCheck = Process::timeout(5)->run("[ -f {$gradlePath} ] && echo 'gradle'"); - - if ($pomCheck->successful() && trim($pomCheck->output()) === 'pom') { - Log::info('[EmulatorUpdate] Building with Maven (pom.xml found in ' . $pomPath . ')'); - - return "cd {$pomPath} && mvn clean package -DskipTests 2>&1"; - } - - if ($gradlewCheck->successful() && trim($gradlewCheck->output()) === 'gradlew') { - Log::info('[EmulatorUpdate] Building with Gradle (gradlew found)'); - - return "cd {$sourcePath} && chmod +x gradlew && ./gradlew clean build -x test 2>&1"; - } - - if ($gradleCheck->successful() && trim($gradleCheck->output()) === 'gradle') { - Log::info('[EmulatorUpdate] Building with Gradle (build.gradle found)'); - - return "cd {$sourcePath} && gradle clean build -x test 2>&1"; - } - - Log::warning('[EmulatorUpdate] No build system found in source directory'); - - return "cd {$sourcePath} && ls -la"; - } - - private function findBuiltJar(string $sourcePath): ?string - { - $patterns = [ - $sourcePath . '/target/*.jar', - $sourcePath . '/build/libs/*.jar', - $sourcePath . '/*/*.jar', - $sourcePath . '/*.jar', - // Also check in Emulator subdirectory - $sourcePath . '/Emulator/target/*.jar', - $sourcePath . '/Emulator/build/libs/*.jar', - $sourcePath . '/Emulator/*/*.jar', - ]; - - foreach ($patterns as $pattern) { - $result = Process::timeout(10)->run('ls -t ' . $pattern . ' 2>/dev/null | head -1'); - if ($result->successful()) { - $jarPath = trim($result->output()); - if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) { - return $jarPath; - } - } - } - - return null; - } - - public function isSourceBuildAvailable(): bool - { - $hasRepo = ! in_array($this->sourceRepo, [null, '', '0'], true) || ! in_array($this->githubRepo, [null, '', '0'], true); - $hasPath = ! in_array($this->emulatorSourcePath, [null, '', '0'], true); - - if (! $hasRepo || ! $hasPath) { - return false; - } - - // Check via command line to bypass open_basedir restriction try { - $result = Process::timeout(5)->run("[ -d {$this->emulatorSourcePath} ] && echo 'exists' || echo 'not_exists'"); + Log::info('[EmulatorUpdate] Restarting emulator service: ' . $serviceName); + + $result = Process::timeout(30)->run("systemctl restart {$serviceName} 2>&1"); + if ($result->successful()) { + return true; + } + + $result = Process::timeout(30)->run("service {$serviceName} restart 2>&1"); + + return $result->successful(); + } catch (\Exception $e) { + Log::error('[EmulatorUpdate] Failed to restart emulator', ['error' => $e->getMessage()]); - return trim($result->output()) === 'exists'; - } catch (\Exception) { return false; } } - private function extractVersionFromFilename(string $filename): string + public function getBackupList(): array { - $filename = basename($filename, '.jar'); - - if (preg_match('/[\d]+\.[\d]+\.[\d]+/', $filename, $matches)) { - return $matches[0]; - } - - if (preg_match('/v?([\d]+)/', $filename, $matches)) { - return $matches[1]; - } - - return ''; + return $this->backupService->getList(); } - private function parseGitHubUrl(string $url): void + public function restoreBackup(string $backupName): array { - $parsed = $this->parseGithubRepoUrl($url); - $this->githubRepo = $parsed['repo']; - $this->githubBranch = $parsed['branch']; - } - - private function parseSourceRepo(string $url): void - { - $parsed = $this->parseGithubRepoUrl($url); - $this->sourceRepo = $parsed['repo']; - $this->sourceBranch = $parsed['branch']; - } - - private function parseGithubRepoUrl(string $url): array - { - if ($url === '' || $url === '0') { - return ['repo' => null, 'branch' => 'main']; - } - - if (preg_match('/github\.com\/([^\/]+)\/([^\/\?#]+)/', $url, $matches)) { - $repo = $matches[1] . '/' . $matches[2]; - $branch = 'main'; - - if (preg_match('/\/tree\/([^\/]+)/', $url, $branchMatch)) { - $branch = $branchMatch[1]; - } - - return ['repo' => $repo, 'branch' => $branch]; - } - - return ['repo' => null, 'branch' => 'main']; + return $this->backupService->restore($backupName); } public function getInstalledVersion(): string { - return setting('emulator_version', 'Onbekend'); + return $this->statusService->getInstalledVersion(); } public function getInstalledJar(): ?string { - try { - $result = Process::timeout(5)->run("ls -1 {$this->jarPath}/*.jar 2>/dev/null | head -1"); - - if ($result->successful()) { - $jarPath = trim($result->output()); - if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) { - return basename($jarPath); - } - } - } catch (\Exception) { - } - - return setting('emulator_version', 'Onbekend') !== 'Onbekend' - ? 'Emulator v' . setting('emulator_version') - : null; + return $this->statusService->getInstalledJar(); } public function getInstalledJarInfo(): array { - $files = []; - - try { - $result = Process::timeout(5)->run("ls -1lh {$this->jarPath}/*.jar 2>/dev/null"); - if ($result->successful()) { - $lines = array_filter(explode("\n", trim($result->output()))); - foreach ($lines as $line) { - $line = trim($line); - if ($line === '' || $line === '0') { - continue; - } - $parts = preg_split('/\s+/', $line); - $filename = end($parts); - if (str_contains((string) $filename, '.jar')) { - $size = $parts[4] ?? '?'; - $files[] = [ - 'name' => basename((string) $filename), - 'size' => $size, - ]; - } - } - } - - if ($files === []) { - $result = Process::timeout(5)->run("ls {$this->jarPath}/*.jar 2>/dev/null"); - if ($result->successful()) { - $lines = array_filter(explode("\n", trim($result->output()))); - foreach ($lines as $line) { - $line = trim($line); - if (str_contains($line, '.jar')) { - $files[] = [ - 'name' => basename($line), - 'size' => '?', - ]; - } - } - } - } - } catch (\Exception) { - } - - return $files; + return $this->statusService->getInstalledJarInfo(); } public function getLastSqlUpdate(): ?string @@ -2085,86 +188,21 @@ BASH; return setting('emulator_last_sql_update'); } - public function getBackupList(): array - { - $backups = []; - - try { - $backupDir = $this->jarPath . '/backup'; - if (! is_dir($backupDir)) { - return $backups; - } - - $result = Process::timeout(5)->run("ls -1t {$backupDir}/*.jar 2>/dev/null"); - if ($result->successful()) { - $lines = array_filter(explode("\n", trim($result->output()))); - foreach ($lines as $line) { - $line = trim($line); - if ($line === '' || $line === '0' || ! str_contains($line, '.jar')) { - continue; - } - - $filename = basename($line); - $version = $this->extractVersionFromFilename($filename); - - preg_match('/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})/', $line, $dateMatches); - $date = $dateMatches[1] ?? date('Y-m-d_H-i'); - - $backups[] = [ - 'name' => $filename, - 'jar' => $filename, - 'date' => $date, - 'version' => $version, - ]; - } - } - } catch (\Exception) { - } - - return $backups; - } - - public function restoreBackup(string $backupName): array - { - try { - $backupDir = $this->jarPath . '/backup'; - $backupPath = $backupDir . '/' . $backupName; - $targetPath = $this->jarPath . '/' . $backupName; - - if (! file_exists($backupPath)) { - return ['success' => false, 'error' => 'Backup niet gevonden']; - } - - if (file_exists($targetPath)) { - unlink($targetPath); - } - - copy($backupPath, $targetPath); - chmod($targetPath, 0755); - - return ['success' => true, 'message' => 'Backup hersteld: ' . $backupName]; - } catch (\Exception $e) { - return ['success' => false, 'error' => $e->getMessage()]; - } - } - public function debugStatus(): array { return Cache::remember('emulator_debug_status', 120, function () { - $this->ensureInstalledDateIsSet(); - $installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null); $sourceCommit = $this->settings->getOrDefault('emulator_source_commit', null); $sourceDate = $this->settings->getOrDefault('emulator_source_date', null); $emulatorVersion = $this->settings->getOrDefault('emulator_version', null); - $jarFiles = $this->getInstalledJarInfo(); + $jarFiles = $this->statusService->getInstalledJarInfo(); return [ - 'github_url' => $this->githubUrl, - 'github_repo' => $this->githubRepo, - 'github_branch' => $this->githubBranch, - 'source_repo' => $this->sourceRepo, - 'source_branch' => $this->sourceBranch, + 'github_url' => $this->settings->getOrDefault('emulator_github_url', ''), + 'github_repo' => $this->settings->getOrDefault('emulator_source_repo', ''), + 'github_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'), + 'source_repo' => $this->settings->getOrDefault('emulator_source_repo', ''), + 'source_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'), 'installed_date' => $installedDate, 'installed_date_formatted' => $installedDate ? date('Y-m-d H:i:s', (int) $installedDate) : null, 'source_commit' => $sourceCommit, @@ -2172,7 +210,6 @@ BASH; 'source_date_formatted' => $sourceDate ? date('Y-m-d H:i:s', (int) $sourceDate) : null, 'emulator_version' => $emulatorVersion, 'jar_files' => $jarFiles, - 'jar_path' => $this->jarPath, 'installed_branch' => $this->settings->getOrDefault('emulator_installed_branch', null), ]; }); @@ -2180,57 +217,9 @@ BASH; public function resetInstalledDate(): void { - WebsiteSetting::where('key', 'emulator_jar_installed_date')->delete(); - WebsiteSetting::where('key', 'emulator_source_commit')->delete(); - WebsiteSetting::where('key', 'emulator_source_date')->delete(); - } - - private function commandFileExists(string $path): bool - { - $result = Process::timeout(5)->run('ls ' . $path . ' 2>/dev/null | head -1'); - - return $result->successful() && trim($result->output()) !== ''; - } - - private function commandDirExists(string $path): bool - { - $result = Process::timeout(5)->run('[ -d ' . escapeshellarg($path) . ' ] && echo "1" || echo "0"'); - - return trim($result->output()) === '1'; - } - - private function persistSourceCommitInfo(string $sha, int $timestamp): void - { - $this->settings->set('emulator_source_commit', $sha); - $this->settings->set('emulator_source_date', (string) $timestamp); - } - - private function ensureInstalledDateIsSet(): void - { - if ($this->settings->getOrDefault('emulator_jar_installed_date', null) !== null) { - return; - } - - if (! $this->commandFileExists($this->jarPath . '/*.jar')) { - return; - } - - $installedVersion = $this->settings->getOrDefault('emulator_version', null); - if (! $installedVersion) { - return; - } - - $jarInfo = $this->findLatestJar(); - if ($jarInfo && ($jarInfo['commit_date'] ?? null)) { - $this->settings->set('emulator_jar_installed_date', (string) $jarInfo['commit_date']); - - return; - } - - $sourceInfo = $this->checkForSourceUpdates(); - if ($sourceInfo && ($sourceInfo['latest_timestamp'] ?? null)) { - $this->settings->set('emulator_jar_installed_date', (string) $sourceInfo['latest_timestamp']); - } + \App\Models\Miscellaneous\WebsiteSetting::where('key', 'emulator_jar_installed_date')->delete(); + \App\Models\Miscellaneous\WebsiteSetting::where('key', 'emulator_source_commit')->delete(); + \App\Models\Miscellaneous\WebsiteSetting::where('key', 'emulator_source_date')->delete(); } public function clearAllLogs(): array @@ -2261,7 +250,6 @@ BASH; } } - // Also clear Laravel log try { $laravelLog = storage_path('logs/laravel.log'); if (is_file($laravelLog)) { @@ -2271,7 +259,6 @@ BASH; } catch (\Exception) { } - // Clear tmp files older than 1 day Process::timeout(10)->run("find /tmp -name 'emulator_*' -mtime +1 -delete 2>/dev/null || true"); Process::timeout(10)->run("find /tmp -name 'nitro_*' -mtime +1 -delete 2>/dev/null || true"); Process::timeout(10)->run("find /tmp -name 'deploy_*' -mtime +1 -delete 2>/dev/null || true"); @@ -2312,21 +299,7 @@ BASH; } } - $emulatorPath = $this->jarPath; - $pathExists = $this->commandDirExists(dirname((string) $emulatorPath)); - if (! $pathExists) { - $actions[] = 'Emulator pad bestaat niet - aanmaken...'; - Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $emulatorPath))); - Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $emulatorPath))); - $actions[] = 'Emulator pad aangemaakt'; - } - - if (! ($status['emulator_db_connected'] ?? false)) { - $actions[] = 'Emulator database niet bereikbaar - controleer connectie'; - $errors[] = 'Emulator database connectie probleem'; - } - - $sqlRepairResult = $this->repairSqlUpdates(); + $sqlRepairResult = $this->sqlService->repair(); if (! empty($sqlRepairResult['actions'])) { $actions = array_merge($actions, $sqlRepairResult['actions']); } @@ -2334,19 +307,7 @@ BASH; $errors = array_merge($errors, $sqlRepairResult['errors']); } - $this->ensureGitSafeDirectories(); - - $sudoersFile = '/etc/sudoers.d/atomcms-emulator'; - $checkResult = Process::timeout(5)->run("test -f {$sudoersFile} && echo 'exists' || echo 'missing'"); - if (trim($checkResult->output()) !== 'exists') { - $actions[] = 'Sudoers configuratie ontbreekt - toevoegen...'; - $this->ensureSudoAccess(); - $actions[] = 'Sudoers configuratie toegevoegd'; - } - if ($errors !== []) { - Log::warning('[EmulatorUpdate] Repair completed with errors', ['errors' => $errors]); - return [ 'success' => false, 'actions' => $actions, @@ -2355,14 +316,11 @@ BASH; ]; } - Log::info('[EmulatorUpdate] Repair completed successfully', ['actions' => $actions]); - return [ 'success' => true, 'actions' => $actions, 'message' => count($actions) . ' acties uitgevoerd', ]; - } catch (\Exception $e) { Log::error('[EmulatorUpdate] Repair exception', ['error' => $e->getMessage()]); @@ -2374,55 +332,6 @@ BASH; } } - public function repairSqlUpdates(): 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->checkForSqlUpdates(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->runSqlUpdates(); - - 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('[EmulatorUpdate] SQL repair failed', ['error' => $e->getMessage()]); - } - - return [ - 'success' => $errors === [], - 'actions' => $actions, - 'errors' => $errors, - ]; - } - public function diagnose(): array { $diagnosis = [ @@ -2442,11 +351,8 @@ BASH; $diagnosis['checks']['emulator_db_connected'] = $status['emulator_db_connected'] ?? false; $diagnosis['checks']['is_configured'] = $this->isConfigured(); $diagnosis['checks']['update_available'] = $status['update_available'] ?? false; - $diagnosis['checks']['jar_path'] = $this->jarPath; - $diagnosis['checks']['source_path'] = $this->emulatorSourcePath; - $diagnosis['checks']['service_name'] = $this->emulatorService; - $sqlDiagnosis = $this->diagnoseSqlUpdates(); + $sqlDiagnosis = $this->sqlService->diagnose(); $diagnosis['checks']['sql_table_exists'] = $sqlDiagnosis['table_exists'] ?? false; $diagnosis['checks']['sql_updates_applied'] = $sqlDiagnosis['applied_count'] ?? 0; $diagnosis['checks']['sql_pending'] = $sqlDiagnosis['pending_count'] ?? 0; @@ -2458,7 +364,7 @@ BASH; if (! ($status['service_running'] ?? false)) { $diagnosis['issues'][] = 'Emulator service draait niet'; - $diagnosis['recommendations'][] = 'Start de service met: sudo systemctl start ' . $this->emulatorService; + $diagnosis['recommendations'][] = 'Start de service met: sudo systemctl start ' . $this->settings->getOrDefault('emulator_service_name', 'emulator'); } if (! ($status['emulator_db_connected'] ?? false)) { @@ -2466,7 +372,7 @@ BASH; $diagnosis['recommendations'][] = 'Controleer de database credentials in de settings'; } - if (! ($status['source_exists'] ?? false) && $this->isSourceBuildAvailable()) { + if (! ($status['source_exists'] ?? false) && $this->sourceService->isSourceBuildAvailable()) { $diagnosis['issues'][] = 'Source code niet gevonden'; $diagnosis['recommendations'][] = 'Voer emulator:update --rebuild uit om vanaf source te bouwen'; } @@ -2480,41 +386,6 @@ BASH; $diagnosis['issues'][] = $sqlDiagnosis['pending_count'] . ' SQL updates pending'; $diagnosis['recommendations'][] = 'Voer reparatie uit om SQL updates toe te passen'; } - - if (! ($status['update_available'] ?? false) && $this->isConfigured()) { - $diagnosis['recommendations'][] = 'Emulator is up-to-date'; - } - - } catch (\Exception $e) { - $diagnosis['error'] = $e->getMessage(); - } - - return $diagnosis; - } - - public function diagnoseSqlUpdates(): 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->checkForSqlUpdates(false); - if (isset($sqlCheck['count'])) { - $diagnosis['pending_count'] = $sqlCheck['count']; - } - } - } catch (\Exception $e) { $diagnosis['error'] = $e->getMessage(); }