#!/usr/bin/sh
# httpd-iworx-migrate-conf (CPS-60): carry an upgraded server's panel web
# server config over to the event MPM and mod_proxy_fcgi. Run by the
# package's %post, so it lands before the old package's %postun restarts
# httpd-iworx.
#
# Usage: httpd-iworx-migrate-conf VERSION-RELEASE [--upgrade]
#
# VERSION-RELEASE is the package being installed, so the previous one can be
# found in the rpm database. --upgrade also checks that the resulting config
# loads, and says why if it doesn't.
#
# 1. App configs. Each app's config in conf.d/iworx-apps is split in two:
#    10-<app>.conf holds its Alias lines and stays %config(noreplace), since
#    that is what gets toggled (NodeWorx turns Roundcube off by commenting out
#    its Alias line); 11-<app>.conf holds the rest and is replaced on every
#    upgrade. rpm keeps a 10-<app>.conf modified before the split, with the
#    app's whole old config, FastCGIExternalServer included, which the event
#    MPM can't load. Such a file is rebuilt from the pristine copy the package
#    ships, and the old one kept as .rpmsave.
#
#    Its enabled/disabled state carries over when the old copy matches, apart
#    from the Alias toggles, a copy some httpd-iworx package shipped. If it
#    doesn't, it may hold access restrictions (e.g. Require ip) that
#    11-<app>.conf's "Require all granted" would lift, so the app is turned
#    off instead: every Alias line is commented out and, for Roundcube,
#    NodeWorx's own setting is turned off to match, so NodeWorx shows it off
#    and turns it back on as usual. Where the previous package can't be
#    compared with (an EL7 httpd-iworx 2.2 upgrade), the state carries over
#    unless the old copy holds access restrictions of its own.
#
# 2. mod_fastcgi directives in any other file httpd.conf loads
#    (httpd-custom.conf, say) are commented out, after a backup.
#
# It only acts on what it finds, so it is safe to run again; never writes
# through a symlink; and always exits 0, so it can't fail the package install.
#
# IWORX_HOME and IWORX_PRISTINE_DIR override the paths, for testing.

THISVR=$1
[ "$2" = --upgrade ] && UPGRADE=1

IWORX_HOME=${IWORX_HOME:-/home/interworx}
PRISTINE=${IWORX_PRISTINE_DIR:-/usr/share/httpd-iworx/iworx-apps}
APPDIR=$IWORX_HOME/etc/httpd/conf.d/iworx-apps
HTTPD_CONF=$IWORX_HOME/etc/httpd/httpd.conf
INI=$IWORX_HOME/iworx.ini
# Relative Include paths resolve against ServerRoot.
SERVERROOT=$IWORX_HOME/html

warn() {
    echo "httpd-iworx: $1" >&2
    logger -t httpd-iworx "$1" 2>/dev/null || :
}

# sha256 of every 10-<app>.conf an httpd-iworx package has shipped, with its
# Alias lines uncommented (see untoggled). Add each new shipped version here.
known_stock() {
    grep -qxF "$1" <<'EOF'
f1a7ad897dd881bcbfa661767a0422f7f1c9f2b2223da40a49fa52bbdbe788e7  10-db-phpmyadmin.conf
fd29c444a75c4164ff5a6617f1f5813e891a6e7e91fa6e28fc512aebc04ed643  10-db-phpmyadmin.conf
09dd3a88c3d9ea2ef9a47fed77e0a831d2e9a9704c2e98a19fe85035bc590043  10-webmail-roundcube.conf
40987888113cf952f37174accaa114a5a6b399c47b511dffcd337a2b8aefd5b9  10-webmail-roundcube.conf
e435443e8c908f6a614d73b2be95d01bf860e9951339dfedd4f412dca73efb1a  10-webmail-roundcube.conf
a38db0b81d5b0b542d47a200d047b4109aa5cf4863ddd25ccccc2b2c54499e21  10-webmail-horde.conf
09db5fcd40600fa0b5a45daa78a4e22e31430688dae57729f851fc71f0b920bd  10-webmail-horde.conf
EOF
}

# The file with every commented-out Alias line uncommented, so copies that
# differ only in which Aliases are on compare equal.
untoggled() { sed -E 's/^#+[[:space:]]*([Aa][Ll][Ii][Aa][Ss][[:space:]])/\1/' "$1"; }

