#!/bin/bash
### Copyright 1999-2025. WebPros International GmbH. All rights reserved.

export LANG="C"
export LC_ALL="C"

set -u

# Here you can define checking routines list for every mode
brief_list="
    get_sysinfo
    get_common_info
    get_common_logs
    get_ipconfig
    get_mounts_info
    get_last
    get_selinux_info
    get_pkglist
    get_components
    get_system_users
    get_system_services
    get_ai_log
    get_apache_info
    get_nginx_info
    get_cpserver_info
    get_php_info
    get_psa_conf
    get_plesk_repair_status
"

# removed from brief: get_history

middle_list="
    $brief_list
    get_db_misc
    get_db_eventhandlers
    get_maillog
    get_mail_handlers_list
    get_vhost_conf
    get_php_conf
    get_pmm_info
    get_webmail_conf
"

full_list="
    $middle_list
    get_db_dumps
"

MYSQL_BIN_D="/usr/bin"

# common options
curdir="`dirname $0`"
tmp_d="/usr/local/psa/var"
infodir="$tmp_d/collect"
info_plesk="$infodir/plesk"
info_system="$infodir/system"
info_db="$infodir/db"

my_access="-uadmin -p`cat /etc/psa/.psa.shadow`"

# global options
pkgtype=''  # defined in get_sysinfo
os_descr='' # defined in get_sysinfo
root_d=''   # defined in get_sysinfo

# Common functions
# ---------------------------------------------------------------------------------------
help()
{
cat <<-EOT
USAGE: $progname [--quiet] [--output FILE] [--mode [brief|middle|full]]
       $progname --help

OPTIONS:
    --mode      Set required verbosity level. See MODES section
                to get an additional info.
    --quiet     Do not output any info on stdout.
    --output    Specify file where data will be saved.
    --sanitizer Specify path to sanitizer unitity.
    --tail-size Number of lines collected from logfiles.
    --help      Show this message

MODES:
    brief       Short basic info.
    middle      Detailed info about system and product. This is default.
    full        Includes detailed info, product logs and database dumps.
EOT
}

parse_options()
{
	local count="$#"

	# global params:
	progname="`basename $0`"
	mode=""
	quiet=""
	output=""
	sanitize_bin="/usr/local/psa/admin/sbin/sanitize"
	tail_size=1000
	actions_list=""

	local TEMP
	TEMP=`getopt -o m:qo:S:t:h --long mode:,quiet,output:,sanitizer:,tail-size:,actions:,help -n "$progname" -- "$@"`
	if [ $? != 0 ]; then
		echo "Internal error in parsing options" >&2
		exit 1
	fi
	eval set -- "$TEMP"

	while true; do
		case "$1" in
			-m|--mode)
				case "$2" in
					brief) actions_list="$brief_list" ;;
					middle) actions_list="$middle_list" ;;
					full) actions_list="$full_list" ;;
					*) help && exit 1 ;;
				esac
				shift 2
				;;
			-q|--quiet)
				quiet=1;
				exec >/dev/null
				shift
				;;
			-o|--output) output="$2"; shift 2;;
			-h|--help) help && exit 0;;
			-S|--sanitizer) sanitize_bin="$2"; shift 2;;
			-t|--tail-size) tail_size="$2"; shift 2;;
			--actions) actions_list="$2"; shift 2;;
			--) shift; break;;
			*) echo "Internal error in processing options: '$1'"; exit 1;;
		esac
	done

	# allow relative path for $output:
	if [ -n "$output" ]; then
		expr match "$output" "/" >/dev/null || output="`pwd`/$output"
	fi

	# allow relative path for $sanitize_bin
	if [ -n "$sanitize_bin" ]; then
		expr match "$sanitize_bin" "/" >/dev/null || sanitize_bin="`pwd`/$sanitize_bin"
	fi

	# default mode is middle:
	[ -n "$actions_list" ] || actions_list="$middle_list"
}

ok()
{
    [ ! -z "$quiet" ] || echo "ok"
}

info()
{
    [ ! -z "$quiet" ] || echo "$*"
}

try()
{
    [ ! -z "$quiet" ] || echo -n "Trying to $*... "
}

