0, 'failed' => 0, 'warnings' => 0, 'fixed' => 0]; $groupsToFix = []; // ───────────────────────────────────────────────────────────────────────────── // Helper functions // ───────────────────────────────────────────────────────────────────────────── function ok(string $msg): void { global $results; $results['passed']++; echo " " . COLOR_GREEN . "✅" . COLOR_RESET . " {$msg}\n"; } function fail(string $msg, ?string $detail = null): void { global $results; $results['failed']++; echo " " . COLOR_RED . "❌" . COLOR_RESET . " {$msg}"; if ($detail !== null) { echo COLOR_RED . " — {$detail}" . COLOR_RESET; } echo "\n"; } function warn(string $msg, ?string $detail = null): void { global $results; $results['warnings']++; echo " " . COLOR_YELLOW . "⚠️ " . COLOR_RESET . " {$msg}"; if ($detail !== null) { echo COLOR_YELLOW . " — {$detail}" . COLOR_RESET; } echo "\n"; } function fixed(string $msg): void { global $results; $results['fixed']++; echo " " . COLOR_GREEN . "🔧" . COLOR_RESET . " {$msg}\n"; } function heading(string $title): void { echo "\n" . COLOR_BOLD . COLOR_CYAN . "═══ {$title} ═══" . COLOR_RESET . "\n\n"; } function v(string $msg): void { global $verbose; if ($verbose) { echo " " . COLOR_YELLOW . "[verbose]" . COLOR_RESET . " {$msg}\n"; } } function subheading(string $title): void { echo " " . COLOR_BOLD . "→ {$title}" . COLOR_RESET . "\n"; } // ───────────────────────────────────────────────────────────────────────────── // Parse .env // ───────────────────────────────────────────────────────────────────────────── function parseEnv(string $path): array { if (!file_exists($path)) { return []; } $env = []; $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $line = trim($line); if ($line === '' || str_starts_with($line, '#')) { continue; } if (str_contains($line, '=')) { $parts = explode('=', $line, 2); $key = trim($parts[0]); $value = trim($parts[1] ?? ''); if ((str_starts_with($value, '"') && str_ends_with($value, '"')) || (str_starts_with($value, "'") && str_ends_with($value, "'"))) { $value = substr($value, 1, -1); } $value = preg_replace_callback('/\$\{(\w+)\}/', function ($m) use ($env) { return $env[$m[1]] ?? $m[0]; }, $value); $env[$key] = $value; } } return $env; } function saveEnv(string $path, array $env): bool { $out = ''; foreach ($env as $key => $value) { $out .= "{$key}={$value}\n"; } return file_put_contents($path, $out) !== false; } function returnBytes(string $val): int { $val = trim($val); $last = strtolower($val[-1]); $num = (int) $val; return match ($last) { 'g' => $num * 1024 * 1024 * 1024, 'm' => $num * 1024 * 1024, 'k' => $num * 1024, default => $num, }; } function formatBytes(int|float $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $i = 0; while ($bytes >= 1024 && $i < count($units) - 1) { $bytes /= 1024; $i++; } return round($bytes, 1) . ' ' . $units[$i]; } function sudoExec(string $cmd): array { $output = []; $exitCode = 0; exec($cmd, $output, $exitCode); return ['output' => $output, 'exit' => $exitCode]; } function isRoot(): bool { return (function_exists('posix_getuid') && posix_getuid() === 0) || trim(shell_exec('whoami') ?? '') === 'root'; } // ───────────────────────────────────────────────────────────────────────────── // Fix runner // ───────────────────────────────────────────────────────────────────────────── function runFixes(array $groupsToFix): void { if (empty($groupsToFix)) { return; } echo "\n" . COLOR_BOLD . COLOR_GREEN . "═══ APPLICAZIONE FIX ═══" . COLOR_RESET . "\n"; $needsSudo = false; // 1. Create missing directories $dirsToCreate = $groupsToFix['mkdir'] ?? []; if (!empty($dirsToCreate)) { subheading("Creazione directory mancanti"); foreach ($dirsToCreate as $dir) { if (mkdir($dir, 0775, true)) { fixed("Creata directory: " . str_replace(BASE_DIR . '/', '', $dir)); } else { fail("Impossibile creare directory: " . str_replace(BASE_DIR . '/', '', $dir)); } } } // 2. Fix permissions $dirsToPerm = $groupsToFix['chmod'] ?? []; if (!empty($dirsToPerm)) { subheading("Permessi directory"); foreach ($dirsToPerm as $dir) { if (chmod($dir, 0775)) { fixed("chmod 775: " . str_replace(BASE_DIR . '/', '', $dir)); } else { fail("chmod fallito: " . str_replace(BASE_DIR . '/', '', $dir)); } } } // 3. Chown $dirsToChown = $groupsToFix['chown'] ?? []; if (!empty($dirsToChown)) { subheading("Proprietà directory"); $chownCmd = 'chown -R ' . WEB_USER . ':' . WEB_USER . ' ' . implode(' ', array_map('escapeshellarg', $dirsToChown)); v("Esecuzione: {$chownCmd}"); $result = sudoExec($chownCmd); if ($result['exit'] === 0) { fixed("Proprietà impostata a " . WEB_USER . ":" . WEB_USER); } else { $needsSudo = true; warn("chown fallito (forse serve sudo): " . implode(', ', $result['output'])); } } // 4. Symlink if (($groupsToFix['symlink'] ?? false) === true) { $target = BASE_DIR . '/public/storage'; if (file_exists($target) || is_link($target)) { unlink($target); } $result = sudoExec('ln -sf ../storage/app/public ' . escapeshellarg($target)); if ($result['exit'] === 0) { fixed("Symlink ricreato: public/storage → ../storage/app/public"); } else { $needsSudo = true; warn("Symlink fallito (forse serve sudo)"); } } // 5. .gitignore in storage/app/public if (($groupsToFix['gitignore'] ?? false) === true) { $gitignore = BASE_DIR . '/storage/app/public/.gitignore'; if (file_put_contents($gitignore, "*\n!.gitignore\n") !== false) { fixed("Creato .gitignore in storage/app/public"); } else { fail("Impossibile creare .gitignore in storage/app/public"); } } // 6. .env variables $envToFix = $groupsToFix['env'] ?? []; if (!empty($envToFix)) { $envPath = BASE_DIR . '/.env'; $env = parseEnv($envPath); subheading("Variabili .env"); $changed = false; foreach ($envToFix as $key => $value) { if (!isset($env[$key])) { $env[$key] = $value; $changed = true; fixed("Aggiunta {$key}={$value}"); } } if ($changed) { $out = ''; foreach ($env as $k => $v) { $out .= "{$k}={$v}\n"; } file_put_contents($envPath, $out); } } // 7. Clear cache dirs $cacheDirs = $groupsToFix['clearcache'] ?? []; if (!empty($cacheDirs)) { subheading("Pulizia cache"); foreach ($cacheDirs as $dir) { array_map('unlink', glob($dir . '/*') ?: []); fixed("Svuotata: " . str_replace(BASE_DIR . '/', '', $dir)); } } if ($needsSudo) { echo "\n " . COLOR_YELLOW . "⚠️" . COLOR_RESET . " Alcune operazioni richiedono sudo. Esegui:\n"; echo " sudo php " . $_SERVER['PHP_SELF'] . " --fix\n"; } echo "\n" . COLOR_GREEN . "═══ FIX COMPLETATI ═══" . COLOR_RESET . "\n"; } // ───────────────────────────────────────────────────────────────────────────── // Banner // ───────────────────────────────────────────────────────────────────────────── echo COLOR_BOLD . "\n" . " ╔════════════════════════════════════════════╗\n" . " ║ Glastree — Diagnosi Installazione ║\n" . " ╚════════════════════════════════════════════╝\n" . COLOR_RESET; echo "\n Data: " . date('Y-m-d H:i:s') . " | PID: " . getmypid(); if ($fixMode) { echo " | " . COLOR_GREEN . "MODALITÀ FIX ATTIVA" . COLOR_RESET; } echo "\n\n"; // ───────────────────────────────────────────────────────────────────────────── // 3.1 PHP Version & Extensions // ───────────────────────────────────────────────────────────────────────────── heading('PHP Version & Extensions'); $phpVersion = PHP_VERSION; if (version_compare($phpVersion, '8.2', '>=')) { ok("PHP version: {$phpVersion}"); } else { fail("PHP version: {$phpVersion} — richiesta ≥ 8.2"); } $requiredExts = [ 'json', 'mbstring', 'openssl', 'PDO', 'pdo_mysql', 'fileinfo', 'xml', 'ctype', 'bcmath', 'zip', 'gd', 'intl', 'curl', 'session', ]; $optionalExts = [ 'imap' => 'Sync email IMAP', 'imagick' => 'Gestione immagini avanzata', 'redis' => 'Cache/sessione Redis', 'sodium' => 'Crittografia (Laravel 11+)', 'sockets' => 'Connessioni socket', 'tokenizer' => 'Laravel optimization', ]; foreach ($requiredExts as $ext) { extension_loaded($ext) ? ok("Estensione {$ext}: OK") : fail("Estensione {$ext}: NON TROVATA", "Richiesta da Laravel"); } foreach ($optionalExts as $ext => $label) { extension_loaded($ext) ? ok("Estensione {$ext}: OK ({$label})") : warn("Estensione {$ext}: NON TROVATA ({$label})"); } // INI settings to check $iniSettings = [ 'max_execution_time' => [30, '60+ consigliato per import CSV'], 'memory_limit' => ['128M', '128M minimo, 256M+ consigliato'], 'upload_max_filesize' => ['20M', '20M minimo per CSV/ICS'], 'post_max_size' => ['20M', '20M minimo per upload'], 'max_input_vars' => [1000, '1000+ per form con molti campi'], 'max_input_time' => [60, '60+ per upload file'], ]; heading('PHP INI Settings'); $phpIniPath = php_ini_loaded_file(); $phpIniDir = PHP_CONFIG_FILE_SCAN_DIR; $sapi = PHP_SAPI; if ($phpIniPath) { ok("php.ini caricato (SAPI: {$sapi}): {$phpIniPath}"); } else { warn("php.ini: nessun file caricato (SAPI: {$sapi})"); } v("Directory scan config: " . ($phpIniDir ?: 'nessuna')); // Detect web server php.ini (common paths) $webIniPath = null; if ($sapi === 'cli') { $phpMajorMinor = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; foreach (['apache2', 'fpm', 'cgi'] as $sapiDir) { $p = "/etc/php/{$phpMajorMinor}/{$sapiDir}/php.ini"; if (file_exists($p)) { $webIniPath = $p; break; } } if ($webIniPath) { ok("WEB php.ini rilevato: {$webIniPath}"); } } echo "\n " . COLOR_BOLD . "Valori SAPI corrente ({$sapi}):" . COLOR_RESET . "\n"; foreach ($iniSettings as $iniKey => [$minVal, $note]) { $actual = ini_get($iniKey); $isOk = false; if (is_numeric($minVal) && is_numeric($actual)) { $isOk = (int) $actual >= (int) $minVal; } elseif (is_string($minVal)) { $isOk = returnBytes($actual) >= returnBytes($minVal); } $isOk ? ok("{$iniKey} = {$actual}") : warn("{$iniKey} = {$actual} (minimo: {$minVal})", $note); } // Show web server values if different php.ini exists if ($webIniPath && $sapi === 'cli') { echo "\n " . COLOR_BOLD . "Valori WEB server (da {$webIniPath}):" . COLOR_RESET . "\n"; $webRaw = file_get_contents($webIniPath); foreach ($iniSettings as $iniKey => [$minVal, $note]) { $cliVal = ini_get($iniKey); $webVal = null; if (preg_match('/^' . preg_quote($iniKey, '/') . '\s*=\s*(.+)$/m', $webRaw, $m)) { $webVal = trim($m[1]); } if ($webVal !== null && $webVal !== $cliVal) { warn("{$iniKey} = {$webVal} (CLI: {$cliVal})", "DIVERGENTE da CLI"); } elseif ($webVal !== null) { ok("{$iniKey} = {$webVal}"); } else { ok("{$iniKey} = [non impostato, usa default]"); } } } // ───────────────────────────────────────────────────────────────────────────── // 3.2 .env File // ───────────────────────────────────────────────────────────────────────────── heading('File .env'); $envPath = BASE_DIR . '/.env'; if (!file_exists($envPath)) { fail('.env NON TROVATO', "Cercato in: {$envPath}"); $env = []; } elseif (!is_readable($envPath)) { fail('.env NON LEGGIBILE', "Permessi: " . decoct(fileperms($envPath) & 0777)); $env = []; } else { ok(".env trovato e leggibile"); $env = parseEnv($envPath); v("Variabili parsate: " . count($env)); } // ───────────────────────────────────────────────────────────────────────────── // 3.3 Configurazione .env // ───────────────────────────────────────────────────────────────────────────── heading('Configurazione .env'); // APP_KEY if (empty($env['APP_KEY'])) { fail('APP_KEY non impostato'); } elseif (!str_starts_with($env['APP_KEY'], 'base64:')) { fail('APP_KEY formato errato', "Deve iniziare con 'base64:'"); } else { $decoded = base64_decode(substr($env['APP_KEY'], 7), true); ($decoded !== false && strlen($decoded) === 32) ? ok("APP_KEY: valido (32 bytes)") : fail('APP_KEY: decodifica fallita o lunghezza errata', "Deve essere 32 bytes dopo decodifica base64"); } // APP_URL — test duale HTTP/HTTPS heading('Test HTTP/HTTPS (APP_URL)'); $baseUrl = $env['APP_URL'] ?? ''; if (empty($baseUrl)) { fail('APP_URL non impostato'); } elseif (!str_starts_with($baseUrl, 'http://') && !str_starts_with($baseUrl, 'https://')) { fail('APP_URL non valido', $baseUrl); } else { $httpsUrl = $baseUrl; $httpUrl = str_replace('https://', 'http://', $baseUrl); $hasHttps = str_starts_with($baseUrl, 'https://'); ok("APP_URL (HTTPS): {$httpsUrl}"); if ($hasHttps) { ok("APP_URL (HTTP): {$httpUrl}"); } $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_NOBODY => true, ]); $httpsWorks = false; $httpWorks = false; $httpsCode = 0; $httpCode = 0; // Test HTTPS if ($hasHttps) { $chH = curl_copy_handle($ch); curl_setopt($chH, CURLOPT_URL, $httpsUrl); $resp = curl_exec($chH); $httpsWorks = $resp !== false; $httpsCode = curl_getinfo($chH, CURLINFO_HTTP_CODE); $httpsErr = curl_error($chH); curl_close($chH); $httpsWorks ? ok("HTTPS raggiungibile (HTTP {$httpsCode})") : fail("HTTPS non raggiungibile", $httpsErr); } // Test HTTP $chH2 = curl_copy_handle($ch); curl_setopt($chH2, CURLOPT_URL, $httpUrl); $resp2 = curl_exec($chH2); $httpWorks = $resp2 !== false; $httpCode = curl_getinfo($chH2, CURLINFO_HTTP_CODE); $httpErr = curl_error($chH2); curl_close($chH2); $httpWorks ? ok("HTTP raggiungibile (HTTP {$httpCode})") : fail("HTTP non raggiungibile", $httpErr); if ($hasHttps && !$httpsWorks && !$httpWorks) { fail("Nessun protocollo raggiungibile", "Verifica DNS, firewall o configurazione server"); } elseif ($hasHttps && !$httpsWorks && $httpWorks) { warn("Solo HTTP funziona — APP_URL punta a HTTPS. Imposta APP_URL=http://..."); } elseif ($hasHttps && $httpsWorks && !$httpWorks) { ok("Solo HTTPS disponibile (configurazione normale)"); } elseif (!$hasHttps && $httpWorks) { ok("HTTP disponibile (nessun HTTPS configurato)"); } } // APP_URL host mismatch check $urlHost = parse_url($baseUrl, PHP_URL_HOST); $serverHost = gethostname(); if ($urlHost && $serverHost && $urlHost !== $serverHost) { warn("APP_URL host ({$urlHost}) ≠ server hostname ({$serverHost})", "Se accedi con un dominio/IP diverso da APP_URL, il token CSRF può fallire"); } // Also check if APP_URL host resolves to this server if ($urlHost) { $resolvedIps = gethostbynamel($urlHost); if ($resolvedIps !== false) { ok("APP_URL host ({$urlHost}) → IP: " . implode(', ', $resolvedIps)); } else { warn("APP_URL host ({$urlHost}) NON risolvibile via DNS"); } } // Check cached config $cachedConfig = BASE_DIR . '/bootstrap/cache/config.php'; if (file_exists($cachedConfig)) { warn("Config CACHED in bootstrap/cache/config.php", "Le modifiche a .env NON sono attive finché non esegui: php artisan config:clear"); // Peek at cached app.url $cachedContent = file_get_contents($cachedConfig); if (preg_match("/'url' => '([^']+)'/", $cachedContent, $m)) { $cachedUrl = $m[1]; if ($cachedUrl !== $baseUrl) { fail("APP_URL nel config cache differisce da .env", "Cache: {$cachedUrl} — .env: {$baseUrl}. Esegui php artisan config:clear"); } else { ok("APP_URL in config cache coincide con .env: {$cachedUrl}"); } } } else { ok("Config cache: assente (config non cached, valori da .env live)"); } // FORCE_HTTPS vs SESSION_SECURE_COOKIE $forceHttps = ($env['FORCE_HTTPS'] ?? 'false') === 'true'; $sessionSecure = ($env['SESSION_SECURE_COOKIE'] ?? 'false') === 'true'; if ($forceHttps && !$sessionSecure) { warn('FORCE_HTTPS=true ma SESSION_SECURE_COOKIE non impostato', 'Il cookie di sessione non verrà inviato su HTTP'); if ($fixMode) { $groupsToFix['env']['SESSION_SECURE_COOKIE'] = 'true'; } } elseif (!$forceHttps && $sessionSecure) { warn('SESSION_SECURE_COOKIE=true ma FORCE_HTTPS non attivo', 'Il cookie di sessione non verrà inviato su HTTP'); } else { ok("FORCE_HTTPS / SESSION_SECURE_COOKIE: coerente"); } // SESSION_DRIVER $sessionDriver = $env['SESSION_DRIVER'] ?? 'file'; in_array($sessionDriver, ['file', 'database', 'redis', 'memcached', 'cookie', 'dynamodb']) ? ok("SESSION_DRIVER: {$sessionDriver}") : fail("SESSION_DRIVER: {$sessionDriver} sconosciuto"); // SESSION_DOMAIN if (!empty($env['SESSION_DOMAIN']) && $env['SESSION_DOMAIN'] !== 'null') { $parsedUrl = parse_url($env['APP_URL'] ?? 'http://localhost'); $urlHost = $parsedUrl['host'] ?? 'localhost'; str_contains($env['SESSION_DOMAIN'], $urlHost) ? ok("SESSION_DOMAIN: {$env['SESSION_DOMAIN']}") : warn("SESSION_DOMAIN ({$env['SESSION_DOMAIN']}) non corrisponde a APP_URL ({$urlHost})"); } else { ok("SESSION_DOMAIN: null (usa dominio richiesta)"); } // DB_CONNECTION $dbConnection = $env['DB_CONNECTION'] ?? 'mysql'; in_array($dbConnection, ['mysql', 'mariadb', 'sqlite', 'pgsql', 'sqlsrv']) ? ok("DB_CONNECTION: {$dbConnection}") : fail("DB_CONNECTION: {$dbConnection} sconosciuto"); // APP_DEBUG $appEnv = $env['APP_ENV'] ?? 'production'; $appDebug = ($env['APP_DEBUG'] ?? 'false') === 'true'; if ($appEnv === 'production' && $appDebug) { warn('APP_DEBUG=true in ambiente production', 'Disabilita in produzione: espone errori sensibili'); if ($fixMode) { $groupsToFix['env']['APP_DEBUG'] = 'false'; } } else { ok("APP_DEBUG: " . ($appDebug ? 'true' : 'false') . " in ambiente {$appEnv}"); } // Ensure SESSION_SAME_SITE exists if (!isset($env['SESSION_SAME_SITE'])) { warn('SESSION_SAME_SITE non impostato (default: lax)'); if ($fixMode) { $groupsToFix['env']['SESSION_SAME_SITE'] = 'lax'; v("Aggiunto SESSION_SAME_SITE=lax in attesa fix"); } } if (!isset($env['SESSION_SECURE_COOKIE'])) { warn('SESSION_SECURE_COOKIE non impostato'); if ($fixMode && !isset($groupsToFix['env']['SESSION_SECURE_COOKIE'])) { $groupsToFix['env']['SESSION_SECURE_COOKIE'] = 'false'; v("Aggiunto SESSION_SECURE_COOKIE=false in attesa fix"); } } if (!isset($env['FORCE_HTTPS'])) { warn('FORCE_HTTPS non impostato'); if ($fixMode) { $groupsToFix['env']['FORCE_HTTPS'] = 'false'; v("Aggiunto FORCE_HTTPS=false in attesa fix"); } } if (!isset($env['TRUSTED_PROXIES'])) { warn('TRUSTED_PROXIES non impostato'); if ($fixMode) { $groupsToFix['env']['TRUSTED_PROXIES'] = '"10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"'; v("Aggiunto TRUSTED_PROXIES in attesa fix"); } } // ───────────────────────────────────────────────────────────────────────────── // 3.4 Directory Permissions // ───────────────────────────────────────────────────────────────────────────── heading('Directory & Permessi'); $writeDirs = [ 'storage' => BASE_DIR . '/storage', 'storage/logs' => BASE_DIR . '/storage/logs', 'storage/framework' => BASE_DIR . '/storage/framework', 'storage/framework/sessions' => BASE_DIR . '/storage/framework/sessions', 'storage/framework/views' => BASE_DIR . '/storage/framework/views', 'storage/framework/cache' => BASE_DIR . '/storage/framework/cache', 'storage/framework/cache/data' => BASE_DIR . '/storage/framework/cache/data', 'storage/app/public' => BASE_DIR . '/storage/app/public', 'storage/app/public/logos' => BASE_DIR . '/storage/app/public/logos', 'storage/app/public/documenti/eventi' => BASE_DIR . '/storage/app/public/documenti/eventi', 'storage/app/private' => BASE_DIR . '/storage/app/private', 'bootstrap/cache' => BASE_DIR . '/bootstrap/cache', ]; $missingDirs = []; $unwritableDirs = []; $allDirsOk = true; foreach ($writeDirs as $label => $dir) { if (!file_exists($dir)) { warn("{$label}: directory NON ESISTE"); $missingDirs[] = $dir; $allDirsOk = false; continue; } if (!is_dir($dir)) { fail("{$label}: NON è una directory"); $allDirsOk = false; continue; } $perms = decoct(fileperms($dir) & 0777); $owner = function_exists('posix_getpwuid') ? (posix_getpwuid(fileowner($dir))['name'] ?? (string) fileowner($dir)) : (string) fileowner($dir); if (is_writable($dir)) { ok("{$label}: {$perms} proprietario: {$owner} — SCRIVIBILE"); } elseif ($owner === WEB_USER && in_array($perms, ['775', '777', '755', '770'])) { warn("{$label}: {$perms} proprietario: {$owner} — NON scrivibile da CLI ma WEB server sì"); } else { fail("{$label}: {$perms} proprietario: {$owner} — NON SCRIVIBILE"); $unwritableDirs[] = $dir; $allDirsOk = false; } } if (!$allDirsOk) { if ($fixMode) { if (!empty($missingDirs)) { $groupsToFix['mkdir'] = ($groupsToFix['mkdir'] ?? []); foreach ($missingDirs as $d) { if (!in_array($d, $groupsToFix['mkdir'])) { $groupsToFix['mkdir'][] = $d; } } } if (!empty($unwritableDirs)) { $groupsToFix['chmod'] = ($groupsToFix['chmod'] ?? []); foreach ($unwritableDirs as $d) { if (!in_array($d, $groupsToFix['chmod'])) { $groupsToFix['chmod'][] = $d; } } } $chownDirs = array_unique(array_merge( [BASE_DIR . '/storage', BASE_DIR . '/bootstrap/cache'], $missingDirs, $unwritableDirs )); $groupsToFix['chown'] = $chownDirs; } } // ───────────────────────────────────────────────────────────────────────────── // 3.5 Symlink Check // ───────────────────────────────────────────────────────────────────────────── heading('Symlink'); $publicStorage = BASE_DIR . '/public/storage'; $symlinkOk = true; if (!file_exists($publicStorage) && !is_link($publicStorage)) { fail("public/storage: NON ESISTE", "Esegui: php artisan storage:link"); $symlinkOk = false; } elseif (!is_link($publicStorage)) { fail("public/storage: NON è un symlink (è una directory/file)"); $symlinkOk = false; } else { $readTarget = readlink($publicStorage); $expected = realpath(BASE_DIR . '/storage/app/public'); $resolved = str_starts_with($readTarget, '/') ? realpath($readTarget) : realpath(dirname($publicStorage) . '/' . $readTarget); if ($resolved === $expected) { $displayTarget = str_starts_with($readTarget, '/') ? str_replace(BASE_DIR . '/', '', $readTarget) : $readTarget; ok("public/storage → {$displayTarget} (puntamento corretto)"); } else { fail("public/storage → {$readTarget}", "Dovrebbe puntare a: " . str_replace(BASE_DIR . '/', '', $expected)); $symlinkOk = false; } } if (!$symlinkOk && $fixMode) { $groupsToFix['symlink'] = true; } // .gitignore in storage/app/public $gitignorePath = BASE_DIR . '/storage/app/public/.gitignore'; if (!file_exists($gitignorePath)) { warn('.gitignore mancante in storage/app/public (serve a Laravel)'); if ($fixMode) { $groupsToFix['gitignore'] = true; } } else { ok(".gitignore presente in storage/app/public"); } // ───────────────────────────────────────────────────────────────────────────── // 3.6 Session Test // ───────────────────────────────────────────────────────────────────────────── heading('Sessione — Test scrittura/lettura'); $sessionDir = BASE_DIR . '/storage/framework/sessions'; if (!is_dir($sessionDir)) { fail("Directory session NON ESISTE: {$sessionDir}"); } elseif (!is_writable($sessionDir)) { fail("Directory session NON scrivibile — test saltato"); } else { // Show session files count and first files with permissions $sessionFiles = glob($sessionDir . '/*'); $fileCount = count($sessionFiles); if ($fileCount > 0) { $exampleFiles = array_slice($sessionFiles, 0, 3); $fileInfo = []; foreach ($exampleFiles as $f) { $fileInfo[] = basename($f) . ' (' . decoct(fileperms($f) & 0777) . ')'; } ok("Session files presenti: {$fileCount} (esempi: " . implode(', ', $fileInfo) . "...)"); } else { ok("Session files: nessuno (normale su installazione pulita)"); } $testFile = $sessionDir . '/.diagnose_test_' . getmypid(); $testVal = 'diagnose_' . bin2hex(random_bytes(8)); $written = file_put_contents($testFile, $testVal); if ($written === false) { fail("Scrittura file sessione: FALLITA", $testFile); } else { $read = file_get_contents($testFile); unlink($testFile); ($read === $testVal) ? ok("Scrittura/lettura file sessione: OK") : fail("Scrittura/lettura file sessione: DATI NON CORRISPONDONO", "Letto: {$read}, atteso: {$testVal}"); } } // ───────────────────────────────────────────────────────────────────────────── // 3.7 Database Connectivity // ───────────────────────────────────────────────────────────────────────────── heading('Database'); $dbDriver = $env['DB_CONNECTION'] ?? 'mysql'; $dbHost = $env['DB_HOST'] ?? '127.0.0.1'; $dbPort = $env['DB_PORT'] ?? '3306'; $dbName = $env['DB_DATABASE'] ?? ''; $dbUser = $env['DB_USERNAME'] ?? ''; $dbPass = $env['DB_PASSWORD'] ?? ''; if (empty($dbName)) { fail("DB_DATABASE non impostato nel .env"); } elseif ($dbDriver === 'sqlite') { $sqlitePath = $env['DB_DATABASE_SQLITE'] ?? 'database/database.sqlite'; $absPath = str_starts_with($sqlitePath, '/') ? $sqlitePath : BASE_DIR . '/' . $sqlitePath; if (file_exists($absPath)) { ok("SQLite database trovato: {$sqlitePath} (" . decoct(fileperms($absPath) & 0777) . ")"); } else { fail("SQLite database NON TROVATO: {$sqlitePath}", "Cercato in: {$absPath}"); } } else { $dsn = "{$dbDriver}:host={$dbHost};port={$dbPort};dbname={$dbName};charset=utf8mb4"; v("Tentativo connessione: {$dsn} user={$dbUser}"); try { $pdo = new PDO($dsn, $dbUser, $dbPass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5, ]); $serverInfo = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); $driverInfo = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); ok("Connessione DB: OK (driver: {$driverInfo}, versione: {$serverInfo})"); // Tabelle $stmt = $pdo->query("SHOW TABLES"); $tables = $stmt->fetchAll(PDO::FETCH_COLUMN); $expectedTables = ['individui', 'gruppi', 'eventi', 'users', 'tags', 'mailing_lists', 'documenti']; $foundTables = array_intersect($expectedTables, $tables); $missingTables = array_diff($expectedTables, $tables); if (empty($missingTables)) { ok("Tabelle principali: tutte presenti (" . implode(', ', $foundTables) . ")"); } else { warn("Tabelle mancanti: " . implode(', ', $missingTables), "Esegui: php artisan migrate"); } // Record counts foreach (['users', 'individui', 'gruppi', 'eventi', 'tags'] as $table) { if (in_array($table, $tables, true)) { $count = $pdo->query("SELECT COUNT(*) FROM `{$table}`")->fetchColumn(); ok("Tabella {$table}: {$count} record"); } } // Engine check try { $engines = $pdo->query("SHOW TABLE STATUS WHERE Engine IS NOT NULL")->fetchAll(PDO::FETCH_ASSOC); $nonInnoDb = []; foreach ($engines as $row) { if (($row['Engine'] ?? '') !== 'InnoDB' && ($row['Engine'] ?? '') !== '') { $nonInnoDb[] = $row['Name'] . ' (' . ($row['Engine'] ?? 'unknown') . ')'; } } empty($nonInnoDb) ? ok("Engine: tutte le tabelle usano InnoDB") : warn("Engine non-InnoDB: " . implode(', ', $nonInnoDb)); } catch (PDOException $e) { v("SHOW TABLE STATUS non supportato"); } } catch (PDOException $e) { $errorSafe = str_replace($dbPass, '****', $e->getMessage()); fail("Connessione DB FALLITA", $errorSafe); } if (!extension_loaded('pdo_mysql') && $dbDriver === 'mysql') { fail("PDO MySQL non installato", "Installa: apt install php-mysql"); } } // ───────────────────────────────────────────────────────────────────────────── // 3.8 Bootstrap/app.php middleware // ───────────────────────────────────────────────────────────────────────────── heading('Configurazione Middleware (bootstrap/app.php)'); $appPath = BASE_DIR . '/bootstrap/app.php'; if (!file_exists($appPath)) { fail("bootstrap/app.php NON TROVATO"); } else { $appContent = file_get_contents($appPath); $warned = false; $forceHttpsRegistered = preg_match('/\$middleware\s*->\s*\w+\s*\(.*?ForceHttps::class/s', $appContent) === 1; if ($forceHttpsRegistered) { ok("ForceHttps middleware: registrato correttamente"); } elseif (str_contains($appContent, 'ForceHttps')) { warn("ForceHttps middleware: IMPORTATO ma NON registrato in alcun gruppo middleware", "Aggiungi \$middleware->append(ForceHttps::class) se necessario"); } else { warn("ForceHttps middleware: NON presente"); } if (str_contains($appContent, 'trustProxies')) { ok("TrustProxies: configurato"); } else { warn("TrustProxies: NON configurato", "Può causare problemi con HTTPS dietro proxy"); } // Duplicated web middleware $webAppend = []; if (preg_match('/\$middleware->web\(append:\s*\[(.+?)\]/s', $appContent, $m)) { preg_match_all('/([a-zA-Z]+)::class/', $m[1], $classMatches); $webAppend = $classMatches[1] ?? []; } $defaultWeb = ['EncryptCookies', 'AddQueuedCookiesToResponse', 'StartSession', 'ShareErrorsFromSession']; foreach ($defaultWeb as $mw) { if (in_array($mw, $webAppend)) { warn("Middleware duplicato in web(append): {$mw}", "Già nel default stack. Può causare 419."); $warned = true; } } if (!$warned) { ok("Middleware web group: nessuna duplicazione"); } } // ───────────────────────────────────────────────────────────────────────────── // 3.9 Vendor & Autoload // ───────────────────────────────────────────────────────────────────────────── heading('Vendor & Autoload'); if (file_exists(BASE_DIR . '/vendor/autoload.php')) { ok("vendor/autoload.php: presente"); is_dir(BASE_DIR . '/vendor/composer') ? ok("composer: installato") : fail("vendor/composer/ mancante", "Esegui: composer install"); } else { fail("vendor/autoload.php NON TROVATO", "Esegui: composer install --no-dev"); } is_dir(BASE_DIR . '/node_modules') ? ok("node_modules: presente") : warn("node_modules: assente", "npm install (non serve in prod se public/build/ è presente)"); is_dir(BASE_DIR . '/public/build') ? ok("public/build: presente (asset Vite compilati)") : warn("public/build: assente", "Esegui: npm run build"); if (file_exists(BASE_DIR . '/artisan')) { ok("artisan: presente"); if (!is_executable(BASE_DIR . '/artisan')) { warn("artisan: NON eseguibile", "chmod +x artisan"); } } else { fail("artisan NON TROVATO"); } // ───────────────────────────────────────────────────────────────────────────── // 3.10 CSRF Test — Duale HTTP + HTTPS // ───────────────────────────────────────────────────────────────────────────── heading('Test CSRF (login HTTP + HTTPS)'); function testCsrfLogin(string $url, string $label): ?int { global $verbose; // Note: CURLOPT_COOKIEJAR is broken in this environment (curl 8.14.1 / PHP 8.4). // We manually parse Set-Cookie headers and send them back. // GET login page $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_HEADER => true, ]); $resp = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_error($ch); $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $body = is_string($resp) ? substr($resp, $headerSize) : ''; $headersRaw = is_string($resp) ? substr($resp, 0, $headerSize) : ''; curl_close($ch); if ($resp === false || $body === '') { return null; } // Extract CSRF token from HTML $token = null; if (preg_match('/]*name=["\']_token["\'][^>]*value=["\']([^"\']+)["\']/i', $body, $m)) { $token = $m[1]; } else { return -1; } v("{$label}: token: " . substr($token, 0, 20) . '...'); // Parse all Set-Cookie headers to build a cookie string $cookies = []; if (preg_match_all('/^Set-Cookie:\s*([^;]+)/mi', $headersRaw, $cookieMatches)) { foreach ($cookieMatches[1] as $cookieStr) { $cookies[] = trim($cookieStr); } } $cookieHeader = !empty($cookies) ? implode('; ', $cookies) : ''; v("{$label}: cookies: " . ($cookieHeader ? substr($cookieHeader, 0, 80) . '...' : 'none')); // POST login with manually extracted cookies $ch2 = curl_init($url); $curlOpts = [ CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_HEADER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ '_token' => $token, 'email' => 'test@diagnose.local', 'password' => 'wrong', ]), ]; if ($cookieHeader !== '') { $curlOpts[CURLOPT_COOKIE] = $cookieHeader; } curl_setopt_array($ch2, $curlOpts); $resp2 = curl_exec($ch2); $postCode = curl_getinfo($ch2, CURLINFO_HTTP_CODE); curl_close($ch2); if ($postCode !== 302 && $postCode !== 200) { v("{$label}: POST returned HTTP {$postCode}"); } return $postCode; } $baseLoginUrl = rtrim($baseUrl ?: 'http://localhost', '/') . '/login'; $httpsLoginUrl = $baseLoginUrl; $httpLoginUrl = str_replace('https://', 'http://', $baseLoginUrl); $hasHttps = str_starts_with($baseLoginUrl, 'https://'); $httpsResult = null; $httpResult = null; if ($hasHttps) { echo " " . COLOR_BOLD . "Test HTTPS:" . COLOR_RESET . " {$httpsLoginUrl}\n"; $httpsResult = testCsrfLogin($httpsLoginUrl, 'HTTPS'); } echo " " . COLOR_BOLD . "Test HTTP:" . COLOR_RESET . " {$httpLoginUrl}\n"; $httpResult = testCsrfLogin($httpLoginUrl, 'HTTP'); echo "\n"; if ($hasHttps) { if ($httpsResult === null) { fail("HTTPS: Login page non raggiungibile"); } elseif ($httpsResult === -1) { fail("HTTPS: CSRF token non trovato nella pagina"); } elseif ($httpsResult === 419) { fail("HTTPS: POST login → 419 Page Expired", "Token non riconosciuto. Sessione non persistente o SESSION_SECURE_COOKIE errato."); } elseif ($httpsResult === 302 || $httpsResult === 200) { ok("HTTPS: POST login → HTTP {$httpsResult} (nessun 419)"); } else { warn("HTTPS: POST login → HTTP {$httpsResult} (inaspettato)"); } } if ($httpResult === null) { fail("HTTP: Login page non raggiungibile"); } elseif ($httpResult === -1) { fail("HTTP: CSRF token non trovato nella pagina"); } elseif ($httpResult === 419) { fail("HTTP: POST login → 419 Page Expired", "Token non riconosciuto. Sessione non persistente o SESSION_SECURE_COOKIE errato."); } elseif ($httpResult === 302 || $httpResult === 200) { ok("HTTP: POST login → HTTP {$httpResult} (nessun 419)"); } else { warn("HTTP: POST login → HTTP {$httpResult} (inaspettato)"); } if ($hasHttps && $httpsResult !== null && $httpsResult !== 419 && $httpResult === 419) { warn("HTTPS funziona ma HTTP dà 419", "SESSION_SECURE_COOKIE=true blocca il cookie su HTTP. Imposta SESSION_SECURE_COOKIE=false se serve HTTP."); if ($fixMode) { $groupsToFix['env']['SESSION_SECURE_COOKIE'] = 'false'; } } // Local IP CSRF test — verifica accesso via IP di rete locale (diverso da APP_URL hostname) $localIp = null; $hostnameI = trim((string) exec('hostname -I 2>/dev/null')); if ($hostnameI !== '') { $parts = explode(' ', $hostnameI); $localIp = $parts[0]; } if ($localIp && $localIp !== parse_url($baseUrl, PHP_URL_HOST)) { $localLoginUrl = "http://{$localIp}/login"; echo " " . COLOR_BOLD . "Test Locale IP:" . COLOR_RESET . " {$localLoginUrl}\n"; $localResult = testCsrfLogin($localLoginUrl, 'LOCAL IP'); echo "\n"; if ($localResult === null) { fail("LOCAL IP: Login page non raggiungibile su {$localIp}"); } elseif ($localResult === -1) { fail("LOCAL IP: CSRF token non trovato nella pagina su {$localIp}"); } elseif ($localResult === 419) { fail("LOCAL IP: POST login → 419 Page Expired", "Sessione non funzionante su IP locale. Verifica SESSION_DRIVER, permessi storage o cookie domain."); } elseif ($localResult === 302 || $localResult === 200) { ok("LOCAL IP: POST login → HTTP {$localResult} (nessun 419)"); } else { warn("LOCAL IP: POST login → HTTP {$localResult} (inaspettato)"); } } // ───────────────────────────────────────────────────────────────────────────── // 3.11 Disk Space // ───────────────────────────────────────────────────────────────────────────── heading('Spazio Disco'); $total = disk_total_space(BASE_DIR); $free = disk_free_space(BASE_DIR); $used = $total - $free; $pctUsed = round(($used / $total) * 100, 1); ok("Spazio totale: " . formatBytes($total)); ok("Spazio libero: " . formatBytes($free)); if ($pctUsed > 90) { fail("Spazio utilizzato: {$pctUsed}% — CRITICO"); } elseif ($pctUsed > 80) { warn("Spazio utilizzato: {$pctUsed}%"); } else { ok("Spazio utilizzato: {$pctUsed}%"); } // ───────────────────────────────────────────────────────────────────────────── // 4. Apply fixes // ───────────────────────────────────────────────────────────────────────────── if ($fixMode) { runFixes($groupsToFix); } // ───────────────────────────────────────────────────────────────────────────── // 5. Summary // ───────────────────────────────────────────────────────────────────────────── heading('Riepilogo'); echo "\n"; echo " " . COLOR_GREEN . "✅ Passed: {$results['passed']}" . COLOR_RESET . "\n"; echo " " . COLOR_YELLOW . "⚠️ Warnings: {$results['warnings']}" . COLOR_RESET . "\n"; echo " " . COLOR_RED . "❌ Failed: {$results['failed']}" . COLOR_RESET . "\n"; if ($results['fixed'] > 0) { echo " " . COLOR_GREEN . "🔧 Fixed: {$results['fixed']}" . COLOR_RESET . "\n"; } echo "\n"; if ($results['failed'] > 0) { echo COLOR_BOLD . COLOR_RED . " ⛔ PROBLEMI CRITICI TROVATI." . COLOR_RESET . "\n"; if (!$fixMode) { echo " Per tentare la riparazione automatica: " . COLOR_BOLD . "php diagnose.php --fix" . COLOR_RESET . "\n"; } echo "\n"; exit(1); } elseif ($results['warnings'] > 0) { echo COLOR_BOLD . COLOR_YELLOW . " ⚠️ Warning presenti — Verificare i punti segnalati." . COLOR_RESET . "\n\n"; exit(0); } else { echo COLOR_BOLD . COLOR_GREEN . " ✅ Tutti i controlli superati. Installazione OK." . COLOR_RESET . "\n\n"; exit(0); }