# True if the file uses a mod_fastcgi directive (commented-out ones don't count).
has_fastcgi() { grep -Eqi '^[[:space:]]*FastCGI[[:alnum:]]*[[:space:]]' "$1"; }

# True if the file still has the pre-split layout: the app's whole config,
# not just its Alias lines.
has_old_layout() { grep -Eqi '^[[:space:]]*(<Directory|<Proxy|<FilesMatch|SetHandler|Action|FastCGI[[:alnum:]]*)[[:space:]]' "$1"; }

# True if the file holds an access restriction of its own: stock files have
# only "Require all granted", and "Require all denied" for a few blocked files.
has_restrictions() {
    grep -Ei '^[[:space:]]*(Require|Deny|Satisfy|Auth[[:alnum:]]*|SSLRequire)[[:space:]]|^[[:space:]]*<(Limit|Require)' "$1" 2>/dev/null |
        grep -Eqiv '^[[:space:]]*Require[[:space:]]+all[[:space:]]+(granted|denied)[[:space:]]*$'
}

# A name for a backup that doesn't exist yet, so it is never written through.
backup_name() {
    if [ -e "$1" ] || [ -L "$1" ]; then
        echo "$1.$(date +%Y%m%d%H%M%S).$$"
    else
        echo "$1"
    fi
}

# Replace $1 with the already-written temp file $2, keeping $1 as $3. The
# temp file takes $1's owner and mode. On any failure $1 is left as it was.
replace_keeping() {
    chown --reference="$1" "$2" 2>/dev/null
    chmod --reference="$1" "$2" 2>/dev/null
    if mv -f "$1" "$3"; then
        mv -f "$2" "$1" && return 0
        mv -f "$3" "$1"
    fi
    rm -f "$2"
    return 1
}