fail()
{
    [ ! -z "$quiet" ] || echo "failed"
}

die()
{
    echo "ERROR: $*.. exiting..." >&2
    exit 1
}


is_function()
{
    local type_output=$(type -t "$1")
    test "X${type_output}" = "Xfunction"
}

cleanup()
{
    rm -rf $infodir >/dev/null 2>&1
    rm -f $tmp_d/collect.zip
}

cp_tail()
{
	local src="$1"
	local dest="$2"

	expr match "$dest" / > /dev/null || dest="`pwd`/$dest"

	if [ -d "$dest" ]; then
		dest="$dest/`basename $src`"
	fi

	if [ -d "$src" ]; then
		[ -e "$dest" ] || mkdir "$dest"
		pushd "$src" > /dev/null
		find -type d -exec mkdir -p "$dest/{}" \;
		for i in `find -type f`; do
			tail -n "$tail_size" "$i" > "$dest/$i"
		done
		popd > /dev/null
	else
		tail -n "$tail_size" "$src" > "$dest"
	fi
}

db_do()
{
    local query="$*"
    local mysql_client="$MYSQL_BIN_D/mysql"
    if [ -f "$MYSQL_BIN_D/mariadb" ]; then
        mysql_client="$MYSQL_BIN_D/mariadb"
    fi

    echo "$query" | "$mysql_client" -n -N $my_access psa
}

db_dump()
{
	local database="$1"
	local table where my_opts
	local dump_client="$MYSQL_BIN_D/mysqldump"
	[ -f "$MYSQL_BIN_D/mariadb-dump" ] && dump_client="$MYSQL_BIN_D/mariadb-dump"

	my_opts="--skip-extended-insert --skip-quick --skip-lock-tables --databases $1"

	shift
	[ ! "$#" -ge 1 ] || my_opts+=" --tables $1"

	"$dump_client" $my_access $my_opts
}

collect()
{
    local functions="$1"

    for func in $functions; do
        if is_function $func; then
            $func
            continue
        fi
        echo "Internal error: Unable to find a function '$func'. Skipping..."
    done
}

sanitize()
{
	try "filter out user private data"
	[ -x "$sanitize_bin" ] || die "$sanitize_bin not found"
	"$sanitize_bin" "$tmp_d/collect"
	ok
}

pack()
{
	local output_path="$1"
	[ -n "$output_path" ] || output_path="$tmp_d/collect.zip"
	try "pack gathered info to '$output_path'"

	local zip_bin="`which zip 2>/dev/null`"
	[ -z "$zip_bin" ] && die "ZIP archiver didn't find"

	pushd "$(dirname "$infodir")" > /dev/null
	local include_list="$(mktemp "$tmp_d/include.lst.XXXXXX")"
	find "$(basename "$infodir")" -type f > "$include_list"
	# prevent zip from adding .zip to output_path
	$zip_bin -r -q -b . - "$(basename "$infodir")" --include @"$include_list" > "$output_path"
	popd > /dev/null

	rm -f "$include_list"
	rm -rf "$infodir"

	ok
}

# Routines
# ---------------------------------------------------------------------------------------

get_sysinfo()
{
    try "collect system info"

    res="`/bin/uname -m`"
    [ "$res" = "amd64" -o "$res" = "x86_64" ] && arch="x86_64" || arch="i386"

    found_file=""
    for f in redhat-release SuSE-release lsb-release debian_version; do
            [ -f "/etc/$f" ] && found_file="$f" && break
    done

    case $found_file in
        redhat-release)
            pkgtype="rpm"
            os_descr="`cat /etc/$found_file` $arch"
            root_d="/usr/local/psa"
        ;;
        SuSE-release)
            pkgtype="rpm"
            os_descr="`cat /etc/$found_file | head -n 1` $arch"
            root_d="/usr/local/psa"
        ;;
        lsb-release)
            pkgtype="deb"
            os_descr="`awk -F '=' '/DISTRIB_DESCRIPTION/ {print $2}' < /etc/$found_file | tr -d '"'` $arch"
            root_d="/opt/psa"
        ;;
        debian_version)
            pkgtype="deb"
            os_descr="Debian `cat /etc/$found_file` $arch"
            root_d="/opt/psa"
        ;;
        *)
            fail
            die "Unsupported os has been detected"
        ;;
    esac

    ok
}

