#!/bin/bash

# (C) Jules Field <Jules@Zend.To> 2022 for ZendTo.

# Docker entry point.
# ZendTo will have been installed.
# However, if preferences.php and zendto.ini have not been customised
# yet, then we need to do that first.
# This is mostly worked from the MySQL official Docker image.

set -o pipefail
shopt -s nullglob

ZTCONF=/opt/zendto/config/zendto.conf
ZTPREFS=${ZENDTOPREFS:-/opt/zendto/config/preferences.php}
ZTINTERNAL=/opt/zendto/config/internaldomains.conf
UPGRADE=/opt/zendto/bin/upgrade
OS=Docker

# logging functions
zendto_log() {
	local type="$1"; shift
	# accept argument string or stdin
	local text="$*"; if [ "$#" -eq 0 ]; then text="$(cat)"; fi
	local dt; dt="$(date --rfc-3339=seconds)"
	printf '%s [%s] [Entrypoint]: %s\n' "$dt" "$type" "$text"
}
zendto_note() {
	zendto_log Note "$@"
}
zendto_warn() {
	zendto_log Warn "$@" >&2
}
zendto_error() {
	zendto_log ERROR "$@" >&2
	exit 1
}

# usage: file_env VAR [DEFAULT]
#    ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
#  "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
	local var="$1"
	local fileVar="${var}_FILE"
	local def="${2:-}"
	if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
		mysql_error "Both $var and $fileVar are set (but are exclusive)"
	fi
	local val="$def"
	if [ "${!var:-}" ]; then
		val="${!var}"
	elif [ "${!fileVar:-}" ]; then
		val="$(< "${!fileVar}")"
	fi
	export "$var"="$val"
	unset "$fileVar"
}

# check to see if this file is being run or sourced from another script
_is_sourced() {
	# https://unix.stackexchange.com/a/215279
	[ "${#FUNCNAME[@]}" -ge 2 ] \
		&& [ "${FUNCNAME[0]}" = '_is_sourced' ] \
		&& [ "${FUNCNAME[1]}" = 'source' ]
}

# usage: docker_process_init_files [file [file [...]]]
#    ie: docker_process_init_files /always-initdb.d/*
# process initializer files, based on file extensions
docker_process_init_files() {
	echo
	local f
	for f; do
		case "$f" in
			*.sh)
				if [ -x "$f" ]; then
					zendto_note "$0: running $f"
					"$f"
				else
					zendto_note "$0: sourcing $f"
					. "$f"
				fi
				;;
			*)
				zendto_warn "$0: ignoring $f" ;;
		esac
		echo
	done
}

# Verify the minimum environment is set for us to self-configure
docker_verify_minimum_env() {
	if [ -z "$ZENDTO_URL" -o -z "$ZENDTO_ORG_IP_SUBNETS" -o -z "$ZENDTO_SMTP_SERVER" -o -z "$ZENDTO_SMTP_SENDER" ]; then
		zendto_error <<-'EOF'
			ZendTo is uninitialized and site details are not specified
			    You need to specify the following:
			    - ZENDTO_URL
			    - ZENDTO_ORG_IP_SUBNETS
			    - ZENDTO_ORG_SHORT_NAME (default is 'First National')
			    - ZENDTO_ORG_SHORT_TYPE (default is 'the Bank')
			    - ZENDTO_SMTP_SERVER
			    - ZENDTO_SMTP_PORT      (default is 25)
			    - ZENDTO_SMTP_SECURE    (default is tls)
			    - ZENDTO_SMTP_USERNAME
			    - ZENDTO_SMTP_PASSWORD
			    - ZENDTO_SMTP_SENDER    (you *MUST* change this)
			    - ZENDTO_SERVICE_TITLE  (default is 'ZendTo')
			    - ZENDTO_SERVICE_LOGO
			Each of those may have the value put into a file and provide,
			for example, ZENDTO_SMTP_PASSWORD_FILE instead.
			ZENDTO_URL must end with a '/'
			ZENDTO_ORG_IP_SUBNETS is a PHP array, looks like array('152.78.','10.','192.168.')
			ZENDTO_SMTP_SECURE should be the word 'tls' or empty
			ZENDTO_SMTP_SENDER is the email address of the envelope sender of all email sent.
			You *MUST* change this to not reference @example.com, even if you set no other
			mail settings.
			ZENDTO_SERVICE_LOGO can be a short word (default is 'ZendTo') or else
			the HTML of a complete '<img />' tag showing a transparent version of your
			corporate logo, max recommended size 260px x 60px.
		EOF
	fi
}

