false, 'message' => 'IP non autorisée.', 'client_ip' => ha_client_ip(), ], 403); } $given = (string)($_GET['token'] ?? $_POST['token'] ?? $_SERVER['HTTP_X_HEALTH_TOKEN'] ?? ''); if (HEALTH_AGENT_TOKEN === '' || HEALTH_AGENT_TOKEN === 'CHANGE-ME-TOKEN-LONG') { ha_out([ 'success' => false, 'message' => 'Token agent non configuré. Modifie HEALTH_AGENT_TOKEN.', ], 500); } if (!hash_equals(HEALTH_AGENT_TOKEN, $given)) { ha_out([ 'success' => false, 'message' => 'Token invalide.', ], 403); } } function ha_root(): string { return realpath(HEALTH_AGENT_ROOT) ?: HEALTH_AGENT_ROOT; } function ha_rel_path(string $file): string { $root = rtrim(ha_root(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; $real = realpath($file) ?: $file; return str_starts_with($real, $root) ? substr($real, strlen($root)) : $file; } function ha_safe_identifier(string $name): string { return '`' . str_replace('`', '``', $name) . '`'; } function ha_mask_secret(?string $value): ?string { if ($value === null || $value === '') { return $value; } $len = mb_strlen($value); if ($len <= 4) { return str_repeat('*', $len); } return mb_substr($value, 0, 2) . str_repeat('*', max(4, $len - 4)) . mb_substr($value, -2); } // ----------------------------------------------------------------------------- // CHARGEMENT DU MODEL / DÉTECTION PDO // ----------------------------------------------------------------------------- function ha_include_file_and_capture_pdo(string $file): array { $loaded = false; $error = null; /* * Important : model/model.php crée souvent $pdo au niveau du fichier. * Comme l'include est fait dans cette fonction, le $pdo créé existe ici, * dans ce scope local. On le capture donc immédiatement après l'include. */ try { require_once $file; $loaded = true; } catch (Throwable $e) { $error = $e->getMessage(); } $candidates = [ 'pdo' => $pdo ?? null, 'bdd' => $bdd ?? null, 'db' => $db ?? null, 'database' => $database ?? null, 'dbh' => $dbh ?? null, 'conn' => $conn ?? null, 'connection' => $connection ?? null, ]; foreach ($candidates as $name => $candidate) { if ($candidate instanceof PDO) { try { $candidate->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $candidate->query('SELECT 1')->fetchColumn(); return [ 'pdo' => $candidate, 'status' => 'found', 'source' => '$' . $name, 'message' => 'Connexion PDO détectée via $' . $name . ' dans ' . ha_rel_path($file) . '.', 'file' => ha_rel_path($file), 'loaded' => $loaded, 'error' => $error, ]; } catch (Throwable $e) { return [ 'pdo' => null, 'status' => 'invalid', 'source' => '$' . $name, 'message' => 'PDO détecté via $' . $name . ' dans ' . ha_rel_path($file) . ', mais SELECT 1 échoue : ' . $e->getMessage(), 'file' => ha_rel_path($file), 'loaded' => $loaded, 'error' => $error, ]; } } } return [ 'pdo' => null, 'status' => $error ? 'error' : 'not_found', 'source' => null, 'message' => $error ? 'Erreur au chargement de ' . ha_rel_path($file) . ' : ' . $error : 'Aucune variable PDO détectée dans ' . ha_rel_path($file) . '.', 'file' => ha_rel_path($file), 'loaded' => $loaded, 'error' => $error, ]; } function ha_get_pdo_from_project(): array { static $done = false; static $result = null; if ($done && is_array($result)) { return $result; } $done = true; $loaded = []; $errors = []; $attempts = []; $bootstrapFiles = [ HEALTH_AGENT_ROOT . '/model/model.php', HEALTH_AGENT_ROOT . '/config.php', HEALTH_AGENT_ROOT . '/app/config.php', HEALTH_AGENT_ROOT . '/config/config.php', HEALTH_AGENT_ROOT . '/include/config.php', HEALTH_AGENT_ROOT . '/includes/config.php', HEALTH_AGENT_ROOT . '/db.php', HEALTH_AGENT_ROOT . '/database.php', HEALTH_AGENT_ROOT . '/connexion.php', HEALTH_AGENT_ROOT . '/connection.php', ]; foreach ($bootstrapFiles as $file) { if (!is_file($file) || !is_readable($file)) { continue; } $capture = ha_include_file_and_capture_pdo($file); $attempts[] = [ 'file' => $capture['file'], 'status' => $capture['status'], 'source' => $capture['source'], 'message' => $capture['message'], ]; if (!empty($capture['loaded'])) { $loaded[] = $capture['file']; } if (!empty($capture['error'])) { $errors[] = [ 'file' => $capture['file'], 'message' => $capture['error'], ]; } if (($capture['pdo'] ?? null) instanceof PDO) { $result = [ 'pdo' => $capture['pdo'], 'status' => 'found', 'source' => $capture['source'], 'message' => $capture['message'], 'bootstrap' => [ 'loaded' => $loaded, 'errors' => $errors, 'attempts' => $attempts, 'strategy' => 'include_and_capture_local_pdo', 'stopped_after_pdo_found' => true, ], ]; return $result; } } $result = [ 'pdo' => null, 'status' => 'not_found', 'source' => null, 'message' => 'Aucune connexion PDO détectée après chargement des fichiers de bootstrap.', 'bootstrap' => [ 'loaded' => $loaded, 'errors' => $errors, 'attempts' => $attempts, 'strategy' => 'include_and_capture_local_pdo', 'stopped_after_pdo_found' => false, ], ]; return $result; } function ha_load_project_bootstrap(): array { $pdoResult = ha_get_pdo_from_project(); return $pdoResult['bootstrap'] ?? [ 'loaded' => [], 'errors' => [], 'attempts' => [], 'strategy' => 'none', ]; } // ----------------------------------------------------------------------------- // FICHIERS / ANALYSE STATIQUE // ----------------------------------------------------------------------------- function ha_php_files(): array { $root = ha_root(); $exclude = ['vendor', 'node_modules', '.git', 'storage', 'cache', 'tmp', 'logs', 'uploads', 'backup', 'backups', 'var/cache']; $files = []; try { $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($it as $file) { /** @var SplFileInfo $file */ $path = $file->getPathname(); $normalized = str_replace('\\', '/', $path); foreach ($exclude as $part) { if (preg_match('#(^|/)' . preg_quote($part, '#') . '(/|$)#i', $normalized)) { continue 2; } } if ($file->isFile() && strtolower($file->getExtension()) === 'php') { $files[] = $path; if (count($files) >= HEALTH_MAX_FILES) { break; } } } } catch (Throwable $e) { return []; } sort($files); return $files; } function ha_issue(string $severity, string $rule, string $title, string $message, ?string $file = null, ?int $line = null, ?string $recommendation = null): array { return [ 'severity' => $severity, 'rule' => $rule, 'title' => $title, 'message' => $message, 'file' => $file, 'line' => $line, 'recommendation' => $recommendation, ]; } function ha_action_files_scan(): array { $files = ha_php_files(); $issues = []; $linesTotal = 0; $readFiles = 0; $skippedLarge = 0; foreach ($files as $file) { if (!is_readable($file)) { continue; } $size = @filesize($file); if ($size !== false && $size > HEALTH_MAX_FILE_SIZE) { $skippedLarge++; continue; } $content = (string)@file_get_contents($file); if ($content === '') { continue; } $readFiles++; $rel = ha_rel_path($file); $lines = preg_split('/\R/', $content) ?: []; $lineCount = count($lines); $linesTotal += $lineCount; if ($lineCount > 900) { $issues[] = ha_issue('info', 'large_file', 'Fichier volumineux', $lineCount . ' lignes.', $rel, null, 'Découper le fichier pour améliorer la maintenance.'); } if (preg_match_all('/^(<<<<<<<|=======|>>>>>>>)/m', $content, $m, PREG_OFFSET_CAPTURE)) { foreach ($m[0] as $hit) { $issues[] = ha_issue('critical', 'conflict_marker', 'Conflit Git non résolu', 'Marqueur de conflit détecté.', $rel, substr_count(substr($content, 0, $hit[1]), "\n") + 1, 'Résoudre le conflit avant correction ou mise en ligne.'); } } if (preg_match_all('/