get_ipconfig()
{
    try "collect IP configuration"
    mkdir -p $info_system
    ifconfig > $info_system/ipconfig.txt 2>&1
    ok
}

get_mounts_info()
{
    try "get mountpoints and volumes info"
    mkdir -p $info_system
    mount > $info_system/mounts.txt
    echo "" >> $info_system/mounts.txt
    df -h >> $info_system/mounts.txt
    ok
}

get_last()
{
    try "get last events"
    mkdir -p $info_system
    last > $info_system/last.txt
    ok
}

get_db_misc()
{
    try "collect info from psa.misc database table"
    mkdir -p $info_db
    db_dump "psa" "misc" > $info_db/misc.sql
	ok
}

get_db_eventhandlers()
{
    try "collect info from psa.event_handlers"
    mkdir -p $info_db
    db_dump "psa" "event_handlers" > $info_db/event_handlers.sql
    ok
}

get_selinux_info()
{
    try "get selinux status"

    enforced="`getenforce 2>/dev/null`"

    [ "$pkgtype" != "rpm" -o -z "$enforced" ] \
        && info "not installed" \
        && return

    mkdir -p $info_system
    echo -e "SELinux mode:\n    $enforced" >  $info_system/selinux.txt

    ok
}

get_common_info()
{
    try "collect Plesk common info"

    [ ! -f "$root_d/version" ] && fail && return || info="`cat $root_d/version`"

    up_hist="`db_do "select * from upgrade_history"`"

    domains="`db_do "select count(*) from domains"`"

    case $pkgtype in
        rpm) nginx_str="`head -1 /etc/sysconfig/nginx`" ;;
        deb) nginx_str="`head -1 /etc/default/nginx`" ;;
        *) die "Unsupported OS" ;;
    esac
    nginx_status="`echo $nginx_str | awk -F '=' '{print $2}' | tr -d '"'`"

    vz_env_id="`awk '/envID/ {print $2}' /proc/self/status`"
    if [ -z "$vz_env_id" -o "$vz_env_id" = "0" ]; then
        vz_status="Nope. Is not VZ."
        counters="Not found."
    else
        vz_status="Yep. Is VZ."
        counters="`cat /proc/user_beancounters`"
    fi

    mkdir -p $infodir
    out="$infodir/common_info.txt"

    echo "OS:" > $out
    echo "    $os_descr" >> $out
    echo "" >> $out

    echo "Is virtuozzo container?:" >> $out
    echo "    $vz_status" >> $out
    echo "" >> $out

    echo "Product:" >> $out
    echo "    $info" >> $out
    echo "" >> $out

    echo "Upgrade history:" >> $out
    if [ -z "$up_hist" ]; then
        echo "    Clean installation." >> $out
    else
        IFS_OLD="$IFS"
        IFS=$'\n'
        for str in $up_hist; do
            echo "    $str" >> $out
        done
        IFS="$IFS_OLD"
    fi
    echo "" >> $out

    echo "Count of domains:" >> $out
    echo "    $domains" >> $out
    echo "" >> $out

    echo "Nginx enabled:" >> $out
    echo "    $nginx_status" >> $out
    echo "" >> $out

    echo "Collected info date:" >> $out
    echo "    `date +%s` (`date -u`)" >> $out
    echo "" >> $out

    echo "User Beancounters:" >> $out
    echo "    $counters" >> $out
    echo "" >> $out

    cp -f /etc/sw/keys/registry.xml $infodir/

    ok
}

get_common_logs()
{
    try "collect common system logs"
    dstdir="$info_system/var_log"
    mkdir -p $dstdir
    for entry in cron mail.info syslog maillog messages mysqld.log secure yum.log audit dmesg; do
        [ -e "/var/log/$entry" ] && cp_tail /var/log/$entry $dstdir/
    done
    ok
}

get_pkglist()
{
    try "get packages list"

    mkdir -p $info_system
    case $pkgtype in
        rpm)
                rpm -qa >$info_system/pkglist.txt 2>&1
        ;;
        deb)
                dpkg --list > $info_system/pkglist.txt 2>&1
        ;;
        *)
            die "Unknown packages type."
        ;;
    esac

    ok
}