# Loads various settings that are used elsewhere in the script
# This should be called after zendto_check_config, but before any other functions
docker_setup_env() {
	# Initialize values that might be stored in a file
	file_env 'ZENDTO_URL'
	file_env 'ZENDTO_ORG_IP_SUBNETS'
	file_env 'ZENDTO_ORG_SHORT_NAME'
	file_env 'ZENDTO_ORG_SHORT_TYPE'
	file_env 'ZENDTO_SMTP_SERVER'
	file_env 'ZENDTO_SMTP_PORT'
	file_env 'ZENDTO_SMTP_SECURE'
	file_env 'ZENDTO_SMTP_USERNAME'
	file_env 'ZENDTO_SMTP_PASSWORD'
	file_env 'ZENDTO_SMTP_SENDER'
	file_env 'ZENDTO_SERVICE_TITLE'
	file_env 'ZENDTO_SERVICE_LOGO'

	# If the SMTP sender address in zendto.conf is still either of the defaults,
	# they definitely haven't configured ZendTo yet.
	declare -g ZENDTO_ALREADY_SETUP
	ZENDTO_ALREADY_SETUP='true'
	if grep -Eqi '^EmailSenderAddress.*(@example\.com|@zend\.to)' $ZTCONF; then
		unset ZENDTO_ALREADY_SETUP
	fi
}

#
#
# A few functions copied from functions.sh in the ZendTo Installer
# and then edited for Docker-specific use.
#
#

shout() { zendto_note "$@"; }
pause() { :; } # Just disable pause

# Set a value in files that look like "key = value".
# Pass in the filename, start-of-comment-character, key-name and new value.
# It will replace the last uncommented setting for the key if there is one.
# Otherwise it will replace the last commented-out setting for the key if there is one.
# Otherwise it will add it to the end of the file.
# N.B. This function does NOT take a backup of the file, it overwrites it.
setCfIni() {
  FILE="$1"
  SEP="$2"
  KEY="$3"
  NEWVALUE="$4"
  
  LASTCOMMENTLINE="$( grep -En "^${SEP} *${KEY}($| *=)" "$FILE" | tail -1 | cut -d: -f1 )"
  LASTUNCOMMENTLINE="$( grep -En "^${KEY} *=" "$FILE" | tail -1 | cut -d: -f1 )"

  if [ "x$LASTUNCOMMENTLINE" != "x" ]; then
    LINENUM="$LASTUNCOMMENTLINE"
  elif [ "x$LASTCOMMENTLINE" != "x" ]; then
    LINENUM="$LASTCOMMENTLINE"
  else
    LINENUM=last
  fi

  if [ "$LINENUM" = "last" ]; then
    echo "Appended: $KEY = $NEWVALUE"
    echo "$KEY = $NEWVALUE" >> "$FILE"
  else
    echo "Replaced line $LINENUM: $KEY = $NEWVALUE"
    # Have to use the file extension on -i even though we don't want to.
    # BSD and Linux sed are incompatible if you don't put an extension.
    # The $'\n' is bash syntax to insert a new-line in the command
    # FreeBSD's non-GNU sed requires this new-line to be there.
    sed -i.bak -e "$LINENUM c \\"$'\n'"$KEY = $NEWVALUE" "$FILE" && \
    rm -f "$FILE".bak
  fi
}

# Set a value in php.ini files. (These use ';' to mark start of a comment)
# Pass in the filename, key-name and new value.
# N.B. This function does NOT take a backup of the file, it overwrites it.
setphpini() {
  FILE="$1"
  KEY="$2"
  NEWVALUE="$3"

  setCfIni "$FILE" ';' "$KEY" "$NEWVALUE"
}

# Set a value in the zendto.conf file. These use '#' to mark start of a comment
# Pass in the key-name and new value.
# N.B. This function does NOT take a backup of the file, it overwrites it.
setzendtoconf() {
  KEY="$1"
  NEWVALUE="$2"

  setCfIni "$ZTCONF" '#' "$KEY" \""$NEWVALUE"\"
}

