From 285ea1ebdd6ed29700c5bf076e706396e92cbdd3 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 23 May 2026 19:11:30 +0200 Subject: [PATCH] first commit --- update_php.sh | 954 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 954 insertions(+) create mode 100755 update_php.sh diff --git a/update_php.sh b/update_php.sh new file mode 100755 index 0000000..0052c90 --- /dev/null +++ b/update_php.sh @@ -0,0 +1,954 @@ +Le contenu est généré par les utilisateurs et non vérifié. +#!/bin/bash +# ============================================================================= +# php-upgrade-nextcloud.sh +# Montée de version PHP sur Debian 12+ — contexte Nextcloud +# +# Pourquoi ce script est nécessaire : +# Sur Debian, installer une nouvelle version PHP ne migre RIEN automatiquement. +# Trois composants doivent être mis à jour manuellement et de façon coordonnée : +# 1. Le socket PHP-FPM (chemin dans pool.d/www.conf et dans le vhost) +# 2. La configuration du serveur web (Apache a2enconf / Nginx fastcgi_pass) +# 3. L'alternative CLI (/usr/bin/php via update-alternatives) +# C'est l'absence de cette coordination qui rend les montées "compliquées". +# +# Ce script gère les deux également : +# - Le dépôt Sury (packages.sury.org) qui est le seul moyen d'obtenir PHP 8.3+ +# sur Debian 12, le dépôt officiel Debian 12 ne fournissant que PHP 8.2. +# - La coexistence de plusieurs versions PHP, ce qui permet un rollback rapide. +# +# Sources de référence : +# [1] Sury repo Debian : https://packages.sury.org/php/ +# [2] Nextcloud PHP reqs : https://docs.nextcloud.com/server/latest/admin_manual/ +# installation/system_requirements.html +# [3] PHP versions sup. : https://www.php.net/supported-versions.php +# [4] Apache mod_proxy : https://httpd.apache.org/docs/2.4/mod/mod_proxy_fcgi.html +# ============================================================================= + +# --- Options shell de sécurité --- +# -e : arrête le script à la première erreur non gérée +# -u : traite les variables non définies comme des erreurs +# -o pipefail : un pipe échoue si l'une de ses commandes échoue (pas seulement la dernière) +set -euo pipefail + +# Séparateur de champs interne : on évite que les espaces et tabulations +# cassent le découpage de mots dans les boucles for/while sur des chemins de fichiers +IFS=$'\n\t' + +# --- Codes couleur ANSI pour la lisibilité des logs --- +R='\033[0;31m' # Rouge → erreurs fatales +Y='\033[1;33m' # Jaune → avertissements non bloquants +G='\033[0;32m' # Vert → succès / confirmations +B='\033[0;34m' # Bleu → informations neutres +W='\033[1m' # Gras → titres / séparateurs +N='\033[0m' # Reset → retour à la couleur normale + +info() { echo -e "${B}[INFO]${N} $*"; } +ok() { echo -e "${G}[ OK ]${N} $*"; } +warn() { echo -e "${Y}[WARN]${N} $*"; } +# die() écrit sur stderr (>&2) pour que les messages d'erreur soient capturables +# séparément si le script est piloté par un autre outil +die() { echo -e "${R}[ERR ]${N} $*" >&2; exit 1; } +sep() { echo -e "${W}──────────────────────────────────────────────${N}"; } + +# --- Variables globales --- +# Initialisées ici pour éviter les erreurs "unbound variable" dues à set -u. +# Elles seront remplies au fil des fonctions de détection. +PHP_CURRENT="" # Version PHP actuellement en service (ex: "8.2") +PHP_TARGET="" # Version PHP vers laquelle on migre (ex: "8.3") +WEB_SERVER="" # "apache2" ou "nginx" +PHP_MODE="" # "fpm" (recommandé) ou "mod" (mod_php, déprécié pour Nextcloud) +SURY_PRESENT=false # true si le dépôt packages.sury.org est déjà configuré +DEBIAN_CODENAME="" # ex: "bookworm" — nécessaire pour composer la ligne de dépôt Sury +BACKUP_DIR="" # Chemin du répertoire de backup créé par do_backup() +NC_PATH="" # Chemin d'installation de Nextcloud (répertoire contenant occ) +NC_USER="www-data" # Utilisateur système qui exécute PHP/Nextcloud (défaut Debian) +declare -a AVAILABLE_VERSIONS=() # Tableau des versions PHP-FPM disponibles via apt + + +# ============================================================================= +# 1. PRÉREQUIS SYSTÈME +# ============================================================================= + +check_root() { + # $EUID est l'UID effectif du processus courant. + # Les opérations apt, systemctl, a2enconf, etc. nécessitent tous les droits root. + [[ $EUID -eq 0 ]] || die "Ce script doit être exécuté en root (sudo $0)." +} + +check_debian() { + # /etc/os-release est le fichier standard (LSB) pour identifier la distribution. + # On le source pour obtenir les variables ID, VERSION_ID, VERSION_CODENAME. + [[ -f /etc/os-release ]] || die "/etc/os-release absent." + # shellcheck disable=SC1091 — le fichier est dynamique, shellcheck ne peut pas l'analyser + source /etc/os-release + + [[ "${ID:-}" == "debian" ]] \ + || die "Ce script est Debian-uniquement. OS détecté : ${ID:-inconnu}" + + local vid="${VERSION_ID:-0}" + # Debian 12 (Bookworm) est le minimum : c'est la première version où + # /usr/share/keyrings/ est la pratique standard pour les clés GPG de dépôts tiers, + # et où systemd est suffisamment stable pour nos appels systemctl. + (( vid >= 12 )) \ + || die "Debian ${vid} non supporté. Minimum requis : Debian 12 (Bookworm)." + + DEBIAN_CODENAME="${VERSION_CODENAME:-}" + ok "Debian ${VERSION_ID} (${DEBIAN_CODENAME})" +} + +check_deps() { + # Vérifie que tous les outils nécessaires sont présents avant de commencer. + # On échoue tôt plutôt qu'en plein milieu d'une opération destructive. + local missing=() + for cmd in curl gpg apt-get dpkg grep sed awk; do + command -v "${cmd}" &>/dev/null || missing+=("${cmd}") + done + (( ${#missing[@]} == 0 )) \ + || die "Dépendances manquantes : ${missing[*]}" + ok "Dépendances système : OK" +} + + +# ============================================================================= +# 2. DÉTECTION DE L'ENVIRONNEMENT +# ============================================================================= + +detect_current_php() { + sep; info "Détection PHP actuel..." + + # On commence par la version CLI car elle est la plus simple à détecter. + if command -v php &>/dev/null; then + # On demande à PHP lui-même sa version : plus fiable que de parser + # le nom du binaire ou la sortie de "php -v" (qui contient d'autres infos). + PHP_CURRENT=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') + ok "PHP CLI : ${PHP_CURRENT}" + fi + + # La version FPM est prioritaire sur la version CLI. + # Raison : c'est le processus PHP-FPM qui répond aux requêtes HTTP de Nextcloud, + # pas le PHP CLI. Les deux peuvent différer si l'administrateur a changé + # l'un sans l'autre. On identifie la version FPM par son socket Unix actif. + # + # Convention de nommage des sockets PHP-FPM sur Debian : + # /run/php/phpX.Y-fpm.sock + # Ce chemin est défini dans /etc/php/X.Y/fpm/pool.d/www.conf (directive "listen"). + for sock in /run/php/php*-fpm.sock; do + # -S teste si le fichier est un socket Unix (et non un fichier régulier) + [[ -S "${sock}" ]] || continue + + # grep -oP avec lookahead : extrait uniquement le numéro de version + # depuis un nom de type "php8.2-fpm.sock" + local ver + ver=$(basename "${sock}" | grep -oP '\d+\.\d+' || true) + [[ -n "${ver}" ]] || continue + + if [[ "${ver}" != "${PHP_CURRENT}" ]]; then + warn "FPM actif (${ver}) ≠ PHP CLI (${PHP_CURRENT:-?}). Version FPM retenue." + fi + PHP_CURRENT="${ver}" + ok "PHP-FPM socket actif : ${sock}" + break # On s'arrête au premier socket trouvé (cas standard : une seule version active) + done + + [[ -n "${PHP_CURRENT}" ]] \ + || die "Impossible de détecter une version PHP active (CLI ou FPM)." +} + +detect_webserver() { + sep; info "Détection serveur web..." + WEB_SERVER="" + PHP_MODE="fpm" # FPM est la valeur par défaut — mod_php sera détecté explicitement + + if systemctl is-active --quiet apache2 2>/dev/null; then + WEB_SERVER="apache2" + + # "apache2ctl -M" liste les modules Apache chargés. + # mod_php se présente sous la forme "php8.2_module" (le point est remplacé + # par rien dans le nom du module — "php82_module" serait faux, c'est bien + # "php8.2_module" avec le point conservé). + # Ref : nommage des modules dans /etc/apache2/mods-available/ + if apache2ctl -M 2>/dev/null | grep -q "php${PHP_CURRENT}_module"; then + PHP_MODE="mod" + warn "mod_php${PHP_CURRENT} détecté." + warn "mod_php est déprécié pour Nextcloud (pas compatible avec certains opcaches)." + warn "Ce script basculera vers FPM si mod_php de la version cible est absent." + fi + ok "Apache2 (mode PHP : ${PHP_MODE})" + fi + + if systemctl is-active --quiet nginx 2>/dev/null; then + if [[ -n "${WEB_SERVER}" ]]; then + # Les deux peuvent coexister (ex: Apache pour d'autres vhosts, Nginx devant Nextcloud) + # On prend Nginx car il ne supporte pas mod_php — c'est forcément FPM. + warn "Apache2 ET Nginx sont actifs. Nginx sera utilisé pour la bascule FPM." + fi + WEB_SERVER="nginx" + PHP_MODE="fpm" # Nginx ne supporte pas mod_php, toujours FPM via fastcgi_pass + ok "Nginx (mode : fpm)" + fi + + [[ -n "${WEB_SERVER}" ]] \ + || die "Aucun serveur web actif détecté (apache2, nginx)." +} + +detect_php_repos() { + sep; info "Détection dépôts PHP tiers..." + + # On cherche la chaîne "packages.sury.org" dans tous les fichiers de configuration apt. + # Deux formats coexistent sur Debian 12 : + # - Format one-line (.list) : "deb https://... codename main" + # - Format DEB822 (.sources) : clé-valeur multi-lignes + # grep -rl ("recursive + liste les fichiers seulement") gère les deux. + local found + found=$(grep -rl "packages.sury.org" \ + /etc/apt/sources.list \ + /etc/apt/sources.list.d/ \ + 2>/dev/null | head -n1 || true) + + if [[ -n "${found}" ]]; then + SURY_PRESENT=true + ok "Dépôt Sury trouvé : ${found}" + else + warn "Dépôt packages.sury.org non détecté." + info "Sans ce dépôt, seule la version PHP stock de Debian sera disponible." + # Debian 12 (Bookworm) livré avec PHP 8.2 uniquement dans ses dépôts officiels. + # PHP 8.3 et 8.4 ne sont accessibles que via Sury pour Debian 12. + info "Debian 12 stock = PHP 8.2 uniquement." + fi +} + +detect_nextcloud() { + sep; info "Recherche de Nextcloud..." + + # Chemins d'installation les plus courants selon les pratiques de déploiement Debian. + # La présence du fichier "occ" (outil CLI de Nextcloud) identifie sans ambiguïté + # le répertoire racine de l'installation. + local candidates=( + /var/www/nextcloud # Installation manuelle standard + /var/www/html/nextcloud # Installation via guide officiel Nextcloud + /srv/nextcloud # Alternative FHS pour les données de service + /opt/nextcloud # Installation par paquet tiers ou snap + ) + for path in "${candidates[@]}"; do + if [[ -f "${path}/occ" ]]; then + NC_PATH="${path}" + # On détecte automatiquement l'utilisateur propriétaire de occ. + # Cela évite les problèmes de droits si l'installation n'utilise pas www-data + # (ex: certains setups utilisent "nextcloud", "http", ou un user dédié). + # stat -c '%U' retourne le nom d'utilisateur (pas l'UID). + NC_USER=$(stat -c '%U' "${NC_PATH}/occ") + ok "Nextcloud : ${NC_PATH} (user: ${NC_USER})" + return + fi + done + + warn "Nextcloud non trouvé dans les chemins standard." + read -rp " Chemin Nextcloud (vide pour ignorer occ) : " NC_PATH + if [[ -n "${NC_PATH}" && -f "${NC_PATH}/occ" ]]; then + NC_USER=$(stat -c '%U' "${NC_PATH}/occ") + ok "Nextcloud : ${NC_PATH} (user: ${NC_USER})" + else + NC_PATH="" + # Si occ n'est pas utilisable, la migration reste possible mais sans + # maintenance mode automatique. L'utilisateur devra le gérer manuellement. + warn "occ non utilisé. Activez le mode maintenance manuellement si nécessaire." + fi +} + + +# ============================================================================= +# 3. DÉPÔT SURY +# ============================================================================= + +add_sury_repo() { + # Le dépôt packages.sury.org/php est maintenu par Ondřej Surý, mainteneur + # officiel des paquets PHP dans Debian. C'est la source recommandée pour + # obtenir des versions PHP plus récentes que celles des dépôts Debian stables. + # Procédure d'après : https://packages.sury.org/php/README.txt + info "Ajout du dépôt packages.sury.org/php pour Debian ${DEBIAN_CODENAME}..." + + # Depuis Debian 12, la bonne pratique est de stocker les clés GPG des dépôts + # tiers dans /usr/share/keyrings/ (format binaire .gpg) et de les référencer + # via l'option "signed-by=" dans la ligne de dépôt. + # L'ancienne méthode (apt-key add → /etc/apt/trusted.gpg) est dépréciée car + # elle rend la clé valide pour TOUS les dépôts, ce qui est un risque de sécurité. + local keyring="/usr/share/keyrings/php-sury.gpg" + if [[ ! -f "${keyring}" ]]; then + # Le fichier apt.gpg de Sury est déjà au format binaire GPG (non armored). + # On le télécharge directement sans passer par gpg --dearmor. + # Options curl : + # -s : silencieux (pas de barre de progression) + # -S : montre les erreurs malgré -s + # -f : échoue avec code 22 en cas d'erreur HTTP (4xx, 5xx) + # -L : suit les redirections + curl -sSfL "https://packages.sury.org/php/apt.gpg" -o "${keyring}" \ + || die "Échec téléchargement clé GPG Sury. Vérifiez la connectivité." + ok "Clé GPG Sury : ${keyring}" + else + ok "Clé GPG déjà présente : ${keyring}" + fi + + # Format one-line avec signed-by : lie explicitement cette clé GPG à ce seul dépôt. + local src_file="/etc/apt/sources.list.d/php-sury.list" + echo "deb [signed-by=${keyring}] https://packages.sury.org/php/ ${DEBIAN_CODENAME} main" \ + > "${src_file}" + ok "Dépôt Sury : ${src_file}" + + # -qq : mode très silencieux, n'affiche que les erreurs + apt-get update -qq + SURY_PRESENT=true +} + + +# ============================================================================= +# 4. VERSIONS DISPONIBLES ET CHOIX UTILISATEUR +# ============================================================================= + +detect_available_versions() { + sep; info "Versions PHP-FPM disponibles via apt..." + + # Mise à jour du cache apt avant la recherche, pour avoir les données fraîches. + apt-get update -qq 2>/dev/null + + # On cherche les paquets phpX.Y-fpm disponibles comme proxy pour les versions PHP. + # Raison du choix de -fpm : c'est le paquet qui conditionne l'installation complète + # (FPM est requis pour Nextcloud avec Apache/Nginx). S'il est disponible, + # les extensions (php8.3-gd, etc.) le sont aussi. + # + # apt-cache search --names-only : cherche uniquement dans les noms de paquets + # (pas dans les descriptions), avec regex. + # + # grep -oP avec lookahead/lookbehind PCRE : extrait "8.3" depuis "php8.3-fpm" + # sans capturer le préfixe "php" ni le suffixe "-fpm". + # + # sort -V : tri "version" (naturel) : 8.2 < 8.3 < 8.10 (et non ordre lexicographique) + mapfile -t AVAILABLE_VERSIONS < <( + apt-cache search --names-only '^php[0-9]+\.[0-9]+-fpm$' 2>/dev/null \ + | grep -oP 'php\K\d+\.\d+(?=-fpm)' \ + | sort -V + ) + + (( ${#AVAILABLE_VERSIONS[@]} > 0 )) \ + || die "Aucune version PHP-FPM disponible. Vérifiez vos dépôts." + + ok "Versions trouvées : ${AVAILABLE_VERSIONS[*]}" +} + +select_target_version() { + sep + echo -e "${W}Versions PHP-FPM disponibles :${N}" + for i in "${!AVAILABLE_VERSIONS[@]}"; do + local v="${AVAILABLE_VERSIONS[$i]}" + local tag="" + [[ "${v}" == "${PHP_CURRENT}" ]] && tag=" ${Y}← actuelle${N}" + printf " %d) PHP %s%b\n" "$((i+1))" "${v}" "${tag}" + done + echo "" + + local choice + while true; do + read -rp "Version cible [1-${#AVAILABLE_VERSIONS[@]}] : " choice + # Double condition : + # 1. La chaîne est bien un entier (regex) + # 2. L'entier est dans la plage valide (arithmétique bash) + # Note : (( )) avec set -e est sûr ici car on est dans un "if" + # (les conditions if/while ne déclenchent pas set -e sur faux) + if [[ "${choice}" =~ ^[0-9]+$ ]] \ + && (( choice >= 1 )) \ + && (( choice <= ${#AVAILABLE_VERSIONS[@]} )); then + break + fi + warn "Choix invalide." + done + + PHP_TARGET="${AVAILABLE_VERSIONS[$((choice-1))]}" + # Migrer vers la même version n'aurait aucun sens et risquerait d'écraser + # des configurations sans bénéfice. + [[ "${PHP_TARGET}" != "${PHP_CURRENT}" ]] \ + || die "Version cible identique à la version actuelle (${PHP_CURRENT}). Abandon." + ok "Version cible sélectionnée : PHP ${PHP_TARGET}" +} + + +# ============================================================================= +# 5. SAUVEGARDE +# ============================================================================= + +do_backup() { + sep + + # Timestamp dans le nom du répertoire pour éviter les collisions si le script + # est relancé plusieurs fois, et pour savoir facilement quand la backup a été faite. + BACKUP_DIR="/root/php-upgrade-backup-$(date +%Y%m%d-%H%M%S)" + mkdir -p "${BACKUP_DIR}" + info "Sauvegarde → ${BACKUP_DIR}" + + # --- Configuration PHP (php.ini, conf.d/, fpm/pool.d/) --- + # cp -a : préserve les permissions, timestamps et liens symboliques. + # Cela inclut tous les fichiers dans /etc/php/X.Y/ : + # - fpm/php.ini (paramètres PHP pour les requêtes web) + # - cli/php.ini (paramètres PHP pour occ et les crons) + # - fpm/pool.d/www.conf (configuration du pool FPM : socket, workers, etc.) + # - conf.d/ (extensions activées via des fichiers .ini) + if [[ -d "/etc/php/${PHP_CURRENT}" ]]; then + cp -a "/etc/php/${PHP_CURRENT}" "${BACKUP_DIR}/php-${PHP_CURRENT}-config" + ok "Config PHP ${PHP_CURRENT} sauvegardée." + fi + + # --- Configuration Apache --- + if [[ "${WEB_SERVER}" == "apache2" ]]; then + # sites-available/ : vhosts (contiennent les SetHandler ou ProxyPass vers FPM) + # conf-available/ : confs générales dont phpX.Y-fpm.conf + cp -a /etc/apache2/sites-available/ \ + "${BACKUP_DIR}/apache2-sites-available" 2>/dev/null || true + cp -a /etc/apache2/conf-available/ \ + "${BACKUP_DIR}/apache2-conf-available" 2>/dev/null || true + ok "Config Apache2 sauvegardée." + fi + + # --- Configuration Nginx --- + if [[ "${WEB_SERVER}" == "nginx" ]]; then + # sites-available/ : vhosts (contiennent fastcgi_pass vers le socket FPM) + # conf.d/ : configurations globales + cp -a /etc/nginx/sites-available/ \ + "${BACKUP_DIR}/nginx-sites-available" 2>/dev/null || true + cp -a /etc/nginx/conf.d/ \ + "${BACKUP_DIR}/nginx-conf.d" 2>/dev/null || true + ok "Config Nginx sauvegardée." + fi + + # --- Liste des paquets PHP installés --- + # Cette liste servira de référence pour vérifier quelles extensions ont été migrées + # et pour un éventuel rollback manuel (apt-get install $(cat php-X.Y-packages.txt)). + # dpkg -l "php8.2-*" : liste tous les paquets dont le nom commence par "php8.2-" + # awk '/^ii/' : filtre uniquement les paquets "installed" (état "ii" = installé+propre) + dpkg -l "php${PHP_CURRENT}-*" 2>/dev/null \ + | awk '/^ii/ {print $2}' \ + > "${BACKUP_DIR}/php-${PHP_CURRENT}-packages.txt" || true + ok "Liste paquets PHP ${PHP_CURRENT} : ${BACKUP_DIR}/php-${PHP_CURRENT}-packages.txt" + + ok "Backup complet : ${BACKUP_DIR}" +} + + +# ============================================================================= +# 6. INSTALLATION PHP CIBLE +# ============================================================================= + +install_new_php() { + sep; info "Installation PHP ${PHP_TARGET}..." + + # Stratégie : répliquer exactement les extensions installées sur la version actuelle. + # C'est la cause la plus fréquente de "blanc" après une migration : on installe + # le nouveau PHP sans les extensions, et Nextcloud échoue silencieusement. + # + # On lit la liste des paquets actuels via dpkg (plus fiable qu'apt list + # dont le format de sortie est instable entre versions). + local current_pkgs=() + mapfile -t current_pkgs < <( + dpkg -l "php${PHP_CURRENT}-*" 2>/dev/null \ + | awk '/^ii/ {print $2}' || true + ) + + local to_install=() + for pkg in "${current_pkgs[@]}"; do + # Substitution de version dans le nom du paquet : + # "php8.2-gd" devient "php8.3-gd", etc. + # La syntaxe bash ${var/pattern/replacement} remplace la PREMIÈRE occurrence. + local new_pkg="${pkg/php${PHP_CURRENT}-/php${PHP_TARGET}-}" + + # On vérifie que le paquet existe avant de l'ajouter à la liste. + # Certaines extensions peuvent avoir été renommées ou supprimées d'une + # version PHP à l'autre (ex: php-json a disparu en PHP 8.0, intégré au core). + # &>/dev/null : on supprime stdout ET stderr de apt-cache show + if apt-cache show "${new_pkg}" &>/dev/null 2>&1; then + to_install+=("${new_pkg}") + else + warn "Non disponible pour PHP ${PHP_TARGET} : ${new_pkg} (ignoré)" + fi + done + + # On s'assure que fpm et cli sont toujours présents, même si pour une raison + # quelconque ils n'étaient pas dans la liste dpkg. + to_install+=("php${PHP_TARGET}-fpm" "php${PHP_TARGET}-cli") + + # Déduplique la liste via un pipeline sort -u pour éviter de passer deux fois + # le même paquet à apt (inoffensif mais peu propre). + mapfile -t to_install < <(printf '%s\n' "${to_install[@]}" | sort -u) + + info "Paquets à installer :" + printf ' %s\n' "${to_install[@]}" + echo "" + + # DEBIAN_FRONTEND=noninteractive : supprime les questions interactives de debconf + # (ex: "Voulez-vous redémarrer les services ?") qui bloqueraient le script. + DEBIAN_FRONTEND=noninteractive apt-get install -y "${to_install[@]}" \ + || die "Échec installation PHP ${PHP_TARGET}. Consultez le log apt." + ok "PHP ${PHP_TARGET} installé." +} + + +# ============================================================================= +# 7. MIGRATION CONFIGURATION FPM ET PHP.INI +# ============================================================================= + +migrate_fpm_config() { + sep; info "Migration configuration PHP-FPM..." + + # --- Pool FPM (www.conf) --- + # Le fichier www.conf définit le comportement du pool FPM : + # nombre de workers (pm.max_children), mode de gestion (pm = dynamic/static/ondemand), + # et surtout le chemin du socket Unix (listen = /run/php/phpX.Y-fpm.sock). + # On copie la config de l'ancienne version et on met à jour le chemin du socket. + local src_pool="/etc/php/${PHP_CURRENT}/fpm/pool.d/www.conf" + local dst_pool="/etc/php/${PHP_TARGET}/fpm/pool.d/www.conf" + + if [[ -f "${src_pool}" && -f "${dst_pool}" ]]; then + cp "${src_pool}" "${dst_pool}" + + # sed -i : modification en place du fichier destination. + # On remplace l'ancien chemin de socket par le nouveau. + # Utilisation du séparateur | au lieu de / pour éviter les conflits + # avec les slashes dans les chemins de fichiers. + sed -i \ + "s|/run/php/php${PHP_CURRENT}-fpm\.sock|/run/php/php${PHP_TARGET}-fpm.sock|g" \ + "${dst_pool}" + ok "Pool FPM migré → ${dst_pool}" + else + warn "pool www.conf non migré (source ou destination absente)." + fi + + # --- php.ini FPM --- + # On migre les paramètres critiques pour Nextcloud uniquement, pas tout le php.ini. + # Raison : copier tout le php.ini d'une version à l'autre peut introduire des + # valeurs invalides ou dépréciées pour la nouvelle version. + # On cible les directives documentées par Nextcloud [2] et les directives de base. + local src_ini="/etc/php/${PHP_CURRENT}/fpm/php.ini" + local dst_ini="/etc/php/${PHP_TARGET}/fpm/php.ini" + + if [[ -f "${src_ini}" && -f "${dst_ini}" ]]; then + local keys=( + memory_limit # Nextcloud recommande >= 512M + upload_max_filesize # Doit être >= post_max_size pour les uploads de fichiers + post_max_size # Taille max du corps de la requête HTTP POST + max_execution_time # Délai avant timeout d'un script PHP (occ, tâches fond) + max_input_time # Délai d'attente pour recevoir les données POST + output_buffering # Nextcloud requiert "Off" pour les streams + "date.timezone" # Fuseau horaire — nécessaire pour la cohérence des logs/dates + ) + info "Migration php.ini FPM (paramètres Nextcloud-critiques) :" + for key in "${keys[@]}"; do + # On extrait la ligne de configuration depuis l'ancien php.ini. + # tail -n1 : on prend la DERNIÈRE occurrence en cas de doublon (la plus récente). + # || true : si grep ne trouve rien, il retourne 1 — on l'absorbe pour ne pas + # déclencher set -e (ce n'est pas une erreur si un paramètre n'est pas défini). + local line + line=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "${src_ini}" | tail -n1 || true) + [[ -n "${line}" ]] || continue + + # Deux cas : + # - La directive existe dans le php.ini destination → on la remplace + # - Elle n'existe pas (nouvelle install minimaliste) → on l'ajoute à la fin + if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${dst_ini}" 2>/dev/null; then + sed -i "s|^[[:space:]]*${key}[[:space:]]*=.*|${line}|" "${dst_ini}" + else + echo "${line}" >> "${dst_ini}" + fi + printf ' %-30s OK\n' "${key}" + done + + # --- php.ini CLI --- + # Le PHP CLI est utilisé par "occ" (crons, maintenance, upgrade Nextcloud). + # On migre seulement les deux directives les plus impactantes pour occ. + local src_cli="/etc/php/${PHP_CURRENT}/cli/php.ini" + local dst_cli="/etc/php/${PHP_TARGET}/cli/php.ini" + if [[ -f "${src_cli}" && -f "${dst_cli}" ]]; then + for key in memory_limit "date.timezone"; do + local line + line=$(grep -E "^[[:space:]]*${key}[[:space:]]*=" "${src_cli}" | tail -n1 || true) + [[ -n "${line}" ]] || continue + if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${dst_cli}" 2>/dev/null; then + sed -i "s|^[[:space:]]*${key}[[:space:]]*=.*|${line}|" "${dst_cli}" + else + echo "${line}" >> "${dst_cli}" + fi + done + ok "php.ini CLI migré (memory_limit, date.timezone)." + fi + else + warn "php.ini non migré (source ou destination absente)." + fi +} + + +# ============================================================================= +# 8. BASCULE DU SERVEUR WEB +# ============================================================================= + +switch_apache() { + sep; info "Bascule Apache → PHP ${PHP_TARGET}..." + + # --- Cas mod_php --- + if [[ "${PHP_MODE}" == "mod" ]]; then + # a2dismod désactive le module Apache (supprime le lien symbolique dans mods-enabled/). + # || true : si le module n'est pas activé, a2dismod retourne une erreur qu'on ignore. + a2dismod "php${PHP_CURRENT}" 2>/dev/null || true + + if a2enmod "php${PHP_TARGET}" 2>/dev/null; then + ok "mod_php${PHP_TARGET} activé." + else + # mod_php n'est disponible que si le paquet libapache2-mod-phpX.Y est installé. + # Le dépôt Sury le fournit, mais il peut ne pas être installé. Dans ce cas, + # on bascule vers FPM qui est de toute façon la configuration recommandée. + warn "mod_php${PHP_TARGET} absent. Bascule forcée vers PHP-FPM." + PHP_MODE="fpm" + fi + fi + + # --- Cas PHP-FPM (ou bascule forcée depuis mod_php) --- + if [[ "${PHP_MODE}" == "fpm" ]]; then + # proxy_fcgi et setenvif sont les modules Apache nécessaires pour proxy vers FPM. + # proxy_fcgi : gère le protocole FastCGI vers le socket FPM + # setenvif : nécessaire pour la directive SetEnvIfNoCase (header Authorization) + # Ref Apache : https://httpd.apache.org/docs/2.4/mod/mod_proxy_fcgi.html + a2enmod proxy_fcgi setenvif 2>/dev/null || true + + # Désactive la conf FPM de l'ancienne version (retire le lien dans conf-enabled/). + a2disconf "php${PHP_CURRENT}-fpm" 2>/dev/null || true + + local new_conf="/etc/apache2/conf-available/php${PHP_TARGET}-fpm.conf" + + # Le paquet php${PHP_TARGET}-fpm installe normalement ce fichier automatiquement. + # S'il est absent (cas rare mais possible avec certains setups Sury), on génère + # une configuration minimale fonctionnelle basée sur la doc Apache [4]. + if [[ ! -f "${new_conf}" ]]; then + warn "${new_conf} absent après installation. Génération d'une conf minimale." + cat > "${new_conf}" < + + # Passe le header HTTP Authorization à PHP-FPM (nécessaire pour CalDAV/WebDAV) + SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=\$1 + + + # Redirige les requêtes PHP vers le socket Unix FPM via le protocole FastCGI. + # Le format "proxy:unix:/chemin/socket|fcgi://localhost" est spécifique + # à mod_proxy_fcgi et différent d'un proxy HTTP classique. + SetHandler "proxy:unix:/run/php/php${PHP_TARGET}-fpm.sock|fcgi://localhost" + + + # Refuse l'accès direct aux fichiers cachés PHP (sécurité) + Require all denied + + +APACHECONF + ok "Conf Apache PHP-FPM minimale générée : ${new_conf}" + fi + + a2enconf "php${PHP_TARGET}-fpm" + ok "Conf Apache php${PHP_TARGET}-fpm activée." + fi + + # Toujours vérifier la syntaxe avant de redémarrer Apache. + # apache2ctl configtest retourne 0 et affiche "Syntax OK" si tout est bon. + apache2ctl configtest 2>&1 | grep -q "Syntax OK" \ + || die "Erreur syntaxe Apache. Lancez : apache2ctl configtest" + ok "Syntaxe Apache : OK" +} + +switch_nginx() { + sep; info "Bascule Nginx → PHP ${PHP_TARGET}-FPM..." + + # Nginx référence le socket FPM directement dans les blocs "location ~ \.php$" + # des vhosts, via la directive "fastcgi_pass unix:/run/php/phpX.Y-fpm.sock;". + # Il n'y a pas de mécanisme a2enconf équivalent : on doit modifier les fichiers + # de conf directement. + local old_sock="php${PHP_CURRENT}-fpm.sock" + local new_sock="php${PHP_TARGET}-fpm.sock" + local changed=0 + + # find avec -print0 / read -d '' : gestion correcte des noms de fichiers + # contenant des espaces ou des caractères spéciaux (bonne pratique générale). + # -maxdepth 3 : on évite de descendre trop profond dans l'arborescence. + while IFS= read -r -d '' f; do + grep -q "${old_sock}" "${f}" 2>/dev/null || continue + sed -i "s|${old_sock}|${new_sock}|g" "${f}" + ok "Mis à jour : ${f}" + # Note : on utilise changed=$(( changed + 1 )) plutôt que (( changed++ )) + # car avec set -e, (( )) retourne 1 si le résultat est 0 (faux arithmétique), + # ce qui déclencherait une sortie prématurée du script. + changed=$(( changed + 1 )) + done < <(find /etc/nginx/ -maxdepth 3 -type f \ + \( -name "*.conf" -o -name "*.conf.disabled" \) \ + -print0 2>/dev/null) + + (( changed > 0 )) \ + || warn "Aucun fichier Nginx modifié. Vérifiez manuellement les vhosts." + + # nginx -t : teste la configuration sans redémarrer le service. + # La sortie contient "syntax is ok" (minuscules) sur stdout/stderr. + nginx -t 2>&1 | grep -q "syntax is ok" \ + || die "Erreur syntaxe Nginx. Lancez : nginx -t" + ok "Syntaxe Nginx : OK" +} + + +# ============================================================================= +# 9. UPDATE-ALTERNATIVES — PHP CLI +# ============================================================================= + +switch_cli_alternatives() { + sep; info "Mise à jour update-alternatives (php CLI)..." + + # update-alternatives gère les liens symboliques pour les commandes qui existent + # en plusieurs versions (java, python, php, etc.). + # Sans cette étape, "php" en CLI continuerait à pointer vers l'ancienne version, + # ce qui casse "occ" et tous les crons Nextcloud qui appellent "php" directement. + local new_bin="/usr/bin/php${PHP_TARGET}" + + if [[ ! -x "${new_bin}" ]]; then + warn "${new_bin} introuvable. Alternative CLI non mise à jour." + return + fi + + # Vérifie si l'alternative est déjà enregistrée dans le système. + # Le paquet php${PHP_TARGET}-cli l'enregistre normalement automatiquement, + # mais dans certains cas (install manuelle, Sury mal configuré) ce n'est pas fait. + if ! update-alternatives --list php 2>/dev/null | grep -q "${new_bin}"; then + # Calcul de priorité : "8.3" → tr -d '.' → "83" → * 10 → 830 + # Les priorités plus élevées sont préférées en mode "auto". + # Les versions plus récentes obtiennent une priorité plus haute. + local prio + prio=$(echo "${PHP_TARGET}" | tr -d '.' ) + prio=$(( prio * 10 )) + update-alternatives --install /usr/bin/php php "${new_bin}" "${prio}" 2>/dev/null || true + fi + + # --set force le choix même si une autre version a une priorité plus haute + update-alternatives --set php "${new_bin}" 2>/dev/null \ + && ok "php CLI → ${new_bin}" \ + || warn "update-alternatives a échoué. Vérifiez avec : php -v" +} + + +# ============================================================================= +# 10. REDÉMARRAGE DES SERVICES +# ============================================================================= + +restart_services() { + sep; info "Redémarrage des services..." + + # On démarre d'abord PHP-FPM de la nouvelle version AVANT de redémarrer + # le serveur web. Raison : si Apache/Nginx redémarre et que le socket FPM + # n'existe pas encore, les premières requêtes tombent en 502 Bad Gateway. + systemctl restart "php${PHP_TARGET}-fpm" \ + || die "Échec démarrage php${PHP_TARGET}-fpm. Consultez : journalctl -u php${PHP_TARGET}-fpm" + ok "php${PHP_TARGET}-fpm : actif" + + systemctl restart "${WEB_SERVER}" \ + || die "Échec redémarrage ${WEB_SERVER}. Consultez : journalctl -u ${WEB_SERVER}" + ok "${WEB_SERVER} : redémarré" + + # On arrête et désactive l'ancien FPM pour libérer les ressources (workers, + # mémoire du pool) et éviter qu'il redémarre au prochain boot inutilement. + # On conserve les fichiers de config et les paquets installés pour un rollback rapide. + if systemctl is-active --quiet "php${PHP_CURRENT}-fpm" 2>/dev/null; then + systemctl stop "php${PHP_CURRENT}-fpm" || true + systemctl disable "php${PHP_CURRENT}-fpm" || true + ok "php${PHP_CURRENT}-fpm : arrêté et désactivé (paquets conservés pour rollback)" + fi +} + + +# ============================================================================= +# 11. HELPERS NEXTCLOUD (occ) +# ============================================================================= + +run_occ() { + # Guard clause : si NC_PATH est vide ou si occ n'existe pas, on sort silencieusement. + # Cela permet d'appeler run_occ dans le flux principal sans vérification répétée. + [[ -n "${NC_PATH}" && -f "${NC_PATH}/occ" ]] || return 0 + + # occ doit être exécuté en tant qu'utilisateur web (www-data ou équivalent). + # L'exécuter en root crée des fichiers de cache avec des permissions root, + # ce qui casse les accès web ultérieurs. + sudo -u "${NC_USER}" php "${NC_PATH}/occ" "$@" +} + +nc_maintenance() { + local state="${1:-on}" + [[ -n "${NC_PATH}" ]] || return 0 + + # Le mode maintenance de Nextcloud bloque toutes les requêtes utilisateur + # pendant la migration, évitant la corruption de sessions ou d'uploads en cours. + # La désactivation en fin de script est garantie par l'ordre d'appel dans main(). + info "Nextcloud maintenance:mode --${state}" + run_occ maintenance:mode "--${state}" || warn "occ maintenance:mode --${state} a échoué." +} + + +# ============================================================================= +# 12. VÉRIFICATION POST-MIGRATION +# ============================================================================= + +verify() { + sep; info "Vérification post-migration..." + + # Vérifie que le PHP CLI utilisé est bien la nouvelle version. + # Si update-alternatives a échoué, cette vérification le détecte. + local cli_ver + cli_ver=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null || echo "?") + + if [[ "${cli_ver}" == "${PHP_TARGET}" ]]; then + ok "PHP CLI actif : ${cli_ver} (cible atteinte)" + else + warn "PHP CLI actif : ${cli_ver} (attendu : ${PHP_TARGET}). Vérifiez update-alternatives." + fi + + # Vérifie que le service FPM de la nouvelle version est bien en cours d'exécution. + if systemctl is-active --quiet "php${PHP_TARGET}-fpm" 2>/dev/null; then + ok "php${PHP_TARGET}-fpm : actif" + else + warn "php${PHP_TARGET}-fpm n'est pas actif !" + fi + + # Pour Apache, vérifie qu'un module PHP est bien chargé (mod_php ou proxy_fcgi). + if [[ "${WEB_SERVER}" == "apache2" ]]; then + apache2ctl -M 2>/dev/null | grep -q "php\|proxy_fcgi" \ + && ok "Module PHP/proxy Apache : chargé" \ + || warn "Aucun module PHP/proxy visible dans Apache. Vérifiez a2enconf." + fi + + # Nextcloud occ status retourne un JSON avec installed, version, maintenance. + # C'est la vérification de bout en bout la plus complète disponible sans navigateur. + if [[ -n "${NC_PATH}" ]]; then + info "Nextcloud occ status :" + run_occ status || warn "occ status a retourné une erreur." + fi +} + + +# ============================================================================= +# 13. NETTOYAGE OPTIONNEL +# ============================================================================= + +maybe_cleanup() { + sep + + # On laisse le choix à l'utilisateur de conserver l'ancienne version. + # Avantages de la conserver : rollback instantané si un problème est détecté + # après la migration (un plugin incompatible, une extension manquante, etc.). + # Inconvénient : occupe de l'espace disque (quelques centaines de Mo max). + echo -e "${Y}Note :${N} les deux versions PHP peuvent coexister sans problème." + read -rp "Supprimer PHP ${PHP_CURRENT} et ses paquets ? [o/N] : " ans + if [[ "${ans,,}" == "o" ]]; then + # On récupère la liste exacte des paquets à purger via dpkg + # plutôt que de passer "php8.2-*" directement à apt-get purge. + # Raison : le glob shell n'est pas garanti d'être étendu correctement + # par apt selon la version et la configuration. + local old_pkgs=() + mapfile -t old_pkgs < <( + dpkg -l "php${PHP_CURRENT}-*" 2>/dev/null \ + | awk '/^ii/ {print $2}' || true + ) + if (( ${#old_pkgs[@]} > 0 )); then + # purge : supprime les paquets ET leurs fichiers de configuration + # (contrairement à "remove" qui laisse les conffiles) + apt-get purge -y "${old_pkgs[@]}" || warn "Purge partielle." + # autoremove : nettoie les dépendances orphelines (libapache2-mod-phpX.Y, etc.) + apt-get autoremove -y || true + ok "PHP ${PHP_CURRENT} supprimé." + else + info "Aucun paquet PHP ${PHP_CURRENT} trouvé." + fi + else + info "PHP ${PHP_CURRENT} conservé." + fi +} + + +# ============================================================================= +# MAIN — Orchestration +# ============================================================================= + +main() { + echo -e "\n${W}================================================${N}" + echo -e "${W} PHP Upgrade — Debian 12+ / Nextcloud ${N}" + echo -e "${W}================================================${N}\n" + + # --- Phase de détection (lecture seule, aucune modification) --- + check_root + check_debian + check_deps + detect_current_php + detect_webserver + detect_php_repos + detect_nextcloud + + # Proposition d'ajout Sury si absent. + # Sans Sury, "detect_available_versions" ne trouvera probablement que PHP 8.2 + # (version stock Debian 12), ce qui rend la migration inutile si on cible 8.3+. + if ! "${SURY_PRESENT}"; then + echo "" + warn "Sans le dépôt Sury, seule la version PHP stock Debian est disponible." + info "Debian 12 stock = PHP 8.2 uniquement. Sury permet 8.3, 8.4, etc." + read -rp "Ajouter packages.sury.org/php ? [O/n] : " ans_sury + [[ "${ans_sury,,}" == "n" ]] || add_sury_repo + fi + + # Maintenant qu'on sait si Sury est présent, on peut lister les versions dispo. + detect_available_versions + select_target_version + + # --- Récapitulatif avant modification --- + # On montre tout à l'utilisateur et on demande confirmation + # avant de toucher quoi que ce soit au système. + sep + echo -e "${W}Récapitulatif de la migration :${N}" + printf " %-20s %s\n" "PHP actuel" ": ${PHP_CURRENT}" + printf " %-20s %s\n" "PHP cible" ": ${PHP_TARGET}" + printf " %-20s %s\n" "Serveur web" ": ${WEB_SERVER} (mode: ${PHP_MODE})" + printf " %-20s %s\n" "Nextcloud occ" ": ${NC_PATH:-non détecté}" + printf " %-20s %s\n" "Backup" ": /root/php-upgrade-backup-" + echo "" + read -rp "Confirmer la migration ? [o/N] : " confirm + [[ "${confirm,,}" == "o" ]] || { info "Annulé par l'utilisateur."; exit 0; } + + # --- Phase de modification (ordre important) --- + do_backup # Toujours en premier : sécurité avant tout + nc_maintenance on # Coupe le trafic Nextcloud pendant la migration + install_new_php # Installe les paquets (ne touche pas encore au service web) + migrate_fpm_config # Copie pool.d/www.conf et les clés php.ini critiques + + # Bascule la configuration du serveur web selon le type détecté + case "${WEB_SERVER}" in + apache2) switch_apache ;; + nginx) switch_nginx ;; + esac + + switch_cli_alternatives # Met à jour le lien /usr/bin/php + restart_services # Démarre nouveau FPM, redémarre web, arrête ancien FPM + verify # Checks PHP CLI + FPM actif + module web + occ status + nc_maintenance off # Rouvre Nextcloud au trafic + maybe_cleanup # Propose de supprimer l'ancienne version + + # --- Résumé final --- + sep + ok "Migration terminée : PHP ${PHP_CURRENT} → PHP ${PHP_TARGET}" + info "Backup disponible : ${BACKUP_DIR}" + echo "" + echo -e "${Y}Rollback rapide si un problème apparaît :${N}" + echo " systemctl stop php${PHP_TARGET}-fpm" + echo " systemctl start php${PHP_CURRENT}-fpm" + if [[ "${WEB_SERVER}" == "apache2" ]]; then + echo " a2disconf php${PHP_TARGET}-fpm && a2enconf php${PHP_CURRENT}-fpm" + fi + echo " systemctl restart ${WEB_SERVER}" + echo " update-alternatives --set php /usr/bin/php${PHP_CURRENT}" + echo "" +} + +main "$@" +