Files
Update-Nextcloud-s-PHP/update_php.sh
T
2026-05-23 19:11:30 +02:00

955 lines
43 KiB
Bash
Executable File

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}" <<APACHECONF
# Généré par php-upgrade-nextcloud.sh
# Référence : https://httpd.apache.org/docs/2.4/mod/mod_proxy_fcgi.html
<IfModule mod_proxy_fcgi.c>
<IfModule mod_setenvif.c>
# Passe le header HTTP Authorization à PHP-FPM (nécessaire pour CalDAV/WebDAV)
SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=\$1
</IfModule>
<FilesMatch ".+\.ph(?:ar|p|tml)$">
# 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"
</FilesMatch>
<FilesMatch "^\.ph(?:ar|p|ps|tml)$">
# Refuse l'accès direct aux fichiers cachés PHP (sécurité)
Require all denied
</FilesMatch>
</IfModule>
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-<timestamp>"
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 "$@"