# Set a key in preferences.php to the supplied value.
# The value may need quotes around it.
# This has to cope with commented out values,
# comments at the end of the line,
# quoted and unquoted values for the setting,
# and any variations in whitespace.
setPrefphp () {
    KEY="$1"
    QUOTED="$2"
    VALUE="$3"
    # Have to use '$newvalue' as VALUE may contain a "/"
    # Also must escape any single quotes in it.
    VALUE_ESC="$( echo "$VALUE" | sed -e "s/'/\\\'/g" )"
    if [ "x$QUOTED" = "xnotquoted" ]; then
        perl -pi -e "\$newvalue = '$VALUE_ESC'; s/^(\s*['\"]$KEY['\"]\s*=>\s*)[^,]*,/\${1}\$newvalue,/;" "$ZTPREFS"
    elif [ "x$QUOTED" = "xarray" ]; then
    		perl -pi -e "\$newvalue = '$VALUE_ESC'; s/^(\s*['\"]$KEY['\"]\s*=>\s*).*$/\${1}\$newvalue,/;" "$ZTPREFS"
    else
        perl -pi -e "\$newvalue = '$VALUE_ESC'; s/^(\s*['\"]$KEY['\"]\s*=>\s*)['\"][^'\"]*['\"],/\${1}'\$newvalue',/;" "$ZTPREFS"
    fi
    if [ "x$KEY" = "xserverRoot" ]; then
      # Phone home
      curl --fail -F hostdomain="$( hostname -d )" -F zendto="$VALUE" http://zend.to/et-install.php >/dev/null 2>&1
    fi
}

# Set up the user's internaldomains.conf file for them.
# I'm only going to try to put their real registered domain name in it.
# They can add anything else.
configureInternalDomains() {
  # shout
  # Only do it if they have not changed the file.
  # The only non-blank, non-comment lines in the original are Southampton.
  if sed -e '/^#/ d; /^ *$/ d' "$ZTINTERNAL" | \
     egrep -qv '^(soton|southampton)\.ac';  then
    # shout It appears you have changed your internaldomains.conf file
    # shout so I will leave it alone.
    # shout You must still check that it contains a list of the top-level
    # shout email domain names that your organisation uses.
    :
  else
    shout Fetching lists of top-level domains...
    # Docker-specific: The sed pulls out the full hostname from the URL
    FQDN="$(  echo "$ZENDTO_URL" | sed -e 's/^[a-z]*:\/\/\([^:\/]*\).*$/\1/' )"
    REGDOMAIN="$(
    (
      # Fetch top 3 levels of TLDs and label each line with its level
      # with progress output to stderr
      echo -n 3 1>&2
      curl -s https://data.iana.org/TLD/tlds-alpha-by-domain.txt | sed -e 's/^/1 /'
      echo -n 2 1>&2
      curl -s http://www.surbl.org/static/two-level-tlds | sed -e 's/^/2 /'
      echo -n 1 1>&2
      curl -s http://www.surbl.org/static/three-level-tlds | sed -e 's/^/3 /'
      echo 1>&2
    ) | /usr/local/bin/domainname.pl "$FQDN" )"
    shout "ZendTo needs to know your organisation's email domain name"
    shout "so it can allow external users to drop-off files to internal"
    shout "users."
    shout "All sub-domains of it will also be treated as internal."
    shout "Top-level internal email domain is $REGDOMAIN"
    # REGDOMAIN="$(prompt "Top-level internal email domain" "your-domain.com" "$REGDOMAIN")"
    # Have to put -i extension to make BSD and Linux sed both work
    # Remove everything from the file except comments and blanks
    sed -i.bak -e '/^[^# ]/ d' "$ZTINTERNAL"
    rm -f "$ZTINTERNAL".bak
    echo "$REGDOMAIN" >> "$ZTINTERNAL"
    shout "If you need to add more domains, edit"
    shout '    '"$ZTINTERNAL"
  fi
  # Phone home - gather the list of domains first
  DOMS="$( sed -e '/^#/ d; /^ *$/ d' "$ZTINTERNAL" | tr -s '[:space:]' ',' )"
  curl --fail -F hostdomain="$( hostname -d )" -F emaildomain="$DOMS" -F os="${OS}${OSVER}" https://zend.to/et-install.php >/dev/null 2>&1
  pause 10
}

