
­­­­­­­­­­­­­­­­­­
<!DOCTYPE html>
<html>
#!/bin/bash
# i360-altphp-activate - put "extension=i360.so" where alt-php reads it.
#
# The location is decided from /usr/share/i360-php-opts/cagefs.ini on every run and
# is never stored:
#   on   - /opt/alt/phpXX/etc/php.ini
#   off  - /opt/alt/phpXX/etc/php.d/i360.ini
#   auto - php.d/i360.ini on a Plesk host that has neither CageFS nor CloudLinux,
#          php.ini everywhere else (the shipped default).  A missing file or an
#          unrecognised value is read as auto, so one typo cannot leave a host
#          unprotected.
#
# Why two locations (DEF-34424, DEF-52216): inside a CageFS cage the PHP Selector
# swaps /opt/alt/phpXX/link/conf for the tenant's own directory, so php.d/i360.ini is
# never read there and php.ini is the only file every caged PHP reads that a tenant
# cannot edit.  On a Plesk host without CageFS nothing swaps that directory: php.d/i360.ini
# is read by every SAPI and, unlike a line appended to php.ini, it survives Plesk's
# tuxcare-php extension, which replaces php.ini wholesale on every install/configure/switch.
#
# Only Plesk-without-CageFS moves.  Every cPanel, DirectAdmin and panel-less host keeps
# php.ini, and so does every CloudLinux host whether or not CageFS is installed yet.
# A host that gains CageFS later is moved back to php.ini at the next package event:
# %post of imunify360-php-i360, %triggerin on alt-phpXX-common, %triggerin on cagefs.
# The steps are the ones the spec used to carry inline in %post and %triggerin.
#
# On Plesk every run also brings the per-site php.ini of each site on that version's CGI or
# FastCGI handler in line with it (sync_plesk_site_ini).
#
# Usage: i360-altphp-activate [phpXX ...]
#   Without arguments every version listed in /usr/share/i360-php/altphps.txt is handled.
#
# Exit status is always 0.

KNOB=/usr/share/i360-php-opts/cagefs.ini
VERSIONS_FILE=/usr/share/i360-php/altphps.txt
PLESK=/usr/sbin/plesk

# Versions this run handled.
HANDLED=()

# A cage swaps the php.d scan directory out from under us for every tenant who selected a
# PHP version, so php.ini is the only location that reaches all of them.  CageFS ships only
# on CloudLinux and a CloudLinux host can install it at any moment, so either signal is
# enough to keep the host on php.ini.
needs_php_ini() {
    [ -e /usr/sbin/cagefsctl ] && return 0
    # CloudLinux 10 keeps AlmaLinux's os-release; cloudlinux-release still marks it.
    [ -e /etc/cloudlinux-release ] && return 0
    grep -qsi cloudlinux /etc/os-release /etc/redhat-release
}

# The house test for Plesk, same as the spec's %post and %preun.
is_plesk() {
    [ -e /usr/local/psa/version ]
}

# Prints "php.ini" or "php.d".
delivery_mode() {
    local knob
    knob=$(sed -n 's/^cagefs_integration=[[:space:]]*//p' "$KNOB" 2>/dev/null | tail -n1)
    knob=${knob%%[[:space:]]*}
    case "$knob" in
        off) echo php.d ;;
        on)  echo php.ini ;;
        *)   if needs_php_ini; then echo php.ini
             elif is_plesk;    then echo php.d
             else                   echo php.ini
             fi ;;
    esac
}

enable_in_php_ini() {
    local ver=$1 etc=/opt/alt/$1/etc
    # Write the new location before clearing the old one: the transient state is then
    # "loaded twice", which costs a warning, rather than "not loaded", which costs cover.
    if ! grep -qs i360.so "$etc/php.ini"; then
        # Keep the old location if the write fails.
        cat "$etc/php.d.i360/i360.ini" >> "$etc/php.ini" || return 0
        echo "i360: $ver: extension=i360.so enabled in $etc/php.ini"
    fi
    # Not conditional on the append above: with the line in php.ini *and* a copy in php.d
    # the module loads twice for good.  And with i360 in the places the PHP Selector
    # manages, 'selectorctl --user user --set-user-current 8.2' turns i360 off (DEF-34424).
    if grep -qs i360 /opt/alt/"$ver"/link/conf/default.ini; then
        sed -i -e '/i360\./d' -e '/\[i360\]/d' /opt/alt/"$ver"/link/conf/default.ini
    fi
    rm -f "$etc/php.d/i360.ini" "$etc/php.d.all/i360.ini"
}

