diagnose
This commit is contained in:
+8
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+797
@@ -0,0 +1,797 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* diagnose.php — Standalone verificare installazione Glastree (Laravel)
|
||||
*
|
||||
* Esecuzione:
|
||||
* php diagnose.php
|
||||
* php diagnose.php --verbose
|
||||
*
|
||||
* Non richiede Laravel. Legge .env direttamente e testa DB via PDO.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 1. Setup
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
define('BASE_DIR', __DIR__);
|
||||
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 = in_array('--verbose', $argv ?? []);
|
||||
|
||||
$results = [
|
||||
'passed' => 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('/<input[^>]*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);
|
||||
}
|
||||
Binary file not shown.
Vendored
+2
-2
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user