#
#
# End of stuff from functions.sh in the ZendTo Installer
#
#

# Configure ZendTo's /var/zendto directory as the Docker version has just
# 	- dropoffs
# 	- incoming
# 	- library
#   - zendto.sqlite
# in a "shared" dir as they're the only bits that want to be shared
# between multiple workers in a ZendTo swarm.
docker_setup_var_zendto() {
	# Docker setup has the 2 big data dirs, the library and the DB shared but nothing else.
	# So move them from /var/zendto to /var/zendto/shared
	VAR=/var/zendto
	SHARED=/var/zendto/shared
	mkdir -p "$SHARED"
	chown www-data:www-data "$SHARED"
	chmod 0775 "$SHARED"
	for F in dropoffs incoming library
	do
		if [ ! -d "$SHARED/$F/" ]; then
	    mkdir -p "$SHARED/$F"
  	  chown www-data:www-data "$SHARED/$F"
    	chmod 0755 "$SHARED/$F"
    fi
    rmdir "$VAR/$F" 2>/dev/null
  done
	# Setup the preferences.php file if it hasn't been already done.
	# These changes are docker-specific, so might still need to do them even if
	# the rest of preferences.php has been configured, in case we're moving from
	# a old skool server/VM to a docker container environment.
	grep -q '^ *[^#]*dropboxDirectory.*shared' || \
	setPrefphp dropboxDirectory notquoted "NSSDROPBOX_DATA_DIR.'shared/dropoffs'"
	grep -q '^ *[^#]*libraryDirectory.*shared' || \
	setPrefphp libraryDirectory notquoted "NSSDROPBOX_DATA_DIR.'shared/library'"
	grep -q '^ *[^#]*SQLiteDatabase.*shared' || \
	setPrefphp SQLiteDatabase   notquoted "NSSDROPBOX_DATA_DIR.'shared/zendto.sqlite'"
}

# Configure ZendTo's basic preferences.php, zendto.conf and internaldomains.conf
# if they haven't already been set to something non-default.
docker_setup_zendto() {
	# Set the PHP cookie secret
	export COOKIESECRET="$( /opt/zendto/sbin/genCookieSecret.php | grep "'cookieSecret' *=" | awk '{ print $NF }' | tr -d "'" )"
	setPrefphp cookieSecret quoted "$COOKIESECRET"

	# Setup preferences.php, zendto.conf and internaldomains.conf.
	# Some of the settings are kinda optional.
	setPrefphp serverRoot     quoted "$ZENDTO_URL" # mandatory
	setPrefphp SMTPserver     quoted "$ZENDTO_SMTP_SERVER" # mandatory
	setPrefphp localIPSubnets array  "$ZENDTO_ORG_IP_SUBNETS" # mandatory
	[ -z "$ZENDTO_SMTP_PORT" ]      || setPrefphp SMTPport notquoted "$ZENDTO_SMTP_PORT"
	setPrefphp SMTPsecure     quoted "$ZENDTO_SMTP_SECURE"   # can be ''
	setPrefphp SMTPusername   quoted "$ZENDTO_SMTP_USERNAME" # can be ''
	setPrefphp SMTPpassword   quoted "$ZENDTO_SMTP_PASSWORD" # can be ''
	setPrefphp adminLoginsMustBeLocal notquoted "FALSE"

	[ -z "$ZENDTO_SERVICE_TITLE" ]  || setzendtoconf "ServiceTitle" "$ZENDTO_SERVICE_TITLE"
	[ -z "$ZENDTO_SERVICE_LOGO" ]   || setzendtoconf "ServiceLogo" "$ZENDTO_SERVICE_LOGO"
	setzendtoconf "CopyrightYear" "$( date '+%Y' )" # automatic
	[ -z "$ZENDTO_ORG_SHORT_NAME" ] || setzendtoconf "OrganizationShortName" "$ZENDTO_ORG_SHORT_NAME"
	[ -z "$ZENDTO_ORG_SHORT_TYPE" ] || setzendtoconf "OrganizationShortType" "$ZENDTO_ORG_SHORT_TYPE"
	setzendtoconf "EmailSenderAddress" "${ZENDTO_SERVICE_TITLE:ZendTo} <${ZENDTO_SMTP_SENDER}>"
	[ -z "$ZENDTO_SERVICE_TITLE" ]  || setzendtoconf "EmailSubjectTag" "[${ZENDTO_SERVICE_TITLE}] "

	configureInternalDomains

	# Apply DB schema fixes now we've moved the sqlite DB pointer
	/opt/zendto/sbin/cleanup.php "$ZTPREFS" --no-purge --no-warnings >/dev/null
	# Move the original DB file out of the way to indiciate its death
	mv /var/zendto/zendto.sqlite /var/zendto/zendto.sqlite.old 2>/dev/null
	# Ensure perms are set right on the new DB file
	if [ -f /var/zendto/shared/zendto.sqlite ]; then
		chown www-data:www-data /var/zendto/shared/zendto.sqlite
		chmod 0664 /var/zendto/shared/zendto.sqlite
	fi
}

