#!/usr/bin/env php '0;31', 'green' => '0;32', 'yellow' => '1;33', 'cyan' => '0;36', 'white' => '1;37', ]; $c = $map[$code] ?? '0'; return "\033[{$c}m{$text}\033[0m"; } function info(string $msg): void { println(color('[INFO]', 'cyan') . ' ' . $msg); } function ok(string $msg): void { println(color('[OK]', 'green') . ' ' . $msg); } function warn(string $msg): void { println(color('[WARN]', 'yellow') . ' ' . $msg); } function error(string $msg): void { println(color('[ERR]', 'red') . ' ' . $msg); } function fail(string $msg): never { error($msg); exit(1); } function ask(string $prompt, ?string $default = null): string { $defaultStr = $default !== null ? " [{$default}]" : ''; echo color('?', 'cyan') . " {$prompt}{$defaultStr}: "; $val = trim(fgets(STDIN) ?: ''); return $val !== '' ? $val : ($default ?? ''); } function askRequired(string $prompt): string { while (true) { $val = ask($prompt); if ($val !== '') break; warn('Valore obbligatorio.'); } return $val; } function confirm(string $prompt): bool { $resp = strtolower(ask($prompt . ' [S/n]', 'S')); return $resp === 's' || $resp === ''; } function menu(string $prompt, array $options): string { foreach ($options as $i => $opt) { println(' ' . ($i + 1) . ') ' . $opt); } while (true) { $val = ask($prompt, '1'); if (isset($options[(int)$val - 1])) return $options[(int)$val - 1]; warn('Scelta non valida.'); } } function checkCmd(string $cmd): bool { $found = !(trim(shell_exec("command -v $cmd 2>/dev/null") ?: '') === ''); if (!$found) warn("Comando '$cmd' non trovato."); return $found; } function run(string $cmd, ?string $label = null): bool { if ($label) info($label); println(" \$ {$cmd}"); passthru($cmd, $exitCode); if ($exitCode !== 0) warn("Comando terminato con codice {$exitCode}"); return $exitCode === 0; } function runCapture(string $cmd): string { return trim(shell_exec($cmd) ?: ''); } /** * Crea l'utente amministratore via PDO prepared statement. * Nessun escaping PHP necessario (tutto via prepared statement). */ function createAdminUser( string $dbHost, string $dbPort, string $dbName, string $dbUser, string $dbPass, string $adminName, string $adminEmail, string $adminPass ): bool { try { $pdo = new PDO( "mysql:host={$dbHost};port={$dbPort};dbname={$dbName}", $dbUser, $dbPass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); $hash = password_hash($adminPass, PASSWORD_BCRYPT); $permissions = json_encode([ 'individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2, ]); // Elimina eventuale utente con stessa email (seeder) $stmt = $pdo->prepare('DELETE FROM users WHERE email = ?'); $stmt->execute([$adminEmail]); // Inserisce admin con is_admin=1, status='active', permissions completi $stmt = $pdo->prepare( "INSERT INTO users (name, email, password, is_admin, status, permissions, role_preset_id, created_at, updated_at) VALUES (?, ?, ?, 1, 'active', ?, 1, NOW(), NOW())" ); $stmt->execute([$adminName, $adminEmail, $hash, $permissions]); $userId = $pdo->lastInsertId(); // Assegna ruolo Spatie 'admin' (role_id = 1) $stmt = $pdo->prepare( "INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)" ); $stmt->execute(['App\\Models\\User', $userId]); return true; } catch (\PDOException $e) { error("Errore creazione admin: " . $e->getMessage()); return false; } } // ── Banner ─────────────────────────────────────────── println(color(' ____ _ _ _', 'white')); println(color(' / ___| | __ _ ___| |_ ___ _ __ ___ | |_ ___', 'white')); println(color('| | _| |/ _` / __| __/ _ \'__/ _ \ | __/ _ \\', 'white')); println(color('| |_| | | (_| \__ \ || __/ | | __/ | || __/', 'white')); println(color(' \____|_|\__,_|___/\__\___|_| \___| \__\___|', 'white')); println(color(' Installer interattivo per sysadmin', 'cyan')); println(); // ── Preflight ──────────────────────────────────────── $scriptDir = dirname(__FILE__); chdir($scriptDir); if (!file_exists('.env.example')) { fail("File .env.example non trovato in {$scriptDir}. Sei nella cartella dell'applicazione?"); } // ── Select mode ────────────────────────────────────── println('Seleziona la modalità di installazione:'); $mode = menu('Scegli', [ 'Fresh Install su Apache (LAMP tradizionale)', 'Fresh Install via Docker', 'Restore da Backup (Apache)', ]); // ── Common params ──────────────────────────────────── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' PARAMETRI GENERALI', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); $appName = askRequired('Nome del sito/app (es. Glastree MyChurch)'); $adminName = askRequired('Nome del primo utente amministratore'); $adminEmail = askRequired('Email dell\'amministratore'); $adminPass = askRequired('Password per l\'amministratore'); $appUrl = askRequired('URL pubblico del sito (es. https://glastree.esempio.it)'); // ── DB params (Apache / Restore) ───────────────────── $dbType = 'mysql'; $dbHost = '127.0.0.1'; $dbPort = '3306'; $dbName = 'glastree'; $dbUser = 'glastree'; $dbPass = ''; if (in_array($mode, ['Fresh Install su Apache (LAMP tradizionale)', 'Restore da Backup (Apache)'])) { println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' DATABASE', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); $dbType = menu('Tipo database', ['mysql', 'sqlite']); if ($dbType === 'mysql') { $dbHost = askRequired('Host database'); $dbPort = ask('Porta database', '3306'); $dbName = ask('Nome database (verrà creato se non esiste)', 'glastree'); $dbUser = askRequired('Utente database'); $dbPass = askRequired('Password database'); } } // ── Web server user detection ───────────────────────── $wwwUser = 'www-data'; $sudo = ''; if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { $detected = trim(shell_exec('ps aux | grep -E "apache|httpd" | grep -v grep | head -1 | awk \'{print $1}\'') ?: ''); if ($detected !== '' && $detected !== 'root') { $wwwUser = $detected; } if (function_exists('posix_getuid') && posix_getuid() === 0) { $sudo = "sudo -u {$wwwUser} "; } } // ══════════════════════════════════════════════════════ // MODE 1: FRESH INSTALL APACHE // ══════════════════════════════════════════════════════ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') { println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' PRE-FLIGHT CHECKS', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); // PHP version $phpVer = PHP_VERSION; info("PHP versione: {$phpVer}"); if (version_compare($phpVer, MIN_PHP, '<')) { fail("PHP >= " . MIN_PHP . " richiesto (trovato {$phpVer})"); } ok("PHP {$phpVer}"); // Extensions foreach (REQUIREMENTS as $ext) { if (!extension_loaded($ext)) { warn("Estensione PHP '{$ext}' non trovata (alcune funzionalità potrebbero non funzionare)"); } else { ok("Estensione PHP '{$ext}'"); } } // Composer checkCmd('composer'); checkCmd('node'); checkCmd('npm'); // ── Step 1: Environment ───────────────────────── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' 1/7 — CONFIGURAZIONE AMBIENTE', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); if (!file_exists('.env')) { copy('.env.example', '.env'); ok('.env creato da .env.example'); } else { info('.env già esistente, lo aggiorno'); } // APP_KEY runCapture("{$sudo}php artisan key:generate --force"); ok('APP_KEY generata'); // Config $dotenv = file_get_contents('.env'); $replacements = [ '/^APP_NAME=.*/m' => 'APP_NAME="' . addslashes($appName) . '"', '/^APP_URL=.*/m' => "APP_URL={$appUrl}", '/^APP_ENV=.*/m' => 'APP_ENV=production', '/^APP_DEBUG=.*/m' => 'APP_DEBUG=false', ]; if ($dbType === 'mysql') { $replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=mysql'; $replacements['/^DB_HOST=.*/m'] = "DB_HOST={$dbHost}"; $replacements['/^DB_PORT=.*/m'] = "DB_PORT={$dbPort}"; $replacements['/^DB_DATABASE=.*/m'] = "DB_DATABASE={$dbName}"; $replacements['/^DB_USERNAME=.*/m'] = "DB_USERNAME={$dbUser}"; $replacements['/^DB_PASSWORD=.*/m'] = "DB_PASSWORD={$dbPass}"; } else { $replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=sqlite'; $replacements['/^DB_HOST=.*/m'] = '# DB_HOST='; $replacements['/^DB_PORT=.*/m'] = '# DB_PORT='; $replacements['/^DB_DATABASE=.*/m'] = 'DB_DATABASE=' . $scriptDir . '/database/database.sqlite'; $replacements['/^DB_USERNAME=.*/m'] = '# DB_USERNAME='; $replacements['/^DB_PASSWORD=.*/m'] = '# DB_PASSWORD='; } foreach ($replacements as $pattern => $replacement) { $dotenv = preg_replace($pattern, $replacement, $dotenv); } file_put_contents('.env', $dotenv); ok('.env configurato'); // ── Step 2: Database ──────────────────────────── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' 2/7 — DATABASE', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); if ($dbType === 'mysql') { info("Connessione a MySQL {$dbHost}:{$dbPort}, database '{$dbName}'..."); try { new PDO( "mysql:host={$dbHost};port={$dbPort};dbname={$dbName}", $dbUser, $dbPass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5] ); ok("Accesso a '{$dbName}' come '{$dbUser}' riuscito"); } catch (\PDOException $e) { // Diagnostica l'errore $errMsg = $e->getMessage(); $diagnosi = match (true) { (bool)preg_match('/1049|Unknown database/', $errMsg) => "Database '{$dbName}' non esiste", (bool)preg_match('/1045|Access denied for user/', $errMsg) => "Utente '{$dbUser}' o password non validi", (bool)preg_match('/1044/', $errMsg) => "Utente '{$dbUser}' non ha accesso al database '{$dbName}'", default => $errMsg, }; warn($diagnosi); info("Connessione come root per creare/riparare utente e database..."); $rootPdo = null; try { $rootPdo = new PDO( "mysql:host={$dbHost};port={$dbPort}", 'root', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5] ); } catch (\PDOException) { $rootPass = askRequired('Password utente root MySQL (lascia vuoto se senza password)'); try { $rootPdo = new PDO( "mysql:host={$dbHost};port={$dbPort}", 'root', $rootPass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5] ); } catch (\PDOException $r) { fail("Connessione come root fallita: " . $r->getMessage()); } } $safeUser = str_replace(["'", '"', '`', "\0", '\\'], '', $dbUser); $safePass = str_replace(["'", '"', '`', "\0", '\\'], '', $dbPass); $safeName = str_replace(["'", '"', '`', "\0", '\\'], '', $dbName); $rootPdo->exec("CREATE DATABASE IF NOT EXISTS `{$safeName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); foreach (['localhost', '%'] as $host) { $rootPdo->exec("CREATE USER IF NOT EXISTS '{$safeUser}'@'{$host}' IDENTIFIED BY '{$safePass}'"); $rootPdo->exec("GRANT ALL PRIVILEGES ON `{$safeName}`.* TO '{$safeUser}'@'{$host}'"); } $rootPdo->exec("FLUSH PRIVILEGES"); ok("Database '{$dbName}' e utente '{$dbUser}' configurati con permessi completi"); } } else { $sqlitePath = $scriptDir . '/database/database.sqlite'; if (!file_exists($sqlitePath)) { touch($sqlitePath); chmod($sqlitePath, 0664); } ok('SQLite pronto: ' . $sqlitePath); } // ── Step 3: Composer ──────────────────────────── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' 3/7 — INSTALLAZIONE DIPENDENZE', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...'); ok('Dipendenze PHP installate'); // ── Step 4: Package discovery ──────────────────── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' 4/7 — REGISTRAZIONE PACCHETTI', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null"); ok('Pacchetti registrati'); // ── Step 5: Schema + Seed ──────────────────────── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' 5/7 — SCHEMA DATABASE E DATI BASE', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); $sqlFile = $scriptDir . '/database/install.sql'; if (!file_exists($sqlFile)) { fail("File database/install.sql non trovato in {$scriptDir}"); } if ($dbType === 'mysql') { $importCmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}"; if (!run($importCmd, 'Importazione struttura database e dati base...')) { fail("Importazione SQL fallita. Verifica il file database/install.sql e le credenziali."); } } else { // SQLite fallback: usa le migration (install.sql è solo per MySQL) if (!run("{$sudo}php artisan migrate --seed --force", 'Esecuzione migration e seed...')) { fail('Migration fallita. Verifica la configurazione SQLite.'); } } ok('Database popolato con struttura e dati base'); runCapture("{$sudo}php artisan storage:link --force 2>/dev/null"); // ── Step 6: Admin user (via PDO — no tinker) ───── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' 6/7 — CREAZIONE AMMINISTRATORE', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); if ($dbType === 'mysql') { if (createAdminUser($dbHost, $dbPort, $dbName, $dbUser, $dbPass, $adminName, $adminEmail, $adminPass)) { ok("Amministratore {$adminEmail} creato con permessi completi"); } else { fail("Creazione admin fallita. Verifica i dati inseriti."); } } else { // SQLite: usa seeder (l'admin verrà creato da DatabaseSeeder) // Poi aggiorna la password con quella scelta dall'utente runCapture("{$sudo}php artisan tinker --execute=" . escapeshellarg( "\$u = \App\Models\User::first(); " . "if (\$u) { " . "\$u->name = " . var_export($adminName, true) . "; " . "\$u->email = " . var_export($adminEmail, true) . "; " . "\$u->password = bcrypt(" . var_export($adminPass, true) . "); " . "\$u->is_admin = true; " . "\$u->status = 'active'; " . "\$u->permissions = " . var_export([ 'individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2, ], true) . "; " . "\$u->role_preset_id = 1; " . "\$u->save(); " . "echo 'OK'; " . "}" ) . " 2>/dev/null"); ok("Amministratore {$adminEmail} configurato"); } // Clear all caches after schema + admin creation runCapture("{$sudo}php artisan optimize:clear 2>/dev/null"); ok('Cache ottimizzate'); // ── Step 7: Asset ──────────────────────────────── println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' 7/7 — ASSET FRONTEND', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); $hasNpm = checkCmd('npm'); if ($hasNpm && confirm('Compilare gli asset frontend con npm?')) { run("{$sudo}npm install", 'Installazione dipendenze npm...'); run("{$sudo}npm run build", 'Compilazione asset...'); ok('Asset compilati'); } else { if (!$hasNpm) warn('npm non disponibile — asset non compilati (AdminLTE via CDN)'); else info('Asset non compilati (AdminLTE via CDN)'); } // ── Permissions ────────────────────────────────── if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { info('Impostazione permessi...'); runCapture("chown -R {$wwwUser}:{$wwwUser} vendor storage bootstrap/cache 2>/dev/null"); chmod("{$scriptDir}/storage", 0775); chmod("{$scriptDir}/bootstrap/cache", 0775); ok("Permessi impostati (utente: {$wwwUser})"); } // ── Summary ────────────────────────────────────── println(); println(color('══════════════════════════════════════════', 'green')); println(color(' INSTALLAZIONE COMPLETATA!', 'green')); println(color('══════════════════════════════════════════', 'green')); println(); println(" URL: {$appUrl}"); println(" Admin: {$adminEmail}"); println(" Cartella: {$scriptDir}"); println(); warn('Configura Apache con il VirtualHost (vedi Guida → Installazione, Passo 4).'); if ($dbType === 'mysql') { warn('Se hai appena creato il database, assicurati che l\'utente abbia privilegi completi.'); } } // ══════════════════════════════════════════════════════ // MODE 2: DOCKER // ══════════════════════════════════════════════════════ if ($mode === 'Fresh Install via Docker') { println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' PRE-FLIGHT CHECKS (Docker)', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); checkCmd('docker'); println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' CONFIGURAZIONE DOCKER', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); $dockerDb = menu('Tipo database per Docker', ['sqlite', 'mysql']); $dockerPort = ask('Porta host (es. 8080)', '8080'); $composeFile = $dockerDb === 'mysql' ? 'docker-compose.mysql.yml' : 'docker-compose.yml'; println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' INSTALLAZIONE DOCKER', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); if (!run("docker compose -f {$composeFile} build", 'Build immagine Docker...')) { fail('Build fallita'); } // Override compose con variabili d'ambiente $override = [ 'services:', ' glastree:', ' environment:', " - APP_NAME={$appName}", " - APP_URL={$appUrl}", ' - APP_ENV=production', ' - APP_DEBUG=false', ]; file_put_contents('docker-compose.override.yml', implode(PHP_EOL, $override) . PHP_EOL); if (!run( "docker compose -f {$composeFile} -f docker-compose.override.yml up -d", 'Avvio container...' )) { @unlink('docker-compose.override.yml'); fail('Avvio container fallito'); } // Attendi container pronto info('Attendo che il container sia pronto...'); $containerId = runCapture("docker compose -f {$composeFile} ps -q glastree 2>/dev/null"); $adminCreated = false; if ($containerId !== '') { for ($i = 0; $i < 30; $i++) { $status = runCapture("docker inspect -f '{{.State.Status}}' {$containerId} 2>/dev/null"); if ($status === 'running') { $http = runCapture("docker exec {$containerId} sh -c 'wget -q -O- http://localhost/ 2>/dev/null && echo OK'"); if (str_contains($http, 'OK')) { info('Container pronto'); break; } } sleep(2); } // Import schema SQL nel container $sqlFile = $scriptDir . '/database/install.sql'; if (file_exists($sqlFile) && $dockerDb === 'mysql') { info('Importazione schema nel container...'); runCapture("docker cp {$sqlFile} {$containerId}:/tmp/install.sql 2>/dev/null"); $mysqlHost = 'mysql'; $mysqlUser = 'glastree'; $mysqlPass = 'secret'; $mysqlDb = 'glastree'; $importResult = runCapture( "docker exec {$containerId} sh -c 'mysql -h {$mysqlHost} -u {$mysqlUser} -p{$mysqlPass} {$mysqlDb} < /tmp/install.sql' 2>&1" ); if ($importResult !== '') { warn("Import SQL: {$importResult}"); } else { ok('Schema database importato'); } } elseif ($dockerDb === 'sqlite') { run("docker exec {$containerId} php artisan migrate --seed --force", 'Migration e seed...'); } // Crea admin if ($dockerDb === 'mysql') { // Script PHP temporaneo con var_export (safe) $adminScriptPath = sys_get_temp_dir() . '/glastree-create-admin-' . uniqid() . '.php'; $hash = password_hash($adminPass, PASSWORD_BCRYPT); $perm = json_encode([ 'individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2, ]); $nameExp = var_export($adminName, true); $emailExp = var_export($adminEmail, true); $hashExp = var_export($hash, true); $userExp = var_export($mysqlUser, true); $passExp = var_export($mysqlPass, true); $dbExp = var_export($mysqlDb, true); $hostExp = var_export($mysqlHost, true); $adminPhp = <<execute([{$emailExp}]); \$pdo->prepare("INSERT INTO users (name, email, password, is_admin, status, permissions, role_preset_id, created_at, updated_at) VALUES (?, ?, ?, 1, 'active', ?, 1, NOW(), NOW())")->execute([{$nameExp}, {$emailExp}, {$hashExp}, '{$perm}']); \$uid = \$pdo->lastInsertId(); \$pdo->prepare("INSERT INTO model_has_roles (role_id, model_type, model_id) VALUES (1, ?, ?)")->execute(['App\\\\Models\\\\User', \$uid]); echo "OK:" . \$uid; PHP; file_put_contents($adminScriptPath, $adminPhp); runCapture("docker cp {$adminScriptPath} {$containerId}:/tmp/create_admin.php 2>/dev/null"); $adminResult = runCapture("docker exec {$containerId} php /tmp/create_admin.php 2>/dev/null"); @unlink($adminScriptPath); } else { // SQLite: usa tinker via docker exec $adminResult = runCapture("docker exec {$containerId} php artisan tinker --execute=" . escapeshellarg( "\$u = \App\Models\User::first(); " . "if (\$u) { " . "\$u->name = " . var_export($adminName, true) . "; " . "\$u->email = " . var_export($adminEmail, true) . "; " . "\$u->password = bcrypt(" . var_export($adminPass, true) . "); " . "\$u->is_admin = true; " . "\$u->status = 'active'; " . "\$u->permissions = " . var_export([ 'individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2, ], true) . "; " . "\$u->role_preset_id = 1; " . "\$u->save(); " . "echo 'OK'; " . "}" ) . " 2>/dev/null"); } if (str_contains($adminResult ?? '', 'OK')) { ok("Amministratore {$adminEmail} creato nel container"); $adminCreated = true; } else { warn("Creazione admin fallita — output: " . (($adminResult ?? '') ?: 'nessun output')); } } @unlink('docker-compose.override.yml'); println(); println(color('══════════════════════════════════════════', 'green')); println(color(' INSTALLAZIONE DOCKER COMPLETATA!', 'green')); println(color('══════════════════════════════════════════', 'green')); println(); println(" URL: http://localhost:{$dockerPort}"); println(" Admin: {$adminEmail}"); println(" Container: {$containerId}"); println(); warn('Per URL pubblici, imposta APP_URL nelle environment del container.'); warn('Dati persistenti: volumi Docker (glastree_storage, glastree_db o mysql_data).'); } // ══════════════════════════════════════════════════════ // MODE 3: RESTORE DA BACKUP // ══════════════════════════════════════════════════════ if ($mode === 'Restore da Backup (Apache)') { println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' PRE-FLIGHT CHECKS (Restore)', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); checkCmd('php'); checkCmd('composer'); checkCmd('unzip'); if ($dbType === 'mysql') { checkCmd('mysql'); } $backupZip = askRequired('Percorso del file ZIP di backup'); if (!file_exists($backupZip)) { fail("File non trovato: {$backupZip}"); } println(); println(color('══════════════════════════════════════════', 'yellow')); println(color(' RESTORE DA BACKUP', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); // Extract $tmpDir = sys_get_temp_dir() . '/glastree-restore-' . uniqid(); mkdir($tmpDir, 0700, true); info("Estrazione backup in {$tmpDir}..."); if (!run("unzip -qo {$backupZip} -d {$tmpDir}", null)) { // pulisci tmp dir prima di fallire runCapture("rm -rf {$tmpDir}"); fail('Estrazione backup fallita'); } // Environment if (!file_exists('.env')) { copy('.env.example', '.env'); } // Restore .env from backup preserving DB credentials if (file_exists("{$tmpDir}/.env")) { $currentEnv = file_get_contents('.env'); $backupEnv = file_get_contents("{$tmpDir}/.env"); // Save current DB_* values $currentDb = []; foreach (['DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD'] as $key) { if (preg_match("/^{$key}=(.*)$/m", $currentEnv, $m)) { $currentDb[$key] = $m[1]; } } // Use backup .env as base file_put_contents('.env', $backupEnv); // Restore current DB credentials foreach ($currentDb as $key => $val) { $dotenv = file_get_contents('.env'); $dotenv = preg_replace("/^{$key}=.*/m", "{$key}={$val}", $dotenv); file_put_contents('.env', $dotenv); } ok('.env ripristinato dal backup (credenziali DB preservate)'); } // Update APP_* config $dotenv = file_get_contents('.env'); $dotenv = preg_replace('/^APP_NAME=.*/m', 'APP_NAME="' . addslashes($appName) . '"', $dotenv); $dotenv = preg_replace('/^APP_URL=.*/m', "APP_URL={$appUrl}", $dotenv); $dotenv = preg_replace('/^APP_ENV=.*/m', 'APP_ENV=production', $dotenv); $dotenv = preg_replace('/^APP_DEBUG=.*/m', 'APP_DEBUG=false', $dotenv); file_put_contents('.env', $dotenv); // Generate APP_KEY runCapture("{$sudo}php artisan key:generate --force"); ok('APP_KEY rigenerata'); // Import database if ($dbType === 'mysql') { $sqlFile = "{$tmpDir}/database.sql"; if (file_exists($sqlFile)) { info("Importazione database MySQL: {$sqlFile}"); $cmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}"; if (run($cmd, null)) { ok('Database importato'); } else { warn('Import database fallito — importa manualmente'); } } else { warn('database.sql non trovato nel backup'); } } else { warn('Restore automatico supportato solo con MySQL. Per SQLite ripristina manualmente i file.'); } // Restore storage $storageBackup = "{$tmpDir}/storage"; if (is_dir($storageBackup)) { info('Ripristino file storage...'); runCapture("cp -r {$storageBackup}/* {$scriptDir}/storage/ 2>/dev/null"); ok('Storage ripristinato'); } // Composer run("COMPOSER_ROOT_VERSION=dev-main composer install --no-dev --optimize-autoloader", 'Installazione dipendenze Composer...'); runCapture("{$sudo}php artisan package:discover --ansi 2>/dev/null"); // Storage link + cache runCapture("{$sudo}php artisan storage:link --force 2>/dev/null"); runCapture("{$sudo}php artisan optimize:clear"); // Permissions if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { runCapture("chown -R {$wwwUser}:{$wwwUser} vendor storage bootstrap/cache 2>/dev/null"); chmod("{$scriptDir}/storage", 0775); chmod("{$scriptDir}/bootstrap/cache", 0775); ok("Permessi impostati (utente: {$wwwUser})"); } // Create admin if not exists (via PDO) if ($dbType === 'mysql') { $exists = false; try { $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbName}", $dbUser, $dbPass); $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE email = ?'); $stmt->execute([$adminEmail]); $exists = (int) $stmt->fetchColumn() > 0; } catch (\PDOException) { // Ignora — tenteremo la creazione } if ($exists) { ok("Amministratore {$adminEmail} già esistente"); } elseif (createAdminUser($dbHost, $dbPort, $dbName, $dbUser, $dbPass, $adminName, $adminEmail, $adminPass)) { ok("Amministratore {$adminEmail} creato"); } else { warn("Creazione admin fallita — crealo manualmente dal pannello admin"); } } else { // SQLite fallback: usa tinker $result = runCapture("{$sudo}php artisan tinker --execute=" . escapeshellarg( "\$e = \App\Models\User::where('email', " . var_export($adminEmail, true) . ")->exists(); " . "if (!\$e) { " . "\$u = new \App\Models\User; " . "\$u->name = " . var_export($adminName, true) . "; " . "\$u->email = " . var_export($adminEmail, true) . "; " . "\$u->password = bcrypt(" . var_export($adminPass, true) . "); " . "\$u->is_admin = true; " . "\$u->status = 'active'; " . "\$u->permissions = " . var_export([ 'individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2, ], true) . "; " . "\$u->role_preset_id = 1; " . "\$u->save(); " . "echo 'OK'; " . "} else { echo 'EXISTS'; }" ) . " 2>/dev/null"); if (str_contains($result, 'OK')) { ok("Amministratore {$adminEmail} creato"); } elseif (str_contains($result, 'EXISTS')) { ok("Amministratore {$adminEmail} già esistente"); } else { warn("Creazione admin fallita (output: " . ($result ?: 'vuoto') . ")"); } } // Cleanup runCapture("rm -rf {$tmpDir}"); println(); println(color('══════════════════════════════════════════', 'green')); println(color(' RESTORE COMPLETATO!', 'green')); println(color('══════════════════════════════════════════', 'green')); println(); println(" URL: {$appUrl}"); println(" Admin: {$adminEmail}"); println(" Cartella: {$scriptDir}"); println(); warn('Verifica che APP_KEY non abbia invalidato dati criptati (token OAuth, password IMAP).'); warn('Se necessario, riconfigura SMTP e Google Drive dalle Impostazioni.'); } println();