#!/usr/bin/env php $url, "error" => curl_error($ch)]); return false; } curl_close($ch); $manifest_entries = []; $lines = explode("\n", trim($content)); foreach ($lines as $line) { $line = trim($line); if (empty($line)) continue; // Validation format: hash(64) mode(3) path if (preg_match('/^([0-9a-fA-F]{64})\s+(644|755)\s+((bin|lib|conf)\/[A-Za-z0-9._\/-]+)$/', $line, $matches)) { $manifest_entries[] = [ 'hash' => $matches[1], 'mode' => $matches[2], 'path' => $matches[3] ]; } } if (empty($manifest_entries)) { log_error("manifest_invalid", "Le manifeste distant est invalide ou vide", ["url" => $url]); return false; } log_info("manifest_downloaded", "Manifeste téléchargé", ["url" => $url]); return $manifest_entries; } /** * Met à jour un fichier spécifique */ function update_one_file($entry) { global $MONITORING_BASE_DIR, $UPDATE_BASE_URL, $UPDATE_TMP_DIR, $UPDATE_TIMEOUT_TOTAL; $rel_path = $entry['path']; $target_file = $MONITORING_BASE_DIR . '/' . $rel_path; $remote_url = rtrim($UPDATE_BASE_URL, '/') . '/' . $rel_path; $expected_hash = strtolower($entry['hash']); // Calcul du hash local actuel $local_hash = file_exists($target_file) ? hash_file('sha256', $target_file) : ""; if ($local_hash === $expected_hash) { log_debug("update_not_needed", "Fichier déjà à jour", ["file" => $rel_path]); return true; } // Téléchargement $tmp_file = $UPDATE_TMP_DIR . '/' . basename($rel_path) . '.' . uniqid(); $ch = curl_init($remote_url); $fp = fopen($tmp_file, 'wb'); curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL); curl_setopt($ch, CURLOPT_FAILONERROR, true); $success = curl_exec($ch); $error = curl_error($ch); curl_close($ch); fclose($fp); if (!$success) { log_error("update_download_failed", "Téléchargement impossible", ["file" => $rel_path, "url" => $remote_url, "error" => $error]); @unlink($tmp_file); return false; } // Vérification Hash $downloaded_hash = hash_file('sha256', $tmp_file); if ($downloaded_hash !== $expected_hash) { log_error("update_hash_mismatch", "Hash téléchargé invalide", ["file" => $rel_path, "expected" => $expected_hash, "got" => $downloaded_hash]); @unlink($tmp_file); return false; } // Installation ensure_parent_dir($target_file); chmod($tmp_file, octdec($entry['mode'])); if (!rename($tmp_file, $target_file)) { fail_internal("Échec du déplacement de $tmp_file vers $target_file"); } if ($local_hash === "") { log_notice("file_created", "Fichier créé depuis le manifeste", ["file" => $rel_path, "mode" => $entry['mode']]); } else { log_notice("update_applied", "Mise à jour appliquée", ["file" => $rel_path, "old_hash" => $local_hash, "new_hash" => $expected_hash]); } return true; } /** * Supprime les fichiers locaux absents du manifeste */ function delete_extra_files($remote_files) { global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR; if (!$UPDATE_ALLOW_DELETE) return; $directories = ['bin', 'lib', 'conf']; foreach ($directories as $dir) { $full_path = $MONITORING_BASE_DIR . '/' . $dir; if (!is_dir($full_path)) continue; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { // On récupère le chemin relatif par rapport à la racine du monitoring $rel_path = substr($file->getPathname(), strlen($MONITORING_BASE_DIR) + 1); // 1. Protection : Si c'est dans le manifeste distant, on ne touche à rien if (in_array($rel_path, $remote_files)) { continue; } // 2. Protection générique : On n'efface JAMAIS les fichiers de configuration locale // Cela couvre : *.local.conf.php, *.local.conf, et même *.local.php par sécurité if (str_ends_with($rel_path, '.local.conf.php') || str_ends_with($rel_path, '.local.conf') || str_ends_with($rel_path, '.local.php')) { log_debug("delete_skipped", "Fichier local protégé (ignoré)", ["file" => $rel_path]); continue; } // 3. Suppression si le fichier est obsolète et non protégé if (@unlink($file->getPathname())) { log_notice("file_deleted", "Fichier obsolète supprimé", ["file" => $rel_path]); } else { log_error("delete_failed", "Impossible de supprimer le fichier local", ["file" => $rel_path]); } } } } // --- Main --- $manifest = fetch_manifest($UPDATE_MANIFEST_URL); if ($manifest === false) exit(2); $total = count($manifest); $updated = 0; $failed = 0; $remote_paths = []; foreach ($manifest as $entry) { $remote_paths[] = $entry['path']; if (update_one_file($entry)) { $updated++; } else { $failed++; } } delete_extra_files($remote_paths); if ($failed > 0) { log_warning("update_finished_with_errors", "Mise à jour terminée avec erreurs", ["total" => $total, "updated" => $updated, "failed" => $failed]); } else { log_info("update_finished", "Mise à jour terminée", ["total" => $total, "updated" => $updated]); } exit_with_status();