#!/bin/bash
# info: update backup exclusion list
# options: USER FILE
#
# example: v-update-user-backup-exclusions admin /tmp/backup_exclusions
#
# This function for updating backup exclusion list

#----------------------------------------------------------#
#                Variables & Functions                     #
#----------------------------------------------------------#

# Argument definition
user=$1
vfile=$2

# Includes
# shellcheck source=/etc/tuliocp/tulio.conf
source /etc/tuliocp/tulio.conf
# shellcheck source=/usr/local/tulio/func/main.sh
source $TULIO/func/main.sh
# load config file
source_conf "$TULIO/conf/tulio.conf"

is_file_available() {
	if [ ! -e "$vfile" ]; then
		check_result "$E_NOTEXIST" "file $vfile doesn't exist"
	fi
}

is_file_valid() {
	local allowed_keys=' WEB DNS MAIL DB CRON USER '
	# WEB subpaths may nest, e.g. public_html/cache/
	local path_segment_re='^[a-zA-Z0-9_.][a-zA-Z0-9_./-]*$'
	# MAIL account names are never nested paths
	local mailbox_re='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*$'
	# USER entries are bare directory names, no domain prefix
	local dirname_re='^[a-zA-Z0-9_.][a-zA-Z0-9_./-]*$'
	local line key value entry entries domain_part parts i seg

	while IFS= read -r line || [ -n "$line" ]; do
		[ -z "$line" ] && continue

		# Each line must be a single KEY='...' declaration on one line;
		# this also rejects embedded newlines smuggled inside a value.
		if [[ ! "$line" =~ ^([A-Z]+)=\'(.*)\'$ ]]; then
			check_result "$E_INVALID" "invalid exclusion list format"
		fi
		key="${BASH_REMATCH[1]}"
		value="${BASH_REMATCH[2]}"

		if [[ "$allowed_keys" != *" $key "* ]]; then
			check_result "$E_INVALID" "invalid exclusion list key :: $key"
		fi

		[ -z "$value" ] && continue

		# CRON has no per-job exclusion downstream (bin/v-backup-user only
		# ever checks "$CRON" = '*'), so anything else would silently do
		# nothing. Only the whole-field wildcard is accepted.
		if [ "$key" = 'CRON' ] && [ "$value" != '*' ]; then
			check_result "$E_INVALID" "cron exclusions only support * :: $value"
		fi

		IFS=',' read -ra entries <<< "$value"
		for entry in "${entries[@]}"; do
			[ -z "$entry" ] && continue

			# Reject path traversal anywhere in the entry
			if [[ "$entry" == *..* ]]; then
				check_result "$E_INVALID" "invalid exclusion entry :: $entry"
			fi

			case "$key" in
				WEB | MAIL)
					# entry := (domain|*)[:segment]*
					IFS=':' read -ra parts <<< "$entry"
					domain_part="${parts[0]}"
					if [ "$domain_part" != '*' ]; then
						is_domain_format_valid "$domain_part" "exclusion domain"
					fi

					for ((i = 1; i < ${#parts[@]}; i++)); do
						seg="${parts[$i]}"
						[ -z "$seg" ] && continue
						# No leading '-' (would be read as a tar/du flag downstream)
						if [ "$key" = 'MAIL' ]; then
							if [[ ! "$seg" =~ $mailbox_re ]]; then
								check_result "$E_INVALID" "invalid exclusion entry :: $entry"
							fi
						else
							if [[ ! "$seg" =~ $path_segment_re ]]; then
								check_result "$E_INVALID" "invalid exclusion entry :: $entry"
							fi
						fi
					done
					;;
				DNS)
					# No record-level exclusion exists downstream, only a
					# whole-domain match. entry := domain | *
					if [ "$entry" != '*' ]; then
						if [[ "$entry" == *:* ]]; then
							check_result "$E_INVALID" "invalid exclusion entry :: $entry"
						fi
						is_domain_format_valid "$entry" "exclusion domain"
					fi
					;;
				DB)
					# entry := database-name | *
					if [ "$entry" != '*' ]; then
						is_database_format_valid "$entry" "exclusion database"
					fi
					;;
				USER)
					# entry := directory-name | *
					if [ "$entry" != '*' ] && [[ ! "$entry" =~ $dirname_re ]]; then
						check_result "$E_INVALID" "invalid exclusion entry :: $entry"
					fi
					;;
			esac
		done
	done < "$vfile"
}

#----------------------------------------------------------#
#                    Verifications                         #
#----------------------------------------------------------#

check_args '2' "$#" 'USER FILE'
is_format_valid 'user'
is_object_valid 'user' 'USER' "$user"
is_file_available
is_file_valid

# Perform verification if read-only mode is enabled
check_tulio_demo_mode

#----------------------------------------------------------#
#                       Action                             #
#----------------------------------------------------------#

# Flush variables
WEB=''
DNS=''
MAIL=''
DB=''
CRON=''
USER=''

# Source exclusion list
source_conf "$vfile"

# Updating exlusion list
echo "WEB='$WEB'" > $USER_DATA/backup-excludes.conf
echo "DNS='$DNS'" >> $USER_DATA/backup-excludes.conf
echo "MAIL='$MAIL'" >> $USER_DATA/backup-excludes.conf
echo "DB='$DB'" >> $USER_DATA/backup-excludes.conf
echo "CRON='$CRON'" >> $USER_DATA/backup-excludes.conf
echo "USER='$USER'" >> $USER_DATA/backup-excludes.conf
chmod 660 $USER_DATA/backup-excludes.conf

#----------------------------------------------------------#
#                       Tulio                             #
#----------------------------------------------------------#

# Logging
$BIN/v-log-action "$user" "Info" "Backups" "Updated backup exclusion list."
log_event "$OK" "$ARGUMENTS"

exit
