Files
2026-06-03 16:32:24 +02:00

849 lines
38 KiB
PHP

#!/usr/bin/env php
<?php
/**
* Glastree — Installer interattivo per amministratori (PHP CLI)
*
* Uso:
* php install.php
*
* Richiede: PHP 8.3+, Composer, MySQL o SQLite
* Opzionale: npm (per asset frontend)
*
* Modalità:
* 1) Fresh Install su Apache (LAMP)
* 2) Fresh Install via Docker
* 3) Restore da Backup (Apache)
*/
// ── Safety ──────────────────────────────────────────
declare(strict_types=1);
// ── Constants ────────────────────────────────────────
const REQUIREMENTS = ['pdo_mysql', 'mbstring', 'xml', 'curl', 'zip', 'gd', 'fileinfo'];
const MIN_PHP = '8.3.0';
// ── Utilities ────────────────────────────────────────
function println(string $msg = ''): void { echo $msg . PHP_EOL; }
function color(string $text, string $code): string {
$map = [
'red' => '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 = <<<PHP
<?php
\$pdo = new PDO("mysql:host=" . {$hostExp} . ";dbname=" . {$dbExp}, {$userExp}, {$passExp}, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
\$pdo->prepare("DELETE FROM users WHERE email = ?")->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();