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 "$@"