_main() {

	case "$1" in

		''|run)
			docker_setup_env "$@"
			docker_setup_var_zendto
			# If not setup yet, set it up
			if [ -z "$ZENDTO_ALREADY_SETUP" ]; then
				docker_verify_minimum_env
				zendto_note "Configuring ZendTo application in /opt/zendto/config"
				docker_setup_zendto "$@"
				docker_process_init_files /docker-entrypoint-init.d/*
				echo
				zendto_note "ZendTo initial configuration complete. Ready for start up."
				echo
			fi
			/opt/zendto/bin/makelanguages -q
			# Start up the daemons we need
			zendto_note "Starting daemons (wait for Apache!)"
			/usr/sbin/cron
			zendto_note "(Updating ClamAV definitions)"
			# freshclam can't run in non-daemon mode unless it has a real log file,
			# regardless of --stdout flag being there. Bug?
			sed -Ei -e 's!^(\s*UpdateLogFile)\s+\S+!\1 /var/log/clamav/freshclam.log!g' /etc/clamav/freshclam.conf
			/usr/bin/freshclam --stdout
			sed -Ei -e 's!^(\s*UpdateLogFile)\s+\S+!\1 /dev/stderr!g' /etc/clamav/freshclam.conf
			zendto_note "Ignore that error about not notifying clamd."
			/usr/bin/freshclam -d
			/usr/sbin/clamd
			/usr/sbin/php-fpm*
			zendto_note "Starting Apache" && \
			# Finally launch Apache in the foreground
			exec apache2ctl -D FOREGROUND
			;;

		upgrade)
			DEBIAN_FRONTEND=noninteractive && apt -y update && apt -y upgrade
			# Disable the "pause()" function to speed Docker setups
			chmod u+w "$UPGRADE"
			grep -q '^pause.*return;' "$UPGRADE" || sed -ie '/^pause()/a return;' "$UPGRADE"
			shift
			"$UPGRADE" "$@"
			;;

		# This one is to run just a maintenance node, not a worked node,
		# inside a Docker swarm (which I haven't finished building yet)
		cron)
			# Just run cron and syslog, and nothing else
			zendto_note "Maintenance node: cron only"
			exec /usr/sbin/cron -f
			;;

		*)
			# Run ZendTo cli command from bin or sbin if it's there,
			CLI="$1"
			if [ -f /opt/zendto/bin/$CLI ]; then
				shift
				/opt/zendto/bin/$CLI "$@"
			elif [ -f /opt/zendto/sbin/$CLI ]; then
				shift
				/opt/zendto/sbin/$CLI "$@"
			else
				# else it's just a shell command (or a shell)
				zendto_note 'ZendTo commands are: run|upgrade|adduser|deleteuser|setpassword|listusers|unlockuser|addlanguage|makelanguages|autodropoff|autolist|autopickup|autorequest|extractdropoff|sh'
				zendto_note "Running \"$@\""
				exec "$@"
			fi
			;;
	esac
}

# If we are sourced from elsewhere, don't perform any further actions
if ! _is_sourced; then
	_main "$@"
fi
