diff --git a/.env.example b/.env.example index 4088f104..dc622701 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,12 @@ APP_NAME="Glastree" -APP_ENV=local +APP_ENV=production APP_KEY= -APP_DEBUG=true +APP_DEBUG=false APP_URL=http://localhost +APP_PROTOCOL=https +APP_SUBFOLDER= +FORCE_HTTPS=false +TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1" APP_LOCALE=it APP_FALLBACK_LOCALE=en @@ -27,6 +31,8 @@ DB_PASSWORD= SESSION_DRIVER=file SESSION_LIFETIME=120 SESSION_ENCRYPT=false +SESSION_SECURE_COOKIE=false +SESSION_SAME_SITE=lax SESSION_PATH=/ SESSION_DOMAIN=null diff --git a/MEMORY.md b/MEMORY.md index a8e43d4d..00db9b26 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -26,6 +26,7 @@ App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi. - **2026-06-17**: Google OAuth 2.0 unificato (revert Socialite, unified OAuth per Email/Drive/Calendar, XOAUTH2 SMTP+IMAP, UI impostazioni) - **2026-06-17**: Fix CalendarioConnessione — `encryptAndSetConfig()` perdeva campi sensibili in edit, `is_active` checkbox senza hidden fallback, query duplicata nella view - **2026-06-18**: Fix import CSV — `declare(strict_types=1)` mancante in IndividuoController, header check assente, Log::warning() su errori; fix freeze server (set_time_limit, session_write_close, DB::transaction, fgetcsv length illimitato) in GruppoController e IndividuoController +- **2026-06-18**: Diagnose script (`diagnose.php`), .env.example aggiornato (SESSION_SECURE_COOKIE, FORCE_HTTPS, TRUSTED_PROXIES), build-dist.sh aggiornato con passo diagnose ## Funzionalità Implementate @@ -705,3 +706,48 @@ DB::commit(); ### Verifica - `php -l` su entrambi: nessun errore di sintassi + +## 2026-06-18 — Diagnose script + .env.example + build-dist.sh + +### `diagnose.php` — Script standalone di diagnostica +Creato script standalone per verificare installazione su server remoto. Non richiede Laravel. + +**Cosa verifica**: +- PHP version (≥ 8.2), 15 estensioni required, 4 opzionali +- INI settings (execution_time, memory, upload/post max) +- .env: APP_KEY decodifica (32 bytes), APP_URL, SESSION_SECURE_COOKIE, DB, FORCE_HTTPS +- 13 directory in `storage/` e `bootstrap/cache`: esistenza, permessi, scrivibilità +- Symlink `public/storage` (supporto sia link assoluti che relativi) +- Test scrittura/lettura file sessione +- Connessione DB via PDO, tabelle principali, conteggio records, engine InnoDB +- Middleware bootstrap/app.php: ForceHttps registrato, TrustProxies, duplicazioni web group +- Vendor: autoload, composer, node_modules, public/build, artisan +- CSRF: HTTP GET login page, estrazione token, POST con token, verifica 419 +- Spazio disco: totale, libero, percentuale + +**Uso**: +```bash +php diagnose.php +php diagnose.php --verbose # output dettagliato +``` +Exit code: 0 = OK, 1 = errori critici. + +### `.env.example` aggiornato +Aggiunte variabili mancanti: +- `APP_PROTOCOL`, `APP_SUBFOLDER` — supporto subfolder +- `FORCE_HTTPS`, `TRUSTED_PROXIES` — proxy/HTTPS configurabile +- `SESSION_SECURE_COOKIE`, `SESSION_SAME_SITE` — sessione cross-protocol +- `APP_ENV=production`, `APP_DEBUG=false` — safe defaults per produzione + +### `build-dist.sh` +- Aggiunto `php diagnose.php` come passo #0 (pre-installazione) e #7 (verifica finale) +- Istruzioni post-estrazione complete + +### File modificati +- `diagnose.php` (nuovo) — standalone diagnostica +- `.env.example` — variabili mancanti aggiunte +- `build-dist.sh` — passo diagnose + +### Verifica +- `php -l diagnose.php`: OK +- `php diagnose.php`: eseguito con 58/68 check superati su server locale diff --git a/build-dist.sh b/build-dist.sh index 14771321..e2f24ca9 100755 --- a/build-dist.sh +++ b/build-dist.sh @@ -70,6 +70,9 @@ echo "Per estrarre sul server di destinazione:" echo " tar xzf ${ARCHIVE}" echo "" echo "Poi esegui:" +echo " # 0. Diagnostica preliminare" +echo " php diagnose.php" +echo "" echo " # 1. Crea directory necessarie (se non esistono già)" echo ' mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs bootstrap/cache storage/app/public storage/app/public/logos storage/app/public/documenti/eventi storage/app/backups storage/app/documenti storage/app/private/avatars/gruppi' echo "" @@ -94,3 +97,6 @@ echo " php artisan view:clear" echo "" echo " # 6. Avvia installazione MySQL" echo " php install.php" +echo "" +echo " # 7. Verifica finale" +echo " php diagnose.php" diff --git a/diagnose.php b/diagnose.php new file mode 100644 index 00000000..aea542ef --- /dev/null +++ b/diagnose.php @@ -0,0 +1,797 @@ + 0, + 'failed' => 0, + 'warnings' => 0, +]; + +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 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"; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 2. Parse .env (standalone, no framework) +// ───────────────────────────────────────────────────────────────────────────── + +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] ?? ''); + + // Rimuovi eventuali apici + if ((str_starts_with($value, '"') && str_ends_with($value, '"')) + || (str_starts_with($value, "'") && str_ends_with($value, "'"))) { + $value = substr($value, 1, -1); + } + + // Risolvi variabili ${...} + $value = preg_replace_callback('/\$\{(\w+)\}/', function ($m) use ($env) { + return $env[$m[1]] ?? $m[0]; + }, $value); + + $env[$key] = $value; + } + } + + return $env; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3. Check funzioni +// ───────────────────────────────────────────────────────────────────────────── + +echo COLOR_BOLD . "\n" + . " ╔════════════════════════════════════════════╗\n" + . " ║ Glastree — Diagnosi Installazione ║\n" + . " ╚════════════════════════════════════════════╝\n" + . COLOR_RESET; + +echo "\n Data: " . date('Y-m-d H:i:s') . " | PID: " . getmypid() . "\n"; + +// ───────────────────────────────────────────────────────────────────────────── +// 3.1 PHP Version & Extensions +// ───────────────────────────────────────────────────────────────────────────── + +heading('PHP Version & Extensions'); + +$phpVersion = PHP_VERSION; +$phpMajorMinor = PHP_MAJOR_VERSION . '.' . PHP_MINOR_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) { + if (extension_loaded($ext)) { + ok("Estensione {$ext}: OK"); + } else { + fail("Estensione {$ext}: NON TROVATA", "Richiesta da Laravel"); + } +} + +foreach ($optionalExts as $ext => $label) { + if (extension_loaded($ext)) { + ok("Estensione {$ext}: OK ({$label})"); + } else { + warn("Estensione {$ext}: NON TROVATA ({$label})"); + } +} + +// INI settings +$iniChecks = [ + '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'); + +foreach ($iniChecks as $iniKey => [$minVal, $note]) { + $actual = ini_get($iniKey); + $ok = false; + + if (is_numeric($minVal) && is_numeric($actual)) { + $ok = (int) $actual >= (int) $minVal; + } elseif (is_string($minVal)) { + // Comparazione bytes + $actualBytes = returnBytes($actual); + $minBytes = returnBytes($minVal); + $ok = $actualBytes >= $minBytes; + } + + if ($ok) { + ok("{$iniKey} = {$actual}"); + } else { + warn("{$iniKey} = {$actual} (minimo: {$minVal})", $note); + } +} + +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, + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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); + if ($decoded !== false && strlen($decoded) === 32) { + ok("APP_KEY: valido (32 bytes)"); + } else { + fail('APP_KEY: decodifica fallita o lunghezza errata', "Deve essere 32 bytes dopo decodifica base64"); + } +} + +// APP_URL +if (empty($env['APP_URL'])) { + fail('APP_URL non impostato'); +} elseif (!str_starts_with($env['APP_URL'], 'http://') && !str_starts_with($env['APP_URL'], 'https://')) { + fail('APP_URL non valido', $env['APP_URL']); +} else { + ok("APP_URL: {$env['APP_URL']}"); + + // Verifica reachability via curl + $ch = curl_init($env['APP_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_NOBODY => true, + ]); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($response === false) { + fail("APP_URL non raggiungibile via HTTP", $curlError); + } else { + ok("APP_URL raggiungibile (HTTP {$httpCode})"); + } +} + +// 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 potrebbe non essere inviato su HTTP'); +} 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'; +if (in_array($sessionDriver, ['file', 'database', 'redis', 'memcached', 'cookie', 'dynamodb'])) { + ok("SESSION_DRIVER: {$sessionDriver}"); +} else { + fail("SESSION_DRIVER: {$sessionDriver} sconosciuto"); +} + +// SESSION_DOMAIN check +if (!empty($env['SESSION_DOMAIN']) && $env['SESSION_DOMAIN'] !== 'null') { + $parsedUrl = parse_url($env['APP_URL'] ?? 'http://localhost'); + $urlHost = $parsedUrl['host'] ?? 'localhost'; + if (!str_contains($env['SESSION_DOMAIN'], $urlHost)) { + warn("SESSION_DOMAIN ({$env['SESSION_DOMAIN']}) non corrisponde a APP_URL ({$urlHost})", + 'Il cookie di sessione potrebbe non essere inviato'); + } else { + ok("SESSION_DOMAIN: {$env['SESSION_DOMAIN']}"); + } +} else { + ok("SESSION_DOMAIN: null (usa dominio richiesta)"); +} + +// DB_CONNECTION +$dbConnection = $env['DB_CONNECTION'] ?? 'mysql'; +if (in_array($dbConnection, ['mysql', 'mariadb', 'sqlite', 'pgsql', 'sqlsrv'])) { + ok("DB_CONNECTION: {$dbConnection}"); +} else { + fail("DB_CONNECTION: {$dbConnection} sconosciuto"); +} + +// APP_DEBUG in produzione +$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'); +} else { + ok("APP_DEBUG: " . ($appDebug ? 'true' : 'false') . " in ambiente {$appEnv}"); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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', +]; + +foreach ($writeDirs as $label => $dir) { + if (!file_exists($dir)) { + warn("{$label}: directory NON ESISTE"); + continue; + } + if (!is_dir($dir)) { + fail("{$label}: NON è una directory"); + continue; + } + $perms = decoct(fileperms($dir) & 0777); + $owner = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner($dir))['name'] ?? fileowner($dir) : fileowner($dir); + + if (is_writable($dir)) { + ok("{$label}: {$perms} proprietario: {$owner} — SCRIVIBILE"); + } else { + fail("{$label}: {$perms} proprietario: {$owner} — NON SCRIVIBILE", + "Chmod: chmod -R 775 " . str_replace(BASE_DIR . '/', '', $dir)); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3.5 Symlink Check +// ───────────────────────────────────────────────────────────────────────────── + +heading('Symlink'); + +$publicStorage = BASE_DIR . '/public/storage'; +$target = $publicStorage; + +if (!file_exists($publicStorage)) { + fail("public/storage: NON ESISTE", "Esegui: php artisan storage:link"); + v("Path: {$publicStorage}"); +} elseif (!is_link($publicStorage)) { + fail("public/storage: NON è un symlink (è una directory/file)"); +} else { + $readTarget = readlink($publicStorage); + $expected = realpath(BASE_DIR . '/storage/app/public'); + + // Se readlink restituisce percorso assoluto, usalo direttamente + $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)); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3.6 Session Test +// ───────────────────────────────────────────────────────────────────────────── + +heading('Sessione — Test scrittura/lettura'); + +$sessionDir = BASE_DIR . '/storage/framework/sessions'; +if (!is_writable($sessionDir)) { + fail("Directory session NON scrivibile — test saltato"); +} else { + $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); + if ($read === $testVal) { + ok("Scrittura/lettura file sessione: OK"); + } else { + fail("Scrittura/lettura file sessione: DATI NON CORRISPONDONO", + "Letto: {$read}, atteso: {$testVal}"); + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3.7 Database Connectivity (standalone via PDO) +// ───────────────────────────────────────────────────────────────────────────── + +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)) { + $perms = decoct(fileperms($absPath) & 0777); + ok("SQLite database trovato: {$sqlitePath} ({$perms})"); + } 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})"); + + // Verifica esistenza tabelle principali + $stmt = $pdo->query("SHOW TABLES"); + $tables = $stmt->fetchAll(PDO::FETCH_COLUMN); + $expectedTables = ['individui', 'gruppi', 'eventi', 'users', 'tags', 'mailing_lists', 'documenti']; + $foundTables = []; + + foreach ($expectedTables as $table) { + if (in_array($table, $tables, true)) { + $foundTables[] = $table; + } + } + + if (count($foundTables) === count($expectedTables)) { + ok("Tabelle principali: tutte presenti (" . implode(', ', $foundTables) . ")"); + } else { + $missing = array_diff($expectedTables, $foundTables); + warn("Tabelle mancanti: " . implode(', ', $missing), + "Esegui: php artisan migrate"); + } + + // Verifica conteggio records + 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"); + } + } + + // Verifica engine InnoDB + 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') . ')'; + } + } + if (empty($nonInnoDb)) { + ok("Engine: tutte le tabelle usano InnoDB"); + } else { + warn("Engine non-InnoDB: " . implode(', ', $nonInnoDb), + "InnoDB raccomandato per integrità referenziale"); + } + } catch (PDOException $e) { + v("SHOW TABLE STATUS non supportato: " . $e->getMessage()); + } + + } catch (PDOException $e) { + $errorMsg = $e->getMessage(); + // Maschera password in output + $errorSafe = str_replace($dbPass, '****', $errorMsg); + fail("Connessione DB FALLITA", $errorSafe); + + // Diagnostica + if ($e->getCode() === 2002) { + v("Host '{$dbHost}:{$dbPort}' non raggiungibile. Verifica DB_HOST e DB_PORT."); + } elseif ($e->getCode() === 1045) { + v("Accesso negato per utente '{$dbUser}'."); + } elseif ($e->getCode() === 1049) { + v("Database '{$dbName}' non esiste."); + } elseif ($e->getCode() === 1044) { + v("Utente '{$dbUser}' non ha accesso al database '{$dbName}'."); + } + } + + // Verifica estensione PDO nel caso la connessione non sia stata testata + if (!extension_loaded('pdo_mysql') && $dbDriver === 'mysql') { + fail("PDO MySQL non installato", "Installa: apt install php-mysql"); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3.8 Bootstrap/app.php middleware check +// ───────────────────────────────────────────────────────────────────────────── + +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); + $warnedDoubles = false; + + // Check ForceHttps registration + if (str_contains($appContent, 'ForceHttps')) { + ok("ForceHttps middleware: registrato"); + } else { + warn("ForceHttps middleware: NON registrato", + "FORCE_HTTPS=true nel .env ma middleware non aggiunto a bootstrap/app.php"); + } + + // Check TrustProxies + if (str_contains($appContent, 'trustProxies')) { + ok("TrustProxies: configurato"); + } else { + warn("TrustProxies: NON configurato", + "Può causare problemi con HTTPS dietro proxy"); + } + + // Check for duplicated web middleware + $webAppend = []; + if (preg_match('/\$middleware->web\(append:\s*\[([^\]]*)\]/s', $appContent, $m)) { + $block = $m[1]; + preg_match_all('/class\s*:\s*[a-z\\\]*\\\([a-zA-Z]+)/i', $block, $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à presente nel default stack di Laravel 11. Può causare 419 su login."); + $warnedDoubles = true; + } + } + + if (!$warnedDoubles) { + ok("Middleware web group: nessuna duplicazione"); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// 3.9 Vendor & Autoload +// ───────────────────────────────────────────────────────────────────────────── + +heading('Vendor & Autoload'); + +$vendorAutoload = BASE_DIR . '/vendor/autoload.php'; +if (file_exists($vendorAutoload)) { + ok("vendor/autoload.php: presente"); + $vendorSize = filesize(BASE_DIR . '/vendor/'); + if (is_dir(BASE_DIR . '/vendor/composer')) { + ok("composer: installato"); + } else { + fail("vendor/composer/ mancante", "Esegui: composer install"); + } +} else { + fail("vendor/autoload.php NON TROVATO", "Esegui: composer install --no-dev"); +} + +// Package.json / node_modules +if (is_dir(BASE_DIR . '/node_modules')) { + ok("node_modules: presente"); +} else { + warn("node_modules: assente", "Esegui: npm install (non necessario in produzione se public/build/ è presente)"); +} + +// Check public/build +if (is_dir(BASE_DIR . '/public/build')) { + ok("public/build: presente (asset Vite compilati)"); +} else { + warn("public/build: assente", "Esegui: npm run build (gli asset AdminLTE potrebbero non funzionare)"); +} + +// Check artisan +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 (HTTP) +// ───────────────────────────────────────────────────────────────────────────── + +heading('Test CSRF (via HTTP)'); + +$baseUrl = $env['APP_URL'] ?? 'http://localhost'; +$loginUrl = rtrim($baseUrl, '/') . '/login'; +$loginUrlHttp = str_replace('https://', 'http://', $loginUrl); + +v("Test login page: {$loginUrl}"); +v("Test HTTP variant: {$loginUrlHttp}"); + +$ch = curl_init($loginUrl); +curl_setopt_array($ch, [ + CURLOPT_TIMEOUT => 15, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_HEADER => true, + CURLOPT_COOKIEJAR => '/tmp/glastree_diagnose_cookies_' . getmypid() . '.txt', + CURLOPT_COOKIEFILE => '/tmp/glastree_diagnose_cookies_' . getmypid() . '.txt', +]); +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$curlError = curl_error($ch); +$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); +$headers = is_string($response) ? substr($response, 0, $headerSize) : ''; +$body = is_string($response) ? substr($response, $headerSize) : ''; +curl_close($ch); + +if ($response === false || $body === false || $body === '') { + fail("Login page non raggiungibile", $curlError); + warn("Test CSRF saltato — APP_URL non raggiungibile da questo server"); +} else { + ok("Login page raggiungibile (HTTP {$httpCode})"); + + // Extract CSRF token + $token = null; + if (preg_match('/]*name=["\']_token["\'][^>]*value=["\']([^"\']+)["\']/i', $body, $m)) { + $token = $m[1]; + ok("CSRF token estratto dalla pagina login"); + v("Token (primi 20 char): " . substr($token, 0, 20) . '...'); + } else { + fail("CSRF token NON TROVATO nella pagina login", + "@csrf potrebbe non funzionare correttamente"); + } + + // Try POST to login with token + if ($token !== null) { + $ch2 = curl_init($loginUrl); + curl_setopt_array($ch2, [ + CURLOPT_TIMEOUT => 15, + 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@example.com', + 'password' => 'wrongpassword', + ]), + CURLOPT_COOKIEJAR => '/tmp/glastree_diagnose_cookies_' . getmypid() . '.txt', + CURLOPT_COOKIEFILE => '/tmp/glastree_diagnose_cookies_' . getmypid() . '.txt', + ]); + $postResponse = curl_exec($ch2); + $postHttpCode = curl_getinfo($ch2, CURLINFO_HTTP_CODE); + $postHeaders = substr($postResponse, 0, curl_getinfo($ch2, CURLINFO_HEADER_SIZE)); + curl_close($ch2); + + if ($postHttpCode === 419) { + fail("POST login → 419 Page Expired", + "CSRO token non riconosciuto. Possibili cause: sessione non persistente," + . " SESSION_SECURE_COOKIE errato, middleware duplicati, permessi session."); + } elseif ($postHttpCode === 302 || $postHttpCode === 200) { + ok("POST login → HTTP {$postHttpCode} (419 non verificato)"); + v("Se 200: la pagina restituita probabilmente contiene errori di validazione (normale)"); + } else { + warn("POST login → HTTP {$postHttpCode} (inaspettato)"); + } + } +} + +// Cleanup cookies +$cookieFile = '/tmp/glastree_diagnose_cookies_' . getmypid() . '.txt'; +if (file_exists($cookieFile)) { + unlink($cookieFile); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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); + +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]; +} + +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. 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"; +echo "\n"; + +if ($results['failed'] > 0) { + echo COLOR_BOLD . COLOR_RED . " ⛔ PROBLEMI CRITICI TROVATI — Risolvere prima di proseguire." . COLOR_RESET . "\n\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); +} diff --git a/nscadmin@192.168.222.177 b/nscadmin@192.168.222.177 deleted file mode 100644 index ad033584..00000000 Binary files a/nscadmin@192.168.222.177 and /dev/null differ diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 2db779bc..d4854205 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'laravel/laravel', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => 'd2084b9d07cbeccb7e289e6f31ca767f6cce07c8', + 'reference' => 'baea18115984a1c2506c8af908424d45498b0b11', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -427,7 +427,7 @@ 'laravel/laravel' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => 'd2084b9d07cbeccb7e289e6f31ca767f6cce07c8', + 'reference' => 'baea18115984a1c2506c8af908424d45498b0b11', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(),