enable_in_php_d() {
    local ver=$1 etc=/opt/alt/$1/etc
    # Same order as above, for the same reason.  Copied only when absent, so a drop-in the
    # administrator commented out with 'plesk bin php_modules_ctl -d i360' stays commented.
    if [ ! -f "$etc/php.d/i360.ini" ]; then
        # Through a temporary name: a copy cut short must not leave a php.d/i360.ini that the next
        # run would take for a finished one.
        mkdir -p "$etc/php.d" && cp "$etc/php.d.i360/i360.ini" "$etc/php.d/.i360.ini.new" &&
            mv -fT "$etc/php.d/.i360.ini.new" "$etc/php.d/i360.ini" ||
            { rm -f "$etc/php.d/.i360.ini.new"; return 0; }
        echo "i360: $ver: extension=i360.so enabled in $etc/php.d/i360.ini"
    fi
    if grep -qs i360 "$etc/php.ini"; then
        sed -i -e '/i360\./d' -e '/\[i360\]/d' "$etc/php.ini"
        echo "i360: $ver: extension=i360.so removed from $etc/php.ini"
    fi
}

# Plesk's CGI and FastCGI handlers do not read /opt/alt/phpXX/etc/php.ini: each site reads a copy
# Plesk renders into <vhosts>/system/<site>/etc/php.ini from that file plus the site's own settings,
# and re-renders only when the site's PHP settings are saved.  Our line reaches the copy as
# "extension = i360.so" under [PHP].  Left alone, a php.ini -> php.d move loads i360 twice on those
# sites and a php.d -> php.ini move leaves them without it, so make each copy agree with its
# version's php.ini, which is what the next render would write anyway.  mod_fcgid recycles the PHP
# processes within minutes, so nothing needs a restart.
sync_plesk_site_ini() {
    local ids= v site handler ver ini vhosts
    is_plesk && [ -x "$PLESK" ] || return 0
    vhosts=$(sed -n 's/^HTTPD_VHOSTS_D[[:space:]]\+//p' /etc/psa/psa.conf 2>/dev/null)
    for v in "$@"; do ids="$ids,'alt-$v-cgi','alt-$v-fastcgi'"; done
    "$PLESK" db -Ne "SELECT d.name, h.php_handler_id FROM domains d JOIN hosting h ON h.dom_id = d.id WHERE h.php_handler_id IN (${ids#,})" 2>/dev/null |
    while read -r site handler; do
        ini=${vhosts:-/var/www/vhosts}/system/$site/etc/php.ini
        [ -f "$ini" ] || continue
        ver=${handler#alt-}; ver=${ver%-*}
        if grep -qs '^[[:space:]]*extension[[:space:]]*=[[:space:]]*"\?i360\.so' "/opt/alt/$ver/etc/php.ini"; then
            grep -qs '^extension[[:space:]]*=[[:space:]]*"\?i360\.so' "$ini" && continue
            sed -i '/^\[PHP\]$/a extension = i360.so' "$ini" &&
                echo "i360: $site: extension=i360.so enabled in $ini"
        elif grep -qs '^extension[[:space:]]*=[[:space:]]*"\?i360\.so' "$ini"; then
            sed -i '/^extension[[:space:]]*=[[:space:]]*"\?i360\.so/d' "$ini" &&
                echo "i360: $site: extension=i360.so removed from $ini"
        fi
    done
}

main() {
    local versions=("$@") mode ver etc
    if [ ${#versions[@]} -eq 0 ]; then
        [ -r "$VERSIONS_FILE" ] || exit 0
        read -r -a versions < "$VERSIONS_FILE"
    fi

    mode=$(delivery_mode)
    for ver in "${versions[@]}"; do
        [[ $ver =~ ^php[0-9]{2,3}$ ]] || continue
        etc=/opt/alt/$ver/etc
        # alt-php version not installed, or i360 not shipped for it: nothing to do.
        [ -e "$etc/php.ini" ] && [ -e "$etc/php.d.i360/i360.ini" ] || continue
        case $mode in
            php.ini) enable_in_php_ini "$ver" ;;
            php.d)   enable_in_php_d "$ver" ;;
        esac
        HANDLED+=("$ver")
    done
    [ ${#HANDLED[@]} -eq 0 ] || sync_plesk_site_ini "${HANDLED[@]}"
    exit 0
}

main "$@"