get_components()
{
    try "get Plesk components list"
    mkdir -p $info_plesk
    $root_d/admin/sbin/packagemng --list > $info_plesk/components.txt 2>&1
    ok
}

get_history()
{
    try "get shell history"
    history > $info_system/history.txt 2>&1
    ok
}

get_system_users()
{
    try "get system users and groups"
    mkdir -p $info_system
    cp -f /etc/passwd $info_system/sysusers.txt
    cp -f /etc/group  $info_system/sysgroups.txt
    ok
}

get_system_services()
{
    try "get system active services list"
    mkdir -p $info_system

    if [ -x "/bin/systemctl" ]; then
        echo >> "$info_system/services.txt"
        /bin/systemctl list-unit-files >> "$info_system/services.txt"
    fi

    top -b -n 1 > $info_system/top.txt 2>&1
    ps auxwww > $info_system/ps.txt 2>&1
    ok
}

get_ai_log()
{
    try "get autoinstaller logs"
    mkdir -p "$info_plesk"
    local latest_ai_log="`ls -t /var/log/plesk/install/autoinstaller3*.log 2>/dev/null | head -n 1`"
    [ -z "$latest_ai_log" ] || cp_tail "$latest_ai_log" "$info_plesk/"
    ok
}

get_mail_handlers_list()
{
    try "get mail handlers info"
    mkdir -p $info_plesk
    $root_d/admin/sbin/mail_handlers_control --list > $info_plesk/mhandlers.txt
    echo "" >> $info_plesk/mhandlers.txt
    $root_d/admin/sbin/mail_handlers_control --list --extent >> $info_plesk/mhandlers.txt
    ok
}

get_apache_info()
{
    try "collect customer's web server configuration"

    local cmd
    local out
    local subdir

    mkdir -p $info_plesk/apache/logs
    out="$info_plesk/apache/common.txt"

    cmd="`which httpd 2>/dev/null`"
    [ -z "$cmd" ] && cmd="`which apache 2>/dev/null`"
    [ -z "$cmd" ] && cmd="`which apache2 2>/dev/null`"
    [ -z "$cmd" ] && echo "apache not found.. " && fail && return

    [ "$pkgtype" = "rpm" ] && cmd_bin="$cmd" || cmd_bin="apachectl"

    $cmd_bin -t >> $out 2>&1
    echo "" >> $out
    $cmd_bin -V >> $out 2>&1
    echo "" >> $out
    $cmd_bin -M >> $out 2>&1
    echo "" >> $out

    $cmd_bin -S > $info_plesk/apache/vhosts.txt 2>&1

    subdir="`basename $cmd`"

    [ ! -s "/var/log/$subdir/error_log" ] || cp_tail /var/log/$subdir/error_log $info_plesk/apache/logs/
    cp -fR /etc/$subdir $info_plesk/apache/conf

    ok
}

