Files
glastree/diagnose.php
T
2026-06-23 08:05:51 +02:00

1163 lines
46 KiB
PHP

<?php
/**
* diagnose.php — Verifica e ripara installazione Glastree (Laravel 13)
*
* Esecuzione:
* php diagnose.php # solo diagnostica
* php diagnose.php --verbose # output dettagliato
* php diagnose.php --fix # diagnostica + tenta riparazione automatica
*
* Non richiede Laravel. Legge .env direttamente e testa DB via PDO.
*/
declare(strict_types=1);
define('BASE_DIR', __DIR__);
define('WEB_USER', 'www-data');
const COLOR_GREEN = "\033[32m";
const COLOR_RED = "\033[31m";
const COLOR_YELLOW= "\033[33m";
const COLOR_CYAN = "\033[36m";
const COLOR_BOLD = "\033[1m";
const COLOR_RESET = "\033[0m";
$verbose = false;
$fixMode = false;
foreach ($argv ?? [] as $arg) {
if ($arg === '--verbose') { $verbose = true; }
if ($arg === '--fix') { $fixMode = true; }
}
$results = ['passed' => 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('/<input[^>]*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);
}