# NodeWorx's own Roundcube setting in iworx.ini: 1, 0, or nothing if unknown.
ini_roundcube() {
    [ -r "$INI" ] || return 0
    awk '/^[[:space:]]*\[/ { s = $0; gsub(/[[:space:]]/, "", s); next }
         s == "[roundcube]" && /^[[:space:]]*enabled[[:space:]]*=/ {
             v = $0; sub(/^[^=]*=/, "", v); gsub(/["[:space:]]/, "", v); print v; exit
         }' "$INI"
}

# Turn Roundcube off in iworx.ini, so NodeWorx shows what httpd-iworx serves.
roundcube_off_in_panel() {
    [ "$(ini_roundcube)" = 1 ] || return 0
    [ -f "$INI" ] && [ ! -L "$INI" ] || return 1
    TMP=$(mktemp "$INI.XXXXXX") || return 1
    if awk '/^[[:space:]]*\[/ { s = $0; gsub(/[[:space:]]/, "", s) }
            s == "[roundcube]" && /^[[:space:]]*enabled[[:space:]]*=/ { print "enabled=\"0\""; next }
            { print }' "$INI" > "$TMP"; then
        chown --reference="$INI" "$TMP" 2>/dev/null
        chmod --reference="$INI" "$TMP" 2>/dev/null
        mv -f "$TMP" "$INI" && return 0
    fi
    rm -f "$TMP"
    return 1
}

# The file, then every file it Includes, recursively (as httpd.conf loads them).
list_conf() {
    [ -f "$1" ] && [ "$2" -lt 6 ] || return 0
    echo "$1"
    grep -Ei '^[[:space:]]*Include(Optional)?[[:space:]]' "$1" | awk '{ p = $2; gsub(/"/, "", p); print p }' | while IFS= read -r INC; do
        case $INC in
            /*) ;;
            *) INC=$SERVERROOT/$INC ;;
        esac
        for F in $INC; do
            if [ -d "$F" ]; then
                for G in "$F"/*; do list_conf "$G" $(($2 + 1)); done
            else
                list_conf "$F" $(($2 + 1))
            fi
        done
    done
}

# The previous httpd-iworx package, its digest of a file, and the tool to
# compute one the same way.
OLDVR=
[ -n "$THISVR" ] && OLDVR=$(rpm -q --qf '%{VERSION}-%{RELEASE}\n' httpd-iworx 2>/dev/null | grep -vxF "$THISVR" | head -n 1)
prev_digest() {
    [ -n "$OLDVR" ] || return 0
    rpm -q --qf '[%{FILEDIGESTS} %{FILENAMES}\n]' "httpd-iworx-$OLDVR" 2>/dev/null | awk -v f="$1" '$2 == f { print $1; exit }'
}
prev_digest_cmd() {
    case $(rpm -q --qf '%{FILEDIGESTALGO}' "httpd-iworx-$OLDVR" 2>/dev/null) in
        2) echo sha1sum ;;
        8) echo sha256sum ;;
        9) echo sha384sum ;;
        10) echo sha512sum ;;
        *) echo md5sum ;;
    esac
}

for APPCONF in "$APPDIR"/10-*.conf; do
    NAME=${APPCONF##*/}
    NEW=$PRISTINE/$NAME
    [ -f "$APPCONF" ] && [ -f "$NEW" ] || continue
    MIGRATE=
    has_fastcgi "$APPCONF" && MIGRATE=1
    [ -f "$APPCONF.rpmnew" ] && ! has_old_layout "$APPCONF.rpmnew" && has_old_layout "$APPCONF" && MIGRATE=1
    [ -n "$MIGRATE" ] || continue
    if [ -L "$APPCONF" ]; then
        warn "WARNING: $APPCONF needs converting to run under the event MPM, but it is a symlink, so it was left alone. Replace it with $NEW and re-apply your changes."
        continue
    fi

    # ok: only the Alias toggles differ from a shipped copy; closed: turn the
    # app off; unknown: can't tell
    MINE=$(untoggled "$APPCONF" | sha256sum | awk '{ print $1 }')
    STOCK=$(prev_digest "$APPCONF")
    WHY=
    if known_stock "$MINE  $NAME"; then
        VERDICT=ok
    elif [ -n "$STOCK" ] && SUM=$(prev_digest_cmd) && [ "$STOCK" = "$(untoggled "$APPCONF" | $SUM | awk '{ print $1 }')" ]; then
        VERDICT=ok
    elif [ -n "$STOCK" ]; then
        VERDICT=closed
        WHY="it differs from every copy an httpd-iworx package shipped by more than enabling/disabling the app"
    elif has_restrictions "$APPCONF"; then
        VERDICT=closed
        WHY="it holds access restrictions and the previous httpd-iworx package's copy is not available to compare it with"
    else
        VERDICT=unknown
    fi
    # A pristine copy without Alias lines (EL9's Horde redirect) has nothing to turn off.
    if [ "$VERDICT" = closed ] && ! grep -Eqi '^[[:space:]]*alias[[:space:]]' "$NEW"; then
        VERDICT=replaced
    fi

    TMP=$(mktemp "$APPDIR/.$NAME.XXXXXX") || continue
    if [ "$VERDICT" = closed ]; then
        awk 'tolower($1) == "alias" { print "#" $0; next } { print }' "$NEW" > "$TMP"
    else
        # Re-comment each Alias the old copy had commented out.
        awk 'FNR == NR {
                 if ($1 ~ /^#+$/ && tolower($2) == "alias") { k = $3 }
                 else if (tolower($1) ~ /^#+alias$/) { k = $2 }
                 else { next }
                 gsub(/"/, "", k); off[k] = 1
                 next
             }
             tolower($1) == "alias" { k = $2; gsub(/"/, "", k); if (k in off) { print "#" $0; next } }
             { print }' "$APPCONF" "$NEW" > "$TMP"
    fi
    SAVE=$(backup_name "$APPCONF.rpmsave")
    if ! [ -s "$TMP" ] || ! replace_keeping "$APPCONF" "$TMP" "$SAVE"; then
        rm -f "$TMP"
        warn "WARNING: could not convert $APPCONF to run under the event MPM, so httpd-iworx may not start. Replace it with $NEW and re-apply your changes."
        continue
    fi
    # A leftover .rpmnew is either the split layout just applied or $NEW itself.
    [ -f "$APPCONF.rpmnew" ] && ! has_old_layout "$APPCONF.rpmnew" && rm -f "$APPCONF.rpmnew"
    [ -f "$APPCONF.rpmnew" ] && cmp -s "$NEW" "$APPCONF.rpmnew" && rm -f "$APPCONF.rpmnew"
    echo "httpd-iworx: $APPCONF replaced by this package's copy, with the rest of the app's config, where there is any, in $APPDIR/11-${NAME#10-}; previous copy saved as $SAVE"
    case $VERDICT in
        closed)
            if [ "$NAME" = 10-webmail-roundcube.conf ]; then
                if roundcube_off_in_panel; then
                    HOW="Roundcube is also turned off in NodeWorx; turn it back on there (Settings) once you have reviewed it."
                else
                    HOW="NodeWorx could not be updated to match and may still show Roundcube as on: turn it off and back on there (Settings) once you have reviewed it."
                fi
            else
                HOW="Re-enable it by uncommenting its Alias lines in $APPCONF once you have reviewed it."
            fi
            warn "WARNING: $APPCONF had to be replaced to run under the event MPM, and $WHY, so the app is OFF pending review: every Alias line in the new file is commented out. Review $SAVE and re-apply anything still needed, e.g. access restrictions such as Require ip, in a file of your own in $APPDIR such as 90-local.conf. $HOW"
            ;;
        replaced)
            warn "WARNING: $APPCONF had to be replaced to run under the event MPM, and $WHY. The new file has no Alias lines to turn off, so nothing was turned off. Review $SAVE and re-apply anything still needed, e.g. access restrictions such as Require ip, in a file of your own in $APPDIR such as 90-local.conf."
            ;;
        unknown)
            warn "WARNING: could not verify whether $APPCONF had local changes besides enabling/disabling the app (the previous httpd-iworx package's copy is not available to compare it with). Only its enabled/disabled state was carried over. Review $SAVE and re-apply anything still needed, e.g. access restrictions, in a file of your own in $APPDIR such as 90-local.conf."
            ;;
    esac
done

# mod_fastcgi directives anywhere else httpd.conf loads. Files it doesn't
# load, such as iworx-mod_fastcgi's own fastcgi.conf, are left alone.
list_conf "$HTTPD_CONF" 0 | sort -u | while IFS= read -r CONF; do
    has_fastcgi "$CONF" || continue
    if [ -L "$CONF" ]; then
        warn "WARNING: $CONF uses mod_fastcgi directives, which the event MPM can't load, so httpd-iworx will not start; it is a symlink, so it was left alone. Convert them to mod_proxy_fcgi (see conf.d/iworx-apps/11-*.conf for the pattern)."
        continue
    fi
    TMP=$(mktemp "$CONF.XXXXXX") || continue
    BAK=$(backup_name "$CONF.fastcgi.bak")
    if awk 'c || tolower($0) ~ /^[ \t]*fastcgi[a-z0-9]*[ \t]/ {
                print "#[httpd-iworx: disabled, mod_fastcgi is not supported under the event MPM] " $0
                c = ($0 ~ /\\$/)
                next
            }
            { print }' "$CONF" > "$TMP" && [ -s "$TMP" ] && replace_keeping "$CONF" "$TMP" "$BAK"; then
        warn "WARNING: $CONF used mod_fastcgi directives, which the event MPM can't load and which stopped httpd-iworx from starting. They are commented out, marked with \"httpd-iworx: disabled\"; the original is saved as $BAK. Convert them to mod_proxy_fcgi if still needed (see conf.d/iworx-apps/11-*.conf for the pattern)."
    else
        rm -f "$TMP"
        warn "WARNING: $CONF uses mod_fastcgi directives, which the event MPM can't load, and could not be rewritten, so httpd-iworx will not start. Comment them out or convert them to mod_proxy_fcgi (see conf.d/iworx-apps/11-*.conf for the pattern)."
    fi
done

# Diagnostic only, never changes anything: if the upgraded config won't load,
# say why now rather than leaving a silently dead panel.
if [ -n "$UPGRADE" ] && ! CFGTEST=$("$IWORX_HOME/bin/httpd/httpd" -t -DSSL -f "$HTTPD_CONF" 2>&1); then
    echo "httpd-iworx: WARNING: the panel web server configuration does not load, so httpd-iworx will not start:" >&2
    printf '%s\n' "$CFGTEST" >&2
    if printf '%s\n' "$CFGTEST" | grep -qi fastcgi; then
        echo "httpd-iworx: mod_fastcgi is not available under the event MPM; convert FastCGI directives to mod_proxy_fcgi (see conf.d/iworx-apps/11-*.conf for the pattern)." >&2
    fi
    logger -t httpd-iworx "panel web server configuration does not load after upgrade: $(printf '%s' "$CFGTEST" | tail -n 1)" 2>/dev/null || :
fi
exit 0