get_nginx_info()
{
    try "collect nginx web server info"
    mkdir -p $info_plesk/nginx
    cp -fR /etc/nginx/* $info_plesk/nginx/
    ok
}

get_cpserver_info()
{
    try "collect cp-server info"
    dstdir="$info_plesk/cp-server"

    mkdir -p $dstdir/etc/conf $dstdir/var_logs
    cp -fR /etc/sw-cp-server/* $dstdir/etc/
    for l in error_log sw-engine.log; do
        [ ! -s "/var/log/sw-cp-server/$l" ] || cp_tail "/var/log/sw-cp-server/$l" "$dstdir/var_logs/"
    done

    mkdir -p $dstdir/admin
    cp -fR $root_d/admin/conf $dstdir/admin/
    for l in panel.log sitebuilder.log; do
        [ ! -s "/var/log/plesk/$l" ] || cp_tail "/var/log/plesk/$l" "$dstdir/admin/"
    done
    # Remove pem files that store private key to avoid leakage of them
    rm -f $dstdir/admin/conf/*.pem*
    ok
}

get_php_info()
{
    try "collect info about installed php"
    mkdir -p $info_plesk
    php -r "@phpinfo();" < /dev/null > $info_plesk/phpinfo.txt
    ok
}

get_psa_conf()
{
    try "get psa common configuration"
    mkdir -p $info_plesk/etc_psa
    for l in psa.conf php_versions.json; do
        [ ! -s "/etc/psa/$l" ] || cp -f /etc/psa/$l $info_plesk/etc_psa/
    done
    ok
}

get_webmail_conf()
{
    try "get webmails configuration"
    mkdir -p $info_plesk/webmails
    cp -fR /etc/psa-webmail/* $info_plesk/webmails/
    ok
}

get_maillog()
{
    try "collect mail logs"
    mkdir -p $info_plesk
    cp_tail $root_d/var/log $info_plesk/maillog
    ok
}

get_vhost_conf()
{
    try "collect info from virtual domains"
    local vhosts_d
    vhosts_d="`awk '/^HTTPD_VHOSTS_D/ {print $2}' /etc/psa/psa.conf`"
    is_new_vhost="`which plesk 2>/dev/null`"
    count=0
    for domain in `db_do "select name from domains"`; do
        [ $count -eq 100 ] && echo -n "." && count=0
        [ -n "$is_new_vhost" ] \
            && srcdir="$vhosts_d/system/$domain" \
            || srcdir="$vhosts_d/$domain"

        mkdir -p $info_plesk/vhosts/$domain/logs
        for l in error_log proxy_error_log; do
            [ ! -s "$srcdir/logs/$l" ] || cp_tail $srcdir/logs/$l $info_plesk/vhosts/$domain/logs
        done
        cp -fR $srcdir/conf $info_plesk/vhosts/$domain/ 2>/dev/null
        count=$(expr $count + 1)
    done
    ok
}

get_php_conf()
{
    try "collect php configs"
    mkdir -p $info_plesk/php
    find /etc -maxdepth 1 -type f -name "*php*" -exec cp -f {} $info_plesk/php/ \;
    for p in $(find /etc -maxdepth 1 -type d -name "*php*"); do
        pushd "$p" >/dev/null
        find . -type d -exec mkdir -p $info_plesk/php/$(basename $p)/{} \;
        find . -type f -name "*.ini" -exec cp -f {} $info_plesk/php/$(basename $p)/{} \;
        find . -type f -name "*.conf" -exec cp -f {} $info_plesk/php/$(basename $p)/{} \;
        popd >/dev/null
    done
    ok
}

get_plesk_perms()
{
    try "collect plesk permissions"
    mkdir -p $info_plesk
    find $root_d -type d -exec ls -ld {} \; \
        | awk '{print $1" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "$10" "$11}' \
        > $info_plesk/permissions.txt
    ok
}

get_pmm_info()
{
	try "collect backup/migration logs"

	local pmmd='/var/log/plesk/PMM'
	[ -d "$pmmd" ] || { ok; return; }

	local dstdir
	local srcdir

	dstdir="$info_plesk/PMM"
	mkdir -p $dstdir

	local cur_month=`date +"%Y-%m"`
	local prev_month=`date +"%Y-%m" --date='last month'`
	

	pushd "$pmmd" > /dev/null
	for f in `ls -d *.log *$cur_month* *$prev_month* 2>/dev/null | sort -u`; do
		cp_tail "$f" "$dstdir"
	done
	popd > /dev/null

	ok
}

get_db_dumps()
{
    mkdir -p $info_db

    try "get psa database dump"
    db_dump psa > $info_db/psa.sql
    ok

    try "get apsc database dump"
    db_dump apsc > $info_db/apsc.sql
    ok
}

get_plesk_repair_status()
{
	mkdir -p "$info_plesk"
	"$root_d/bin/repair" --all -n >"$info_plesk/repair_status.txt" 2> "$info_plesk/repair_status.stderr.txt"
}

# --------------------------------------------------------------------------------------------

if [ "`id -u`" -ne 0 ]; then
	echo "$0: This script must be run as root" >&2
	echo "Log in as root then run this script again." >&2
	echo >&2
	exit 1
fi

parse_options "$@"

cleanup
collect "$actions_list"

sanitize

pack "$output"
