#!/bin/bash
# Copyright [2016-2019] Syncwerk GmbH
set -e
# syncwerk trixie dedicated setup delegation
if [ "${1:-}" = "setup" ] ; then
  shift
  _syncwerk_admin_dir="$(cd -- "$(dirname -- "$0")" && pwd -P)"
  if [ -x "${_syncwerk_admin_dir}/syncwerk-server-admin-setup" ] ; then
    exec "${_syncwerk_admin_dir}/syncwerk-server-admin-setup" "$@"
  fi
  if [ -x "/usr/sbin/syncwerk-server-admin-setup" ] ; then
    exec /usr/sbin/syncwerk-server-admin-setup "$@"
  fi
  echo "syncwerk-server-admin-setup not found or not executable" >&2
  exit 127
fi


# Export variables
export PATH=/usr/local/sbin:/usr/local/bin:$PATH
export PYTHONPATH=/usr/share/python/syncwerk/restapi/lib/python2.7/site-packages
export CONFIG_DIR=/etc/syncwerk
export CCNET_CONF_DIR=${CONFIG_DIR}
export SYNCWERK_CONF_DIR=${CONFIG_DIR}
export SYNCWERK_CENTRAL_CONF_DIR=${CONFIG_DIR}
export RESTAPI_DIR=/usr/share/python/syncwerk/restapi
export DJANGO_SETTINGS_MODULE=restapi.settings
export RESTAPI_LOG_DIR=/var/log/syncwerk
export TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
export OBJECT_STORAGE_PATH="/var/lib/syncwerk"
export ZIPPING_COMMAND="pigz --fast"
export DIST="$(lsb_release -sc)"
export DISTID="$(lsb_release -si)"
export DEBIAN_FRONTEND=noninteractive
export LIBEVENT_ROOT="/usr/lib/syncwerk/libevent"

if [ -z "${SYNCWERK_HOSTNAME}" ] ; then
  export HOSTNAME="$(hostname -f)"
else
  export HOSTNAME="${SYNCWERK_HOSTNAME}"
fi


function setup-user {
id -u syncwerk > /dev/null || adduser --system --gecos "syncwerk" syncwerk --home /usr/share/syncwerk
}


function purge {
cat <<EOF

  USE WITH EXTREME CAUTION

  Proceeding WILL permanently DELETE data WITHOUT further NOTICE

  Purge Syncwerk Server on dedicated machine. Don't use on servers
  with other shared services. ALL databases, stored files and
  configurations will get permantly deleted. This script is only
  intended for development purposes where we need to quickly purge
  Syncwerk server installations and perform a fresh reinstallation.

  Hit return to proceed or CTRL-C to abort.

EOF
read dummy
WAIT=7 ; while [ ${WAIT} -gt 0 ] ; do printf "\r  You have %2d second(s) to change your mind. Hit CTRL-C to abort." ${WAIT} ; sleep 1 ; ((WAIT--)) ; done ; echo
syncwerk-server stop
pkill -SIGKILL -f "mysqld" || true
apt purge $(dpkg -l "*syncwerk*" | grep ii | awk '{ print $2 }' | xargs) -y
for TYPE in d f ; do find / -iname "*syncwerk*" -type ${TYPE} | while read LINE ; do rm -rf ${LINE} ; done ; done
apt purge $(dpkg -l "*mariadb*"  | grep ii | awk '{ print $2 }' | xargs) -y
rm -rf /etc/nginx/conf.d/syncwerk.conf
pkill -SIGKILL -f "nginx" || true
apt purge nginx -y
apt-get autoremove -y
aptitude purge ~c -y
rm -rf /var/lib/mysql/ /etc/mysql/
apt-get update
}


# Install python tools
function install-python-tools {
command pip  > /dev/null 2>&1 || (echo "Installing latest Python pip 2.7" ; curl -s https://syncwerk.io/python2.7/get-pip.py | python2.7)
echo "Installing virtualenv 16.7.9" ; pip install virtualenv==16.7.9
echo "Installing virtualenv-tools 1.0.0" ; pip install virtualenv-tools==1.0

}


# Setup virtualenv
function setup-virtualenv {
#   Creating MySQL symlink for Python MySQL module to build on jessie and xenial
if [ ${DIST} = jessie ] || [ ${DIST} = xenial ] ; then
  ln -sf /usr/bin/mariadb_config /usr/bin/mysql_config
fi
virtualenv --python=python2.7 /usr/share/python/syncwerk/restapi
if ! md5sum --check /tmp/syncwerk_restapi_requirements_txt.md5 > /dev/null 2>&1 ; then
  echo "Python requirements changed. Updating.."
  /usr/share/python/syncwerk/restapi/bin/python2.7 /usr/share/python/syncwerk/restapi/bin/pip freeze | xargs pip uninstall -y || true
  /usr/share/python/syncwerk/restapi/bin/python2.7 /usr/share/python/syncwerk/restapi/bin/pip install -r /usr/share/python/syncwerk/restapi/requirements.txt
else
  echo "Python requirements have not changed. Skipping update.."
fi
# Delete md5 file created during package system preinst phase 
if [ -f /tmp/syncwerk_restapi_requirements_txt.md5 ] ; then 
  rm /tmp/syncwerk_restapi_requirements_txt.md5
fi
}


# Install Let's Encrypt certificate
function setup-letsencrypt-certificate-usage {
cat <<EOF
Usage:
  $(basename "${0}") setup-letsencrypt-certificate [OPTIONS]

Options:
  --domain DOMAIN        Public DNS name for this Syncwerk server. Default: ${HOSTNAME}
  --email ADDRESS       Registration and expiry-notification address for Let's Encrypt.
  --no-email            Register without an email address.
  --staging             Use the Let's Encrypt staging CA for validation tests.
  --dry-run             Validate renewal flow without replacing the active certificate.
  --force-renewal       Force certificate renewal even if the current certificate is valid.
  --no-restart-nginx    Validate the nginx configuration but do not reload/restart nginx.
  -h, --help            Show this help.

The command uses certbot's webroot authenticator and serves HTTP-01 challenges
through nginx from /var/lib/syncwerk/acme-challenges. Port 80 must be reachable
from the public internet and DOMAIN must resolve to this server.
EOF
}

function setup-letsencrypt-certificate {
local le_domain="${HOSTNAME}"
local le_email="${LETSENCRYPT_EMAIL:-}"
local le_staging=0
local le_dry_run=0
local le_force_renewal=0
local le_restart_nginx=1
local nginx_conf="/etc/nginx/conf.d/syncwerk.conf"
local acme_webroot="/var/lib/syncwerk/acme-challenges"
local old_pythonpath="${PYTHONPATH:-}"

while [ "$#" -gt 0 ] ; do
  case "$1" in
    --domain)
      [ -n "${2:-}" ] || { echo "Missing value for --domain" >&2 ; return 2 ; }
      le_domain="$2"
      shift 2
      ;;
    --email)
      [ -n "${2:-}" ] || { echo "Missing value for --email" >&2 ; return 2 ; }
      le_email="$2"
      shift 2
      ;;
    --no-email)
      le_email=""
      shift
      ;;
    --staging)
      le_staging=1
      shift
      ;;
    --dry-run)
      le_dry_run=1
      shift
      ;;
    --force-renewal)
      le_force_renewal=1
      shift
      ;;
    --no-restart-nginx)
      le_restart_nginx=0
      shift
      ;;
    -h|--help)
      setup-letsencrypt-certificate-usage
      return 0
      ;;
    *)
      echo "Unknown option for setup-letsencrypt-certificate: $1" >&2
      setup-letsencrypt-certificate-usage
      return 2
      ;;
  esac
done

if [ "$(id -u)" -ne 0 ] ; then
  echo "setup-letsencrypt-certificate must be run as root." >&2
  return 1
fi
if [[ "$le_domain" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || [[ "$le_domain" = *:* ]] ; then
  echo "Let's Encrypt cannot issue certificates for IP addresses. Use a public DNS name." >&2
  return 2
fi
if [ ! -f "$nginx_conf" ] ; then
  echo "${nginx_conf} not found. Run syncwerk-server-admin setup before enabling Let's Encrypt." >&2
  return 1
fi
if ! command -v nginx >/dev/null 2>&1 ; then
  echo "nginx is required for the Syncwerk Let's Encrypt webroot flow." >&2
  return 1
fi
if ! command -v certbot >/dev/null 2>&1 ; then
  if command -v apt-get >/dev/null 2>&1 ; then
    echo "Installing certbot from Debian repositories..."
    apt-get update
    apt-get install -y certbot
  else
    echo "certbot is not installed. Install the Debian certbot package and retry." >&2
    return 127
  fi
fi

unset PYTHONPATH
install -d -m 0755 "$acme_webroot"

python3 - "$nginx_conf" "$acme_webroot" <<'PY_ACME_NGINX'
from pathlib import Path
import sys

conf = Path(sys.argv[1])
acme_root = sys.argv[2].rstrip("/")
text = conf.read_text()

http_return = "    return 301 https://$host$request_uri;\n"
http_return_alt = "    return 301 https://$http_host$request_uri;\n"
http_location = "    location / {\n        return 301 https://$host$request_uri;\n    }\n"
http_location_alt = "    location / {\n        return 301 https://$http_host$request_uri;\n    }\n"

if http_return in text:
    text = text.replace(http_return, http_location, 1)
elif http_return_alt in text:
    text = text.replace(http_return_alt, http_location_alt, 1)

http_acme = (
    "    # syncwerk-acme-challenge: allow Let's Encrypt HTTP-01 before HTTPS redirect\n"
    "    location ^~ /.well-known/acme-challenge/ {\n"
    f"        root {acme_root};\n"
    "        default_type text/plain;\n"
    "        try_files $uri =404;\n"
    "    }\n\n"
)
https_acme = (
    "    # syncwerk-acme-challenge: also serve HTTP-01 files on HTTPS if a CA follows redirects\n"
    "    location ^~ /.well-known/acme-challenge/ {\n"
    f"        root {acme_root};\n"
    "        default_type text/plain;\n"
    "        try_files $uri =404;\n"
    "    }\n\n"
)

if "allow Let's Encrypt HTTP-01 before HTTPS redirect" not in text:
    for marker in (http_location, http_location_alt):
        if marker in text:
            text = text.replace(marker, http_acme + marker, 1)
            break
    else:
        raise SystemExit("Could not add HTTP ACME location; HTTP redirect location not found")

https_marker = "    # syncwerk-dotfile-hardening: never serve SCM or dotfile paths via SPA fallback\n"
if "also serve HTTP-01 files on HTTPS" not in text:
    if https_marker not in text:
        raise SystemExit("Could not add HTTPS ACME location; dotfile hardening marker not found")
    text = text.replace(https_marker, https_acme + https_marker, 1)

conf.write_text(text)
PY_ACME_NGINX

nginx -t
systemctl reload nginx.service >/dev/null 2>&1 || service nginx reload

local certbot_args=(certonly --webroot -w "$acme_webroot" --agree-tos --non-interactive --preferred-challenges http-01 -d "$le_domain")
if [ -n "$le_email" ] ; then
  certbot_args+=(--email "$le_email")
else
  certbot_args+=(--register-unsafely-without-email)
  echo "No Let's Encrypt email address configured; registering without expiry notifications."
fi
[ "$le_staging" -eq 1 ] && certbot_args+=(--staging)
[ "$le_dry_run" -eq 1 ] && certbot_args+=(--dry-run)
[ "$le_force_renewal" -eq 1 ] && certbot_args+=(--force-renewal)

certbot "${certbot_args[@]}"

if [ "$le_dry_run" -eq 1 ] ; then
  echo "Let's Encrypt dry-run completed. The active nginx certificate was not changed."
  [ -n "$old_pythonpath" ] && export PYTHONPATH="$old_pythonpath"
  return 0
fi

local live_dir="/etc/letsencrypt/live/${le_domain}"
local fullchain="${live_dir}/fullchain.pem"
local privkey="${live_dir}/privkey.pem"
if [ ! -f "$fullchain" ] || [ ! -f "$privkey" ] ; then
  echo "Certificate files not found in ${live_dir} after certbot completed." >&2
  [ -n "$old_pythonpath" ] && export PYTHONPATH="$old_pythonpath"
  return 1
fi

sed -i -E "s#^[[:space:]]*ssl_certificate[[:space:]]+.*;#    ssl_certificate ${fullchain};#" "$nginx_conf"
sed -i -E "s#^[[:space:]]*ssl_certificate_key[[:space:]]+.*;#    ssl_certificate_key ${privkey};#" "$nginx_conf"

install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/syncwerk-nginx-reload <<'EOF_RENEWAL_HOOK'
#!/bin/sh
set -e
nginx -t >/dev/null
systemctl reload nginx.service >/dev/null 2>&1 || service nginx reload
EOF_RENEWAL_HOOK
chmod +x /etc/letsencrypt/renewal-hooks/deploy/syncwerk-nginx-reload

nginx -t
if [ "$le_restart_nginx" -eq 1 ] ; then
  systemctl reload nginx.service >/dev/null 2>&1 || service nginx reload
else
  echo "nginx reload skipped because --no-restart-nginx was used."
fi
echo "Let's Encrypt certificate for ${le_domain} is active in ${nginx_conf}."
[ -n "$old_pythonpath" ] && export PYTHONPATH="$old_pythonpath"
}


# Install ONLYOFFICE Document Server - Don't call during initial setup since it will break due to dpkg locks
function setup-onlyoffice {
local ds_port=9090
local ds_nginx_conf="/etc/onlyoffice/documentserver/nginx/ds.conf"
local nginx_conf="/etc/nginx/conf.d/syncwerk.conf"
local jwt_file="/etc/syncwerk/onlyoffice-jwt.secret"
local jwt_secret

if [ "$(id -u)" -ne 0 ] ; then
  echo "setup-onlyoffice must be run as root." >&2
  return 1
fi

install -d -m 0750 /etc/syncwerk
if [ -s "$jwt_file" ] ; then
  jwt_secret="$(tr -d '\n\r' < "$jwt_file")"
else
  jwt_secret="$(openssl rand -hex 32)"
  umask 0177
  printf '%s\n' "$jwt_secret" > "$jwt_file"
  umask 0022
fi

if [ ! -f /usr/share/keyrings/onlyoffice.gpg ] ; then
  mkdir -p -m 700 ~/.gnupg
  curl -fsSL https://download.onlyoffice.com/GPG-KEY-ONLYOFFICE \
    | gpg --no-default-keyring --keyring gnupg-ring:/tmp/onlyoffice.gpg --import
  chmod 0644 /tmp/onlyoffice.gpg
  chown root:root /tmp/onlyoffice.gpg
  mv /tmp/onlyoffice.gpg /usr/share/keyrings/onlyoffice.gpg
fi
cat > /etc/apt/sources.list.d/onlyoffice.list <<EOF
deb [signed-by=/usr/share/keyrings/onlyoffice.gpg] https://download.onlyoffice.com/repo/debian squeeze main
EOF

debconf-set-selections <<EOF
onlyoffice-documentserver onlyoffice/ds-port select ${ds_port}
onlyoffice-documentserver onlyoffice/jwt-enabled boolean true
onlyoffice-documentserver onlyoffice/jwt-secret password ${jwt_secret}
msttcorefonts msttcorefonts/accepted-mscorefonts-eula boolean true
EOF

apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y onlyoffice-documentserver

if [ -f "$ds_nginx_conf" ] ; then
  sed -i -E "s#listen[[:space:]]+0\\.0\\.0\\.0:${ds_port};#listen 127.0.0.1:${ds_port};#" "$ds_nginx_conf"
  sed -i -E "s#listen[[:space:]]+\\[::\\]:${ds_port}[[:space:]]+default_server;#listen [::1]:${ds_port};#" "$ds_nginx_conf"
fi

if [ -f "$nginx_conf" ] && ! grep -q 'location /onlyofficeds/' "$nginx_conf" ; then
  local tmp_conf
  tmp_conf="$(mktemp)"
  awk '
    function print_location() {
      print "    location = /onlyofficeds {"
      print "        return 404;"
      print "    }"
      print ""
      print "    location = /onlyofficeds/ {"
      print "        return 404;"
      print "    }"
      print ""
      print "    location = /onlyofficeds/welcome {"
      print "        return 404;"
      print "    }"
      print ""
      print "    location ^~ /onlyofficeds/welcome/ {"
      print "        return 404;"
      print "    }"
      print ""
      print "    location = /onlyofficeds/example {"
      print "        return 404;"
      print "    }"
      print ""
      print "    location ^~ /onlyofficeds/example/ {"
      print "        return 404;"
      print "    }"
      print ""
      print "    location ~ ^/onlyofficeds/(?<oo_version>[0-9]+\\.[0-9]+\\.[0-9]+[.-][A-Za-z0-9]+)/(?<oo_editor>web-apps/apps/(documenteditor|spreadsheeteditor|presentationeditor|visioeditor)/main)/index_loader\\.html$ {"
      print "        proxy_pass http://127.0.0.1:9090/$oo_version/$oo_editor/index.html$is_args$args;"
      print "        proxy_http_version 1.1;"
      print "        client_max_body_size 100M;"
      print "        proxy_read_timeout 3600s;"
      print "        proxy_connect_timeout 3600s;"
      print "        proxy_send_timeout 3600s;"
      print "        proxy_set_header Upgrade $http_upgrade;"
      print "        proxy_set_header Connection \"upgrade\";"
      print "        proxy_set_header Host $http_host;"
      print "        proxy_set_header X-Forwarded-Host $http_host/onlyofficeds;"
      print "        proxy_set_header X-Forwarded-Proto https;"
      print "        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
      print "        access_log /var/log/syncwerk/onlyoffice.log;"
      print "        error_log /var/log/syncwerk/onlyoffice.log;"
      print "    }"
      print ""
      print "    location /onlyofficeds/ {"
      print "        proxy_pass http://127.0.0.1:9090/;"
      print "        proxy_http_version 1.1;"
      print "        client_max_body_size 100M;"
      print "        proxy_read_timeout 3600s;"
      print "        proxy_connect_timeout 3600s;"
      print "        proxy_send_timeout 3600s;"
      print "        proxy_set_header Upgrade $http_upgrade;"
      print "        proxy_set_header Connection \"upgrade\";"
      print "        proxy_set_header Host $http_host;"
      print "        proxy_set_header X-Forwarded-Host $http_host/onlyofficeds;"
      print "        proxy_set_header X-Forwarded-Proto https;"
      print "        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
      print "        access_log /var/log/syncwerk/onlyoffice.log;"
      print "        error_log /var/log/syncwerk/onlyoffice.log;"
      print "    }"
      print ""
    }
    /^[[:space:]]*server[[:space:]]*{/ {
      in_server=1
      is_https=0
      depth=1
      print
      next
    }
    in_server {
      line=$0
      if (line ~ /^[[:space:]]*listen[[:space:]]+(\[::\]:)?443([[:space:];]|$)/) {
        is_https=1
      }
      tmp=line
      opens=gsub(/\{/, "{", tmp)
      tmp=line
      closes=gsub(/\}/, "}", tmp)
      if (is_https && depth + opens - closes == 0 && !inserted) {
        print_location()
        inserted=1
      }
      print
      depth += opens - closes
      if (depth == 0) {
        in_server=0
        is_https=0
      }
      next
    }
    { print }
    END {
      if (!inserted) {
        exit 3
      }
    }
  ' "$nginx_conf" > "$tmp_conf"
  if ! grep -q 'location /onlyofficeds/' "$tmp_conf" ; then
    rm -f "$tmp_conf"
    echo "Could not add /onlyofficeds/ location to ${nginx_conf}; HTTPS server block not found." >&2
    return 1
  fi
  install -m 0644 "$tmp_conf" "$nginx_conf"
  rm -f "$tmp_conf"
fi

sed -i '/# Syncwerk ONLYOFFICE managed block begin/,/# Syncwerk ONLYOFFICE managed block end/d' /etc/syncwerk/restapi_settings.py
cat >> /etc/syncwerk/restapi_settings.py <<EOF

# Syncwerk ONLYOFFICE managed block begin
ENABLE_ONLYOFFICE = True
VERIFY_ONLYOFFICE_CERTIFICATE = False
ONLYOFFICE_APIJS_URL = '/onlyofficeds/web-apps/apps/api/documents/api.js'
ONLYOFFICE_JWT_SECRET = '${jwt_secret}'
ONLYOFFICE_FILE_EXTENSION = ('doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'odt', 'fodt', 'odp', 'fodp', 'ods', 'fods', 'csv')
ONLYOFFICE_EDIT_FILE_EXTENSION = ('doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'odt', 'fodt', 'odp', 'fodp', 'ods', 'fods', 'csv')
# Syncwerk ONLYOFFICE managed block end
EOF

nginx -t
systemctl restart nginx.service >/dev/null 2>&1 || service nginx restart
systemctl restart syncwerk-server.service

if dpkg-query -W -f='${Version}' onlyoffice-documentserver 2>/dev/null | awk -F'[-.]' '{ exit !($1 > 9 || ($1 == 9 && $2 >= 4)) }' ; then
  echo "ONLYOFFICE Docs 9.4 or newer installed; the Community edition no longer has the historical 20-connection limit."
else
  echo "Installed ONLYOFFICE version may still contain the historical Community connection limit. No license-limit patch was applied."
fi
}


# Ensuring required dirs exist
mkdir -p ${OBJECT_STORAGE_PATH}/template ${OBJECT_STORAGE_PATH}/avatars /var/log/syncwerk

function syncwerk-group {
id -gn syncwerk 2>/dev/null || echo nogroup
}

function seed-default-avatars {
local GROUP SOURCE
GROUP="$(syncwerk-group)"
SOURCE="/usr/share/syncwerk/webapp/assets/images/placeholder-profile.png"
[ -f "${SOURCE}" ] || return 0
install -d -o syncwerk -g "${GROUP}" -m 0755 /var/lib/syncwerk/avatars /var/lib/syncwerk/avatars/groups
[ -f /var/lib/syncwerk/avatars/default.png ] || install -o syncwerk -g "${GROUP}" -m 0644 "${SOURCE}" /var/lib/syncwerk/avatars/default.png
[ -f /var/lib/syncwerk/avatars/groups/default.png ] || install -o syncwerk -g "${GROUP}" -m 0644 "${SOURCE}" /var/lib/syncwerk/avatars/groups/default.png
}


function start-database {
if [ -f /.dockerenv ] ; then
  service mysql start
fi
}


function create-database {
if [ ! -f "/etc/syncwerk/mysql.txt" ] ; then
SYNCWERK_DB_PW=$(pwgen 14 1)
cat > /etc/syncwerk/mysql.txt <<EOF
[client]
user=syncwerk
password=${SYNCWERK_DB_PW}
EOF
chmod 600 /etc/syncwerk/mysql.txt
mysql -e "CREATE DATABASE IF NOT EXISTS \`syncwerk-ccnet\` character set = 'utf8';"
mysql -e "CREATE DATABASE IF NOT EXISTS \`syncwerk-server\` character set = 'utf8';"
mysql -e "CREATE DATABASE IF NOT EXISTS \`syncwerk-restapi\` character set = 'utf8';"
mysql -e "CREATE USER 'syncwerk'@'localhost' IDENTIFIED BY '${SYNCWERK_DB_PW}';"
mysql -e "GRANT ALL PRIVILEGES ON  \`syncwerk-ccnet\` . * TO  'syncwerk'@'localhost';"
mysql -e "GRANT ALL PRIVILEGES ON  \`syncwerk-server\` . * TO  'syncwerk'@'localhost';"
mysql -e "GRANT ALL PRIVILEGES ON  \`syncwerk-restapi\` . * TO  'syncwerk'@'localhost';"
else
    echo "/etc/syncwerk/mysql.txt exists. Skipping.."
fi
}


function update-database {
# Ensuring paths in virtualenv are valid
virtualenv-tools --reinitialize /usr/share/python/syncwerk/restapi

# Ensuring sql_mode is set
grep -q sql_mode /etc/syncwerk/restapi_settings.py || sed -i 's/storage_engine=INNODB/storage_engine=INNODB, sql_mode=STRICT_TRANS_TABLES/' /etc/syncwerk/restapi_settings.py

echo "Upgrading restapi database"
mysql --force syncwerk-restapi < /usr/share/python/syncwerk/restapi/sql/latest-restapi.sql

# Ensuring institutions are renamed
mysql --force syncwerk-restapi <<EOF  >& /dev/null
RENAME TABLE \`syncwerk-restapi\`.\`institutions_institution\` TO \`syncwerk-restapi\`.\`tenants_tenant\`;
RENAME TABLE \`syncwerk-restapi\`.\`institutions_institutionadmin\` TO \`syncwerk-restapi\`.\`tenants_tenantadmin\`;
RENAME TABLE \`syncwerk-restapi\`.\`institutions_institutionquota\` TO \`syncwerk-restapi\`.\`tenants_tenantquota\`;
ALTER TABLE \`tenants_tenantadmin\` CHANGE \`institution_id\` \`tenant_id\` INT(11) NOT NULL;
ALTER TABLE \`tenants_tenantquota\` CHANGE \`institution_id\` \`tenant_id\` INT(11) NOT NULL;
ALTER TABLE \`profile_profile\` CHANGE \`institution\` \`tenant\` VARCHAR(225) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;
UPDATE \`django_migrations\` SET \`app\` = Replace(app, 'institution', 'tenant') WHERE \`app\` like "%institution%";
UPDATE \`django_migrations\` SET \`name\` = Replace(name, 'institution', 'tenant') WHERE \`name\` like "%institution%";
UPDATE \`auth_permission\` SET \`name\` = Replace(name, 'institution', 'tenant') WHERE \`name\` like "%institution%";
UPDATE \`auth_permission\` SET \`codename\` = Replace(codename, 'institution', 'tenant') WHERE \`name\` like "%institution%";
EOF

# Ensuring shared repo table is updated
mysql --force syncwerk-server <<EOF  >& /dev/null
ALTER TABLE \`syncwerk-server\`.\`SharedRepo\` ADD \`allow_view_history\` BOOLEAN DEFAULT True;
ALTER TABLE \`syncwerk-server\`.\`SharedRepo\` ADD \`allow_view_snapshot\` BOOLEAN DEFAULT False;
ALTER TABLE \`syncwerk-server\`.\`SharedRepo\` ADD \`allow_restore_snapshot\` BOOLEAN DEFAULT False;
EOF

# Upgrading API3 related tables
/usr/share/python/syncwerk/restapi/manage.py migrate api3 > /dev/null 2>&1 || true

# Ensuring django site id is set. Otherwise public user registration fails..
mysql -e "INSERT INTO \`syncwerk-restapi\`.\`django_site\` VALUES (1,'example.com','example.com');" || true

# Ensuring api2_tokenv2 is migrated to api3_tokenv2
mysql -e "INSERT INTO \`syncwerk-restapi\`.\`api3_tokenv2\` SELECT * FROM \`syncwerk-restapi\`.\`api2_tokenv2\` AS \`tmp\` WHERE NOT EXISTS ( SELECT * FROM \`syncwerk-restapi\`.\`api3_tokenv2\` WHERE \`syncwerk-restapi\`.\`api3_tokenv2\`.user = \`tmp\`.user AND \`syncwerk-restapi\`.\`api3_tokenv2\`.device_id = \`tmp\`.device_id );" || true

# Check if the database layout has changed. Requires latest-restapi.sql to include all recent changes at any time
if ! md5sum --check /tmp/syncwerk_restapi_sql.md5 > /dev/null 2>&1 ; then
  echo "Reinitialising restapi database schema. This may take a while.."
  for i in $(python /usr/share/python/syncwerk/restapi/manage.py migrate --fake 2> /dev/null | grep "Apply all migrations" | sed 's/Apply all migrations://g; s/, / /g' | xargs) ; do
    python /usr/share/python/syncwerk/restapi/manage.py migrate ${i} > /dev/null 2>&1 || python /usr/share/python/syncwerk/restapi/manage.py migrate ${i} --fake > /dev/null 2>&1
  done
else
  echo "Restapi database schema has not changed. Skipping update.."
fi
# Delete md5 file created during package system preinst phase 
if [ -f /tmp/syncwerk_restapi_sql.md5 ] ; then 
  rm /tmp/syncwerk_restapi_sql.md5
fi

# Fixing typo issue introduce at https://gitlab.syncwerk.com/development/free/syncwerk-server-restapi/-/commit/278b925d217bf2cc75b9117462641774b1f1dc9c
mysql -e "ALTER TABLE \`syncwerk-restapi\`.\`AuditLog\` CHANGE COLUMN \`recepient\` \`recipient\` longtext;" > /dev/null 2>&1 || true

echo "Upgrading ccnet database"
mysql --force syncwerk-ccnet < /usr/share/python/syncwerk/restapi/sql/latest-ccnet.sql > /dev/null 2>&1

# Ensuring language column is present
mysql -e "alter table \`syncwerk-ccnet\`.\`EmailUser\` add \`language\` varchar(255) after \`passwd\`;" > /dev/null 2>&1 || true
mysql -e "alter table \`syncwerk-ccnet\`.\`LDAPUsers\` add \`language\` varchar(255) after \`password\`;" > /dev/null 2>&1 || true 

# Ensuring ctime column is present
mysql -e "alter table \`syncwerk-ccnet\`.\`LDAPUsers\` add \`ctime\` BIGINT after \`is_active\`;" > /dev/null 2>&1 || true 

echo "Upgrading server database"
mysql --force syncwerk-server < /usr/share/python/syncwerk/restapi/sql/latest-server.sql > /dev/null 2>&1
}


function get-database-credentials {
CCNET_DB=$(grep "DB.*=.*" /etc/syncwerk/ccnet.conf | awk '{ print $3 }')
RESTAPI_DB=$(grep "'NAME'.*:.*" /etc/syncwerk/restapi_settings.py | awk -F"'" '{ print $4 }')
SERVER_DB=$(grep -i "db_name" /etc/syncwerk/server.conf | awk '{ print $3 }')
}


function update-translations {
sync
# Updateing translations
for lang in en de ; do nice -n 19 /usr/share/python/syncwerk/restapi/manage.py compilemessages --verbosity 3 --locale ${lang} || true ; done
}


function update-static-files {
sync
# Updateing static files
nice -n 19 /usr/share/python/syncwerk/restapi/manage.py collectstatic --no-input --verbosity 3 || true
}


function help {
cat <<EOF

Name:
  syncwerk-server-admin - A Syncwerk server setup and administration tool

Usage:
  $(basename ${0}) [OPTIONS] [ARGS]

Commands:
  support                         Show how to receive official support
  setup                           Automatically setup production ready Syncwerk server
  update-translations             Update translations (May take very long)
  update-static-files             Update static files (collectstatic)
  start                           Start all Syncwerk server services
  stop                            Stop all Syncwerk server services
  restart                         Restart all Syncwerk server services
  fix-permissions                 Fix file permissions and ownership of Syncwerk server components
  report                          Create report for support requests
  fsck                            Check Syncwerk block storage data
  mount [MOUNTPOINT]              Mount unencrypted folders locally in read only mode
  umount                          Unmount locally mounted folders
  gc                              Run garabage collection to delete admin trash and expired file versions
  change-email-address            Safely change a user account email address; supports --dry-run
  backup-databases                Backup Syncwerk server databases and save to ${OBJECT_STORAGE_PATH}/backup/
  backup-object-storage           Backup Syncwerk server object storage and save to ${OBJECT_STORAGE_PATH}/backup/
  backup-configurations           Backup Syncwerk server configurations and save to ${OBJECT_STORAGE_PATH}/backup/
  backup-server-applications      Backup installed Syncwerk server application and save to ${OBJECT_STORAGE_PATH}/backup/
  backup-all                      Backup Syncwerk server configuration, databases, object-storage and save to ${OBJECT_STORAGE_PATH}/backup/
  setup-onlyoffice                Install ONLYOFFICE for file preview and editing
  harden-nginx-dotfiles           Block dotfiles and SCM paths before the webapp fallback
  setup-clamav-virus-scanner      Install and setup ClamAV virus scanner 
  run-virus-scanner               Runs virus scanner manually
  setup-letsencrypt-certificate   Activate Let's Encrypt TLS using nginx webroot validation.
                                  Run with --help to show domain, email, staging and dry-run options.
  configure-ldap                  Configure Syncwerk LDAP/AD authentication in /etc/syncwerk/ccnet.conf
  test-ldap                       Validate the LDAP section in /etc/syncwerk/ccnet.conf with ldapsearch
  setup-samba-ad-dc               Install Samba based Active Directory for "$(hostname -d | sed 's/^/DC=/ ; s/\./,DC=/g')"
  create-file-list                Create a list of all files in unencrypted folders


SUPPORT:
  For support contact us at support@syncwerk.com or visit https://www.syncwerk.com

EOF
}


function harden-nginx-dotfiles {
local nginx_conf="/etc/nginx/conf.d/syncwerk.conf"
local tmp_conf

if [ ! -f "${nginx_conf}" ] ; then
  echo "${nginx_conf} not found. Run syncwerk-server-admin setup first." >&2
  return 1
fi

if grep -q 'syncwerk-dotfile-hardening' "${nginx_conf}" ; then
  echo "Syncwerk nginx dotfile hardening already present."
  return 0
fi

tmp_conf="$(mktemp)"
awk '
  BEGIN { inserted = 0 }
  /^[[:space:]]*location[[:space:]]+\/[[:space:]]*\{/ && inserted == 0 {
    print "    # syncwerk-dotfile-hardening: never serve SCM or dotfile paths via SPA fallback"
    print "    location ~ /\\.(?!well-known/acme-challenge(?:/|$)) {"
    print "        return 404;"
    print "        access_log off;"
    print "        log_not_found off;"
    print "    }"
    print ""
    inserted = 1
  }
  { print }
  END { if (inserted == 0) exit 1 }
' "${nginx_conf}" > "${tmp_conf}" || {
  rm -f "${tmp_conf}"
  echo "Could not add Syncwerk nginx dotfile hardening; webapp location block not found." >&2
  return 1
}

install -m 0644 "${tmp_conf}" "${nginx_conf}"
rm -f "${tmp_conf}"
nginx -t
systemctl reload nginx.service >/dev/null 2>&1 || service nginx reload
echo "Syncwerk nginx dotfile hardening installed in ${nginx_conf}."
}


function setup-nginx {
if [ -z "${SYNCWERK_HOSTNAME}" ] ; then
  export HOSTNAME="$(hostname -f)"
else
  export HOSTNAME="${SYNCWERK_HOSTNAME}"
fi

echo "Setup NGINX proxy for Syncwerk services"
if [ ! -f "/etc/nginx/conf.d/syncwerk.conf" ] ; then
mkdir -p /etc/nginx/conf.d/
cat > /etc/nginx/conf.d/syncwerk.conf <<'EOF'
upstream syncwerk-server-restapi {
  server unix:/run/syncwerk/restapi.sock fail_timeout=0;
}

# Required for ONLYOFFICE
map $http_x_forwarded_proto $the_scheme {
        default $http_x_forwarded_proto;
        "" $scheme;
    }

map $http_x_forwarded_host $the_host {
        default $http_x_forwarded_host;
        "" $host;
    }

map $http_upgrade $proxy_connection {
        default upgrade;
        "" close;
    }
    
server {
  listen [::]:80 ipv6only=off;
  server_name  "";

  # syncwerk-acme-challenge: allow Let's Encrypt HTTP-01 before HTTPS redirect
  location ^~ /.well-known/acme-challenge/ {
    root /var/lib/syncwerk/acme-challenges;
    default_type text/plain;
    try_files $uri =404;
  }

  location / {
    return 301 https://$http_host$request_uri;
  }
}

server {
  listen [::]:443 ipv6only=off http2;
  server_name  "";
  ssl on;
  ssl_certificate /etc/nginx/ssl/syncwerk.crt;
  ssl_certificate_key /etc/nginx/ssl/syncwerk.key;

  proxy_set_header X-Forwarded-For $remote_addr;
  proxy_max_temp_file_size 0;

  # syncwerk-acme-challenge: also serve HTTP-01 files on HTTPS if a CA follows redirects
  location ^~ /.well-known/acme-challenge/ {
    root /var/lib/syncwerk/acme-challenges;
    default_type text/plain;
    try_files $uri =404;
  }

  # syncwerk-dotfile-hardening: never serve SCM or dotfile paths via SPA fallback
  location ~ /\.(?!well-known/acme-challenge(?:/|$)) {
    return 404;
    access_log off;
    log_not_found off;
  }

  location / {
    try_files $uri $uri/ /index.html;
    root /usr/share/syncwerk/webapp/;
    access_log      /var/log/syncwerk/webapp.log;
    error_log       /var/log/syncwerk/webapp.log;
  }

  location /notification/list {
    return 301 /notifications;
  }

  location /seafhttp {
    rewrite ^/seafhttp(.*)$ $1 break;
    proxy_pass http://127.0.0.1:8082;
    client_max_body_size 0;
    proxy_connect_timeout  36000s;
    proxy_read_timeout  36000s;
    proxy_send_timeout  36000s;
    proxy_request_buffering off;
  }

  location /api2 {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://syncwerk-server-restapi;
    client_max_body_size 0;
    access_log      /var/log/syncwerk/restapi-proxy.log;
    error_log       /var/log/syncwerk/restapi-proxy.log;
  }

  location /api/v2.1 {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://syncwerk-server-restapi;
    client_max_body_size 0;
    proxy_connect_timeout  120s;
    proxy_read_timeout  120s;
    proxy_send_timeout  120s;
    access_log      /var/log/syncwerk/restapi-proxy.log;
    error_log       /var/log/syncwerk/restapi-proxy.log;
  }
  
  # Sync client: Events place holder FIXME TODO
  location /api2/events/ {
    return 200 '{ "more_offset": 0, "events": [ { "author": "john.doe@example.com", "nick": "John Doe", "time": 1524031159, "repo_name": "Demo", "desc": "Events are not implemented yet" }, { "author": "jane.doe@example.com", "nick": "Jane Doe", "time": 1521266009, "repo_name": "Demo", "desc": "Events are not implemented yet" } ] }';
  }

  # Sync client: File search place holder FIXME TODO
  location /api2/search/ {
    return 200 '{ "has_more": false, "total": 1, "results": [ { "name": "File search is not implemented yet", "is_dir": false, "last_modified": 1520007294, "size": 0 } ] }';
  }

  location /api3 {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://syncwerk-server-restapi;
    client_max_body_size 0;
    proxy_connect_timeout  120s;
    proxy_read_timeout  120s;
    proxy_send_timeout  120s;
    access_log      /var/log/syncwerk/restapi-proxy.log;
    error_log       /var/log/syncwerk/restapi-proxy.log;
  }


  location /media {
    root /usr/share/python/syncwerk/restapi/;
  }

  location /client-login {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://syncwerk-server-restapi;
    client_max_body_size 0;
    access_log      /var/log/syncwerk/restapi-proxy.log;
    error_log       /var/log/syncwerk/restapi-proxy.log;
  }
  
  location /webdav {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://127.0.0.1:8080;
    proxy_request_buffering off;
    proxy_buffering off;
    client_max_body_size 0;
    proxy_connect_timeout 120s;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    access_log      /var/log/syncwerk/webdav.log;
    error_log       /var/log/syncwerk/webdav.log;
  }
  
  location = /onlyofficeds {
    return 404;
  }

  location = /onlyofficeds/ {
    return 404;
  }

  location = /onlyofficeds/welcome {
    return 404;
  }

  location ^~ /onlyofficeds/welcome/ {
    return 404;
  }

  location = /onlyofficeds/example {
    return 404;
  }

  location ^~ /onlyofficeds/example/ {
    return 404;
  }

  location ~ ^/onlyofficeds/(?<oo_version>[0-9]+\.[0-9]+\.[0-9]+[.-][A-Za-z0-9]+)/(?<oo_editor>web-apps/apps/(documenteditor|spreadsheeteditor|presentationeditor|visioeditor)/main)/index_loader\.html$ {
    proxy_pass http://127.0.0.1:9090/$oo_version/$oo_editor/index.html$is_args$args;
    proxy_http_version 1.1;
    client_max_body_size 100M;
    proxy_read_timeout 3600s;
    proxy_connect_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $proxy_connection;
    proxy_set_header X-Forwarded-Host $the_host/onlyofficeds;
    proxy_set_header X-Forwarded-Proto $the_scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    access_log      /var/log/syncwerk/onlyoffice.log;
    error_log       /var/log/syncwerk/onlyoffice.log;
  }

  location /onlyofficeds/ {
    proxy_pass http://127.0.0.1:9090/;
    proxy_http_version 1.1;
    client_max_body_size 100M;
    proxy_read_timeout 3600s;
    proxy_connect_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $proxy_connection;
    proxy_set_header X-Forwarded-Host $the_host/onlyofficeds;
    proxy_set_header X-Forwarded-Proto $the_scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    access_log      /var/log/syncwerk/onlyoffice.log;
    error_log       /var/log/syncwerk/onlyoffice.log;
  }
  
}
EOF
sed -i "s/HOSTNAME/${HOSTNAME}/g" /etc/nginx/conf.d/syncwerk.conf
else
    echo "/etc/nginx/conf.d/syncwerk.conf exists. Skipping.."
fi
echo


echo "Setting up self-signed SSL certificate for Syncwerk services"
if [ ! -f "/etc/nginx/ssl/syncwerk.key" ] ; then
mkdir -p /etc/nginx/ssl
openssl genrsa -out /etc/nginx/ssl/syncwerk.key 4096
cat > /etc/nginx/ssl/syncwerk.cnf <<EOF
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[req_distinguished_name]
countryName = DE
stateOrProvinceName = Bayern
localityName = Wiesentheid
organizationName = Syncwerk GmbH
organizationalUnitName = WebApp
emailAddress = support@syncwerk.com
# Must be last for Syncwerk client to validate...
commonName = ${HOSTNAME}
[v3_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:TRUE
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = $(hostname -f)
DNS.3 = $(hostname -s)
EOF
openssl req -new -x509 -key /etc/nginx/ssl/syncwerk.key -out /etc/nginx/ssl/syncwerk.crt -days 10950 -config /etc/nginx/ssl/syncwerk.cnf -sha256
else
    echo "/etc/nginx/ssl/syncwerk.key exists. Skipping.."
fi
echo


echo "Setting up NGINX proxy for Syncwerk services"
echo

echo "Backing up old /etc/nginx/nginx.conf"
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.syncwerk-backup.$(date +"%Y-%m-%d_%H-%M-%S")
echo

echo "Deleting redundant backups"
fdupes /etc/nginx/ | grep "nginx.conf.syncwerk-backup" | xargs rm
echo

echo "Creating optimized nginx.conf"
cat > /etc/nginx/nginx.conf <<ENDOFFILE
worker_processes auto;
events {
  worker_connections 8096;
  multi_accept on;
  use epoll;
}
pid /var/run/nginx.pid;
worker_rlimit_nofile 40000;
http {
  server_tokens off;
  server_names_hash_bucket_size 128;
  client_max_body_size 50M;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
  '\$status \$body_bytes_sent "\$http_referer" '
  '"\$http_user_agent" "\$http_x_forwarded_for"';
  access_log /var/log/nginx/access.log main;
  error_log /var/log/nginx/error.log warn;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  client_body_timeout 12;
  client_header_timeout 12;
  keepalive_timeout 15;
  send_timeout 10;
  gzip on;
  gzip_vary on;
  gzip_proxied expired no-cache no-store private auth any;
  gzip_comp_level 9;
  gzip_min_length 10240;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/xml font/woff2;
  gzip_disable "MSIE [1-6].";
  include /etc/nginx/conf.d/*.conf;
  map \$scheme \$php_https {
    default off;
    https on;
  }
  include perfect-forward-secrecy.conf;
}
ENDOFFILE
echo


echo "Setting up perfect forward secrecy"
if [ ! -f "/etc/nginx/perfect-forward-secrecy.conf" ] ; then
openssl dhparam -dsaparam -out /etc/nginx/dh4096.pem 4096
cat > /etc/nginx/perfect-forward-secrecy.conf <<'EOF'
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA";
ssl_dhparam dh4096.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
EOF
echo
else
    echo "/etc/nginx/ssl/syncwerk.key exists. Skipping.."
fi


# Ensuring log dir exists
mkdir -p /var/log/syncwerk/
echo


OS=$(lsb_release -sc)
if [ ${OS} = jessie ] ; then
  echo "Removing HTTP/2 extension"
  sed -i 's/http2//g' /etc/nginx/conf.d/syncwerk.conf
  echo
fi


echo "Restarting NGINX"
service nginx restart
}


function setup-ccnet-conf {
SERVERID="$(cat /dev/urandom | tr -dc "a-fA-F0-9" | fold -w 40 | head -n 1)"
DBUSER="$(grep user /etc/syncwerk/mysql.txt | awk -F"=" '{ print $2}')"
DBPASS="$(grep password /etc/syncwerk/mysql.txt | awk -F"=" '{ print $2}')"
DBNAME="syncwerk-ccnet"
sed -i "s/SERVERID/${SERVERID}/g" /etc/syncwerk/ccnet.conf
sed -i "s/HOSTNAME/${HOSTNAME}/g" /etc/syncwerk/ccnet.conf
sed -i "s/DBUSER/${DBUSER}/g" /etc/syncwerk/ccnet.conf
sed -i "s/DBPASS/${DBPASS}/g" /etc/syncwerk/ccnet.conf
sed -i "s/DBNAME/${DBNAME}/g" /etc/syncwerk/ccnet.conf
}


function setup-my-key-peer {
if [ ! -f "/etc/syncwerk/mykey.peer" ] ; then
openssl genrsa -out /etc/syncwerk/mykey.peer 2048
chmod 600 /etc/syncwerk/mykey.peer
else
    echo "/etc/syncwerk/mykey.peer exists. Skipping.."
fi
}


function setup-ccnet-database {
# Setup EmailUser and LDAPUsers tables so syncwerk-server binary is satisfied and will not abort. CCNET will create the other missing tables during startup.
mysql -e "CREATE TABLE IF NOT EXISTS \`syncwerk-ccnet\`.\`EmailUser\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`email\` varchar(255) DEFAULT NULL, \`passwd\` varchar(256) DEFAULT NULL, \`is_staff\` tinyint(1) NOT NULL, \`is_active\` tinyint(1) NOT NULL, \`ctime\` bigint(20) DEFAULT NULL, PRIMARY KEY (\`id\`), UNIQUE KEY \`email\` (\`email\`) );"
mysql -e "CREATE TABLE IF NOT EXISTS \`syncwerk-ccnet\`.\`LDAPUsers\` ( \`id\` bigint(20) NOT NULL AUTO_INCREMENT, \`email\` varchar(255) NOT NULL, \`password\` varchar(255) NOT NULL, \`language\` varchar(255) DEFAULT NULL, \`is_staff\` tinyint(1) NOT NULL, \`is_active\` tinyint(1) NOT NULL, \`ctime\` bigint(20) DEFAULT NULL, \`extra_attrs\` text DEFAULT NULL, \`reference_id\` varchar(255) DEFAULT NULL, PRIMARY KEY (\`id\`), UNIQUE KEY \`email\` (\`email\`), UNIQUE KEY \`reference_id\` (\`reference_id\`) );"
}


function setup-file-server-conf {
DBUSER="$(grep user /etc/syncwerk/mysql.txt | awk -F"=" '{ print $2}')"
DBPASS="$(grep password /etc/syncwerk/mysql.txt | awk -F"=" '{ print $2}')"
DBNAME="syncwerk-server"
sed -i "s/DBUSER/${DBUSER}/g" /etc/syncwerk/server.conf
sed -i "s/DBPASS/${DBPASS}/g" /etc/syncwerk/server.conf
sed -i "s/DBNAME/${DBNAME}/g" /etc/syncwerk/server.conf
}


function setup-gunicorn-conf {
if [ ! -f "/etc/syncwerk/gunicorn.conf" ] ; then
cat > /etc/syncwerk/gunicorn.conf <<EOF
import multiprocessing
daemon = False
workers = multiprocessing.cpu_count()
threads = 5
timeout = 1200
pid = "/run/syncwerk/restapi.pid"
bind = "unix:/run/syncwerk/restapi.sock"
EOF
else
    echo "/etc/syncwerk/gunicorn.conf exists. Skipping.."
fi
}


function setup-restapi-settings-py {
SECRETKEY="$(</dev/urandom tr -dc 'A-Za-z0-9!#$%&\''()*+,-.:;<=>?@[\]^_`{|}~' | head -c 50  ; echo)"
APIDBNAME="syncwerk-restapi"
CCNETDBNAME="syncwerk-ccnet"
SERVERDBNAME="syncwerk-server"
DBUSER="$(grep user /etc/syncwerk/mysql.txt | awk -F"=" '{ print $2}')"
DBPASS="$(grep password /etc/syncwerk/mysql.txt | awk -F"=" '{ print $2}')"
sed -i "s/SECRETKEY/${SECRETKEY}/g" /etc/syncwerk/restapi_settings.py
sed -i "s/APIDBNAME/${APIDBNAME}/g" /etc/syncwerk/restapi_settings.py
sed -i "s/CCNETDBNAME/${CCNETDBNAME}/g" /etc/syncwerk/restapi_settings.py
sed -i "s/SERVERDBNAME/${SERVERDBNAME}/g" /etc/syncwerk/restapi_settings.py
sed -i "s/DBUSER/${DBUSER}/g" /etc/syncwerk/restapi_settings.py
sed -i "s/DBPASS/${DBPASS}/g" /etc/syncwerk/restapi_settings.py
sed -i "s/HOSTNAME/${HOSTNAME}/g" /etc/syncwerk/restapi_settings.py
sed -i 's/default_admin/superadmin/g' /etc/syncwerk/restapi_settings.py
}


function setup-services {
# Ensuring required dirs exist
mkdir -p ${OBJECT_STORAGE_PATH}/template ${OBJECT_STORAGE_PATH}/avatars /var/log/syncwerk

# Ensuring correct ownerships is set
chown -R syncwerk:nogroup /etc/syncwerk /usr/share/python/syncwerk /usr/share/syncwerk /var/log/syncwerk
ps aux | grep "chown -R syncwerk:nogroup /var/lib/syncwerk" | grep -vq grep || (nice -n 19 chown -R syncwerk:nogroup /var/lib/syncwerk &)

if [[ ! $(tr -d '\0' < /proc/1/environ) == *container* ]] ; then
echo "Setting up systemd autostart"
cat > /etc/systemd/system/syncwerk-server.service <<'EOF'
[Unit]
Description=Syncwerk Server
After=network.target mysql.service
[Service]
Type=simple
ExecStart=/usr/bin/syncwerk-server start --foreground
ExecStop=/usr/bin/syncwerk-server stop
[Install]
WantedBy=multi-user.target
EOF
systemctl enable syncwerk-server
else
echo "Setting up sysv autostart"
cat > /etc/init.d/syncwerk-server <<'EOF'
#!/bin/bash
### BEGIN INIT INFO
# Provides:          syncwerk-server
# Required-Start:    $remote_fs $syslog mysql
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Syncwerk server
# Description:       Start Syncwerk server
### END INIT INFO
# Author: Alexander Jackson <alexander.jackson@syncwerk.com>
#
case "$1" in
  start)
    /usr/bin/syncwerk-server start
    ;;
  restart)
    /usr/bin/syncwerk-server stop
    /usr/bin/syncwerk-server start
    ;;
  stop)
    /usr/bin/syncwerk-server stop
    ;;
  *)
    echo "Usage: /etc/init.d/syncwerk-server {start|stop|restart}"
    exit 1
    ;;
esac
EOF
chmod +x /etc/init.d/syncwerk-server
update-rc.d syncwerk-server defaults
fi

if [ -f /.dockerenv ] ; then
cat > /etc/init.d/syncwerk-server <<'EOF'
#!/bin/bash
### BEGIN INIT INFO
# Provides:          syncwerk-server
# Required-Start:    $remote_fs $syslog mysql
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Syncwerk server
# Description:       Start Syncwerk server
### END INIT INFO
# Author: Alexander Jackson <alexander.jackson@syncwerk.com>
#
case "$1" in
  start)
    /usr/bin/syncwerk-server start
    ;;
  restart)
    /usr/bin/syncwerk-server stop
    /usr/bin/syncwerk-server start
    ;;
  stop)
    /usr/bin/syncwerk-server stop
    ;;
  status)
    if PID=$(pgrep -f /usr/bin/syncwerk-server)
    then
      echo "syncwerk-server is running with PID ${PID}"
    else
      echo "syncwerk-server is not running"
    fi
    ;;
  *)
    echo "Usage: /etc/init.d/syncwerk-server {start|stop|restart|status}"
    exit 1
    ;;
esac

EOF
chmod +x /etc/init.d/syncwerk-server
update-rc.d syncwerk-server defaults
fi
}


function migrate-avatars {
GROUP="$(syncwerk-group)"
install -d -o syncwerk -g "${GROUP}" -m 0755 /var/lib/syncwerk/avatars
chmod 0711 /var/lib/syncwerk
if [ -L /usr/share/python/syncwerk/restapi/media/avatars ] ; then
  echo "Avatars seem migrated. Ensuring permissions and defaults."
else
  if [ -e /usr/share/python/syncwerk/restapi/media/avatars/ ] ; then
    mv /usr/share/python/syncwerk/restapi/media/avatars/* /var/lib/syncwerk/avatars/
    rm -rf /usr/share/python/syncwerk/restapi/media/avatars
  fi
  ln -s /var/lib/syncwerk/avatars /usr/share/python/syncwerk/restapi/media/avatars
fi
chown -R syncwerk:"${GROUP}" /var/lib/syncwerk/avatars
chown -h syncwerk:"${GROUP}" /usr/share/python/syncwerk/restapi/media/avatars
chmod 0755 /var/lib/syncwerk/avatars
seed-default-avatars
}


function fix-permissions {
# Ensuring required dirs exist
mkdir -p ${OBJECT_STORAGE_PATH}/template ${OBJECT_STORAGE_PATH}/avatars /var/log/syncwerk

echo "Ensuring correct ownership is set. Running chown in background..."
chown -R syncwerk:nogroup /etc/syncwerk /usr/share/python/syncwerk /usr/share/syncwerk /var/log/syncwerk
ps aux | grep "chown -R syncwerk:nogroup /var/lib/syncwerk" | grep -vq grep || (nice -n 19 chown -R syncwerk:nogroup /var/lib/syncwerk &)
chmod 0711 ${OBJECT_STORAGE_PATH}
chmod 0755 ${OBJECT_STORAGE_PATH}/avatars
seed-default-avatars
}


function stop {
syncwerk-server stop
}


function start {
if [ ! -f /.dockerenv ] ; then
  systemctl start syncwerk-server.service
else
  service syncwerk-server start
fi
command supervisorctl start all > /dev/null 2>&1 || true
}


function restart {
stop
start
}


function fsck {
sudo -u syncwerk bash -c "syncwerk-server-fsck -c /etc/syncwerk -d /var/lib/syncwerk -F /etc/syncwerk ${1} ${2} ${3} ${4} ${5} ${6} ${7} ${8} | tee -a /var/log/syncwerk/fsck.log"
}


function mount {
touch /var/log/syncwerk/fuse.log
chown syncwerk:nogroup /var/log/syncwerk/fuse.log
syncwerk-server-fuse -c /etc/syncwerk/ -F /etc/syncwerk/ -d ${OBJECT_STORAGE_PATH} -l /var/log/syncwerk/fuse.log ${1}
}


function umount {
pkill -SIGTERM -f "syncwerk-server-fuse" || true
}


function gc {
sudo -u syncwerk bash -c "syncwerk-server-gc -c /etc/syncwerk -d /var/lib/syncwerk -F /etc/syncwerk ${1} ${2} ${3} ${4} ${5} ${6} ${7} ${8} | tee -a /var/log/syncwerk/gc.log"
}


function backup-databases {
get-database-credentials
if [ ! -z "${CCNET_DB}" ] ; then
  if mysqlshow ${CCNET_DB} > /dev/null 2>&1 && mysqlshow ${RESTAPI_DB} > /dev/null 2>&1 && mysqlshow ${SERVER_DB} > /dev/null 2>&1 ; then
    echo "Could not backup databases. They probably don't exist yet. Skipping.."
  else
    echo "Backing up current databases"
    mkdir -p ${OBJECT_STORAGE_PATH}/backup
    mysqldump --databases ${CCNET_DB} ${RESTAPI_DB} ${SERVER_DB} | pigz > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-databases.sql.gz
    echo "Calculating sha256sum for databases backup file"
    pv ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-databases.sql.gz | sha256sum > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-databases.sql.gz.sha256.txt
  fi
else
  echo "Databases not configured. Skipping backup.."
fi
}


function backup-object-storage {
AVAILABLE_SPACE_ON_BACKUP_DRIVE=$(df --output=avail -m ${OBJECT_STORAGE_PATH}/backup/ | tail -1)
TOTAL_SPACE_USED_FOR_BLOCK_STORAGE=$(du --total --summarize --block-size=1M ${OBJECT_STORAGE_PATH}/fs ${OBJECT_STORAGE_PATH}/storage ${OBJECT_STORAGE_PATH}/commits | tail -1 | cut -f1)
AVAILABLE_SPACE_AFTER_BACKUP=$(( AVAILABLE_SPACE_ON_BACKUP_DRIVE - TOTAL_SPACE_USED_FOR_BLOCK_STORAGE ))
if [[ ${AVAILABLE_SPACE_AFTER_BACKUP} -gt 2000 ]] ; then
  (( ${AVAILABLE_SPACE_AFTER_BACKUP} >= $(( AVAILABLE_SPACE_ON_BACKUP_DRIVE - $(( TOTAL_SPACE_USED_FOR_BLOCK_STORAGE * 3 / 2 )) )) )) || (echo "Not enough backup space available. You need to free up at least $(( AVAILABLE_SPACE_ON_BACKUP_DRIVE - $(( TOTAL_SPACE_USED_FOR_BLOCK_STORAGE * 3 / 2 )))) MB of disk space at ${OBJECT_STORAGE_PATH}/backup/" | tee ${OBJECT_STORAGE_PATH}/${TIMESTAMP}_syncwerk-object-storage-backup-not-possible-due-to-insufficient-backup-space.txt ; df -h >> ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-object-storage-backup-not-possible-due-to-insufficient-backup-space.txt ; exit)
  echo "Backing up object storage"
  mkdir -p ${OBJECT_STORAGE_PATH}/backup
  tar --exclude="${OBJECT_STORAGE_PATH}/backup" -cf - ${OBJECT_STORAGE_PATH} | pv -s $(du -sb ${OBJECT_STORAGE_PATH} | awk '{print $1}') | ${ZIPPING_COMMAND} > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-object-storage.gz
  #tar --exclude="${OBJECT_STORAGE_PATH}/backup" -cf - ${OBJECT_STORAGE_PATH} | pv -s $(du -sb ${OBJECT_STORAGE_PATH} | awk '{print $1}') | ${ZIPPING_COMMAND} | gpg --batch --symmetric --passphrase MySecretPassword1 --cipher-algo aes256 -o backup.tar.gz.gpg
  echo "Calculating sha256sum for object storage backup file"
  pv ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-object-storage.gz | sha256sum > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-object-storage.gz.sha256.txt
else
  echo "Not enough backup space available. You need to free at least 2000 MB of disk space on ${OBJECT_STORAGE_PATH}/backup/. Then try again." | tee ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-object-storage-backup-not-possible-due-to-insufficient-backup-space.txt ; df -h >> ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-object-storage-backup-not-possible-due-to-insufficient-backup-space.txt
fi
}


function backup-configurations {
echo "Backing up configuration"
mkdir -p ${OBJECT_STORAGE_PATH}/backup
tar -cf - /etc/syncwerk/ /etc/nginx/ /etc/mysql/ | ${ZIPPING_COMMAND} > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-configurations.gz
echo "Calculating sha256sum for configurations backup file"
pv ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-configurations.gz | sha256sum > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-configurations.gz.sha256.txt
}


function backup-server-applications {
echo "Backing up server application"
mkdir -p ${OBJECT_STORAGE_PATH}/backup
tar -cf - /usr/bin/syncwerk-server* /usr/sbin/syncwerk-server* /usr/include/syncwerk /usr/share/python/syncwerk /usr/share/syncwerk | pv -s $(du --total --summarize --block-size=1M /usr/bin/syncwerk-server* /usr/sbin/syncwerk-server* /usr/include/syncwerk /usr/share/python/syncwerk /usr/share/syncwerk | tail -1 | cut -f1) | ${ZIPPING_COMMAND} > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-server-applications.gz
pv ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-server-applications.gz | sha256sum > ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_syncwerk-server-applications.gz.sha256.txt
}


function backup-all {
backup-configurations
backup-server-applications
backup-databases
backup-object-storage
echo "Here are your backups"
ls -ahl ${OBJECT_STORAGE_PATH}/backup/${TIMESTAMP}_*
}


function create-admin {
if [ ! -f "/etc/syncwerk/admin.txt" ] ; then
echo "Creating Syncwerk admin user"
ADMIN_USER=admin@${HOSTNAME}
ADMIN_PASSWORD=$(pwgen 14 1)
cat > /etc/syncwerk/admin.txt <<EOF
{
  "email": "${ADMIN_USER}",
  "password": "${ADMIN_PASSWORD}"
}
EOF
/usr/share/python/syncwerk/restapi/bin/python2.7 /usr/share/python/syncwerk/restapi/restapi/create-admin.py
echo "Saving Syncwerk admin credentials to /etc/syncwerk/admin.txt"
cat > /etc/syncwerk/admin.txt <<EOF
Mail = ${ADMIN_USER}
Pass = ${ADMIN_PASSWORD}
EOF
chmod 600 /etc/syncwerk/admin.txt
chown syncwerk:nogroup /etc/syncwerk/admin.txt
echo
fi
}


function report {
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
SERVERID=$(syncwerk-server show-id)
cat >> /var/log/syncwerk/report.log <<EOF
Syncwerk Server System Report created on &(date)


Syncwerk Server ID
-------------------------------------------------------
${SERVERID}


Hostnames
-------------------------------------------------------
$(hostname -f),$(hostname -s),$(hostname -d),$(hostname -i),${HOSTNAME}


Content of /etc/hosts
-------------------------------------------------------
$(cat /etc/hosts)


OS
-------------------------------------------------------
$(lsb_release -a)


OS Architecture
-------------------------------------------------------
${ARCH}


Debian Package Architecture
-------------------------------------------------------
${DEB_ARCH}


CPU info
-------------------------------------------------------
$(cat /proc/cpuinfo)


Memory info
-------------------------------------------------------
$(cat /proc/meminfo)


Active Kernel
-------------------------------------------------------
$(uname -a)


Network Interfaces
-------------------------------------------------------
$(ifconfig -a)


IP Addresses
-------------------------------------------------------
$(ip addr show)


DNS Servers
-------------------------------------------------------
$(cat /etc/resolv.conf)


Routes
-------------------------------------------------------
$(route -n)


Content of /etc/network/interfaces
-------------------------------------------------------
$(cat /etc/network/interfaces)


Debian Version
-------------------------------------------------------
$(cat /etc/debian_version)ort created on &(date)

$(find /etc/apt/ -type f | grep -v gpg | while read line ; do echo ; \
    echo Content of ${line} ; \
    cat ${line} ; \
    echo ;
done)


Installed packages
-------------------------------------------------------
$(dpkg -l)


Process List
-------------------------------------------------------
$(ps axu)


Local user accounts
-------------------------------------------------------
$(cat /etc/passwd)
EOF

tar czf /tmp/syncwerk-server-report_${TIMESTAMP}.tgz --exclude="admin.txt" --exclude="mykey.peer" --exclude="*.pyc" /etc/apt /etc/syncwerk /var/log/syncwerk > /dev/null 2>&1
chmod 600 /tmp/syncwerk-server-report_${TIMESTAMP}.tgz /var/log/syncwerk/report.log
cat <<EOF


  EN The report has been saved to /tmp/syncwerk-server-report_${TIMESTAMP}.tgz
     Would you like to submit the report to Syncwerk for further investigations?
     Press Y for Yes and N for No

     Privacy information can be found at https://www.syncwerk.com/en/privacy-disclaimer/


  DE Der Report wurde unter /tmp/syncwerk-server-report_${TIMESTAMP}.tgz gespeichert
     Möchten Sie den Report zur weiteren Diagnose an Syncwerk übermitteln?
     Drücken Sie J für Ja oder N für Nein

     Datenschutzinfos finden Sie unter https://www.syncwerk.com/datenschutzerklaerung/


EOF

read -p "Submit report to Syncwerk? [YyNn] / Report an Syncwerk übertragen? [JjNn]" -n 1 -r
if [[ ${REPLY} =~ ^[YyJj]$ ]]
then
mv /tmp/syncwerk-server-report_${TIMESTAMP}.tgz /tmp/syncwerk-server-report_${SERVERID}_${TIMESTAMP}.tgz
curl -sS "https://$(curl 'https://app.syncwerk.de/ajax/u/d/01ff77fb9f/upload/?r=d99f9958-cedd-41f0-b513-21e5f06a8326' -H 'Accept: application/json' -H 'X-Requested-With: XMLHttpRequest' | awk -F'"https://' '{ print $2 }' | sed 's/"}//')" -F file=@/tmp/syncwerk-server-report_${SERVERID}_${TIMESTAMP}.tgz -F filename=syncwerk-server-report_${SERVERID}_${TIMESTAMP}.tgz -F parent_dir="/" > /dev/null 2>&1 && \

cat <<EOF


  EN Your report was uploaded. Now submit your error description to support@syncwerk.com
     and include this reference: syncwerk-server-report_${SERVERID}_${TIMESTAMP}

  DE Ihr Report wurde auf übertragen. Senden Sie uns jetzt Ihre Fehlerbeschreibung an
     support@syncwerk.com und fügen folgenden Referenz hinzu: syncwerk-server-report_${SERVERID}_${TIMESTAMP}


EOF
else
cat <<EOF


  EN For technical support submit this report with your error description to support@syncwerk.com

  DE Für technische Unterstützung senden Sie den Report inkl. Fehlerbeschreibung an support@syncwerk.com


EOF
fi
}


function ldap_usage {
cat <<EOF

Usage:
  $(basename ${0}) configure-ldap --host LDAP_URI --base BASE_DN [OPTIONS]
  $(basename ${0}) test-ldap [CONFIG_FILE]

Options:
  --host URI                  LDAP URI, for example ldap://dc01.example.local/ or ldaps://dc01.example.local/
  --base DN                   Search base, for example CN=Users,DC=example,DC=local
  --bind-dn DN                Bind DN or AD UPN. If omitted, anonymous bind is used.
  --bind-password PASSWORD    Bind password. Prefer SYNCWERK_LDAP_BIND_PASSWORD or --bind-password-file.
  --bind-password-file FILE   Read bind password from FILE.
  --login-attr ATTRIBUTE      Login attribute. Default: mail. Samba AD bootstrap uses sAMAccountName.
  --filter FILTER             Additional LDAP filter fragment, for example memberOf=CN=Syncwerk,CN=Users,DC=example,DC=local
  --follow-referrals BOOL     true or false. Default: false.
  --cache-ttl SECONDS         Cache LDAP DN lookup only. Password binds still hit LDAP. Default: 300, 0 disables.
  --force                     Replace an existing [LDAP] section in /etc/syncwerk/ccnet.conf.
  --no-test                   Write configuration without ldapsearch validation.
  --restart                   Restart syncwerk-server after writing configuration.
  --dry-run                   Print the resulting LDAP section with the password redacted.

Environment:
  SYNCWERK_CCNET_CONF         Override ccnet.conf path. Default: /etc/syncwerk/ccnet.conf

EOF
}

function _syncwerk_ldap_filter_expr {
local login_attr="${1}"
local filter="${2}"

if [ -n "${filter}" ] ; then
  if [[ "${filter}" == \(* ]] ; then
    printf '%s' "${filter}"
  else
    printf '(%s)' "${filter}"
  fi
else
  printf '(%s=*)' "${login_attr}"
fi
}

function _syncwerk_ldap_search_test {
local host="${1}"
local base="${2}"
local bind_dn="${3}"
local bind_password="${4}"
local login_attr="${5}"
local filter="${6}"
local ldap_filter
local passfile=""
local rc=0

if ! command -v ldapsearch > /dev/null 2>&1 ; then
  echo "ldapsearch not found. Install ldap-utils or use --no-test." >&2
  return 1
fi

ldap_filter="$(_syncwerk_ldap_filter_expr "${login_attr}" "${filter}")"

if [ -n "${bind_dn}" ] ; then
  passfile="$(mktemp /tmp/syncwerk-ldap-bind.XXXXXX)"
  chmod 600 "${passfile}"
  printf '%s' "${bind_password}" > "${passfile}"
  ldapsearch -x -LLL -H "${host}" -D "${bind_dn}" -y "${passfile}" -b "${base}" -z 1 "${ldap_filter}" dn "${login_attr}" > /dev/null || rc=$?
  rm -f "${passfile}"
else
  ldapsearch -x -LLL -H "${host}" -b "${base}" -z 1 "${ldap_filter}" dn "${login_attr}" > /dev/null || rc=$?
fi

if [ "${rc}" -ne 0 ] ; then
  echo "LDAP validation failed for host=${host}, base=${base}, filter=${ldap_filter}." >&2
  return "${rc}"
fi
}

function configure-ldap {
local ccnet_conf="${SYNCWERK_CCNET_CONF:-/etc/syncwerk/ccnet.conf}"
local host=""
local base=""
local bind_dn=""
local bind_password="${SYNCWERK_LDAP_BIND_PASSWORD:-}"
local login_attr="mail"
local filter=""
local follow_referrals="false"
local cache_ttl="300"
local force="0"
local test_config="1"
local restart_service="0"
local dry_run="0"
local backup_conf=""

while [ "$#" -gt 0 ] ; do
  case "${1}" in
    --host|--url) host="${2:-}" ; shift 2 ;;
    --base) base="${2:-}" ; shift 2 ;;
    --bind-dn|--user-dn) bind_dn="${2:-}" ; shift 2 ;;
    --bind-password) bind_password="${2:-}" ; shift 2 ;;
    --bind-password-file)
      bind_password="$(tr -d '\r\n' < "${2}")"
      shift 2
      ;;
    --login-attr) login_attr="${2:-}" ; shift 2 ;;
    --filter) filter="${2:-}" ; shift 2 ;;
    --follow-referrals) follow_referrals="${2:-}" ; shift 2 ;;
    --cache-ttl) cache_ttl="${2:-}" ; shift 2 ;;
    --force) force="1" ; shift ;;
    --no-test) test_config="0" ; shift ;;
    --restart) restart_service="1" ; shift ;;
    --dry-run) dry_run="1" ; shift ;;
    -h|--help) ldap_usage ; return 0 ;;
    *)
      echo "Unknown configure-ldap option: ${1}" >&2
      ldap_usage
      return 2
      ;;
  esac
done

if [ -z "${host}" ] || [ -z "${base}" ] ; then
  echo "--host and --base are required." >&2
  ldap_usage
  return 2
fi

if [ -n "${bind_dn}" ] && [ -z "${bind_password}" ] ; then
  echo "--bind-password, --bind-password-file or SYNCWERK_LDAP_BIND_PASSWORD is required when --bind-dn is set." >&2
  return 2
fi

case "${follow_referrals}" in
  true|false|TRUE|FALSE|True|False) ;;
  *)
    echo "--follow-referrals must be true or false." >&2
    return 2
    ;;
esac

if ! [[ "${cache_ttl}" =~ ^[0-9]+$ ]] ; then
  echo "--cache-ttl must be a non-negative integer." >&2
  return 2
fi

if [ "${dry_run}" = "1" ] ; then
cat <<EOF
[LDAP]
HOST = ${host}
BASE = ${base}
EOF
if [ -n "${bind_dn}" ] ; then
cat <<EOF
USER_DN = ${bind_dn}
PASSWORD = ********
EOF
fi
cat <<EOF
LOGIN_ATTR = ${login_attr}
EOF
if [ -n "${filter}" ] ; then
cat <<EOF
FILTER = ${filter}
EOF
fi
cat <<EOF
FOLLOW_REFERRALS = ${follow_referrals}
CACHE_TTL = ${cache_ttl}
EOF
  return 0
fi

if [ ! -f "${ccnet_conf}" ] ; then
  echo "${ccnet_conf} not found." >&2
  return 1
fi

if grep -Eq '^\[LDAP\]' "${ccnet_conf}" && [ "${force}" != "1" ] ; then
  echo "Found existing [LDAP] section in ${ccnet_conf}. Use --force to replace it." >&2
  return 1
fi

if [ "${test_config}" = "1" ] ; then
  _syncwerk_ldap_search_test "${host}" "${base}" "${bind_dn}" "${bind_password}" "${login_attr}" "${filter}"
fi

backup_conf="${ccnet_conf}.${TIMESTAMP}.bak"
cp -a "${ccnet_conf}" "${backup_conf}"
SYNCWERK_LDAP_HOST="${host}" \
SYNCWERK_LDAP_BASE="${base}" \
SYNCWERK_LDAP_BIND_DN="${bind_dn}" \
SYNCWERK_LDAP_BIND_PASSWORD="${bind_password}" \
SYNCWERK_LDAP_LOGIN_ATTR="${login_attr}" \
SYNCWERK_LDAP_FILTER="${filter}" \
SYNCWERK_LDAP_FOLLOW_REFERRALS="${follow_referrals}" \
SYNCWERK_LDAP_CACHE_TTL="${cache_ttl}" \
python3 - "${ccnet_conf}" <<'PY'
import os
import sys
from pathlib import Path

path = Path(sys.argv[1])
text = path.read_text()
lines = text.splitlines()
out = []
skip = False

for line in lines:
    if line.strip().lower() == "[ldap]":
        skip = True
        continue
    if skip and line.startswith("[") and line.strip().endswith("]"):
        skip = False
    if not skip:
        out.append(line)

while out and out[-1] == "":
    out.pop()

section = [
    "",
    "[LDAP]",
    f"HOST = {os.environ['SYNCWERK_LDAP_HOST']}",
    f"BASE = {os.environ['SYNCWERK_LDAP_BASE']}",
]

bind_dn = os.environ.get("SYNCWERK_LDAP_BIND_DN", "")
if bind_dn:
    section.append(f"USER_DN = {bind_dn}")
    section.append(f"PASSWORD = {os.environ.get('SYNCWERK_LDAP_BIND_PASSWORD', '')}")

section.append(f"LOGIN_ATTR = {os.environ['SYNCWERK_LDAP_LOGIN_ATTR']}")

ldap_filter = os.environ.get("SYNCWERK_LDAP_FILTER", "")
if ldap_filter:
    section.append(f"FILTER = {ldap_filter}")

section.append(f"FOLLOW_REFERRALS = {os.environ['SYNCWERK_LDAP_FOLLOW_REFERRALS']}")
section.append(f"CACHE_TTL = {os.environ['SYNCWERK_LDAP_CACHE_TTL']}")
section.append("")

insert_at = len(out)
in_database = False
for idx, line in enumerate(out):
    stripped = line.strip().lower()
    if stripped == "[database]":
        in_database = True
        insert_at = idx + 1
        continue
    if in_database and stripped.startswith("[") and stripped.endswith("]"):
        insert_at = idx
        break
    if in_database:
        insert_at = idx + 1

new_lines = out[:insert_at] + section + out[insert_at:]
while new_lines and new_lines[-1] == "":
    new_lines.pop()

path.write_text("\n".join(new_lines) + "\n")
PY

if id -u syncwerk > /dev/null 2>&1 ; then
  chown "syncwerk:$(id -gn syncwerk)" "${ccnet_conf}" "${backup_conf}"
  chmod 0640 "${ccnet_conf}" "${backup_conf}"
else
  chmod 0600 "${ccnet_conf}" "${backup_conf}"
fi

echo "LDAP configuration written to ${ccnet_conf}. Backup: ${backup_conf}"
echo "DN cache TTL: ${cache_ttl}s. Password validation is not cached."

if [ "${restart_service}" = "1" ] ; then
  systemctl restart syncwerk-server
fi
}

function test-ldap {
local ccnet_conf="${1:-/etc/syncwerk/ccnet.conf}"

python3 - "${ccnet_conf}" <<'PY'
import configparser
import os
import shutil
import subprocess
import sys
import tempfile

conf = sys.argv[1]
if not os.path.exists(conf):
    print(f"{conf} not found.", file=sys.stderr)
    sys.exit(1)

if not shutil.which("ldapsearch"):
    print("ldapsearch not found. Install ldap-utils.", file=sys.stderr)
    sys.exit(1)

cfg = configparser.ConfigParser(strict=False, interpolation=None)
cfg.optionxform = str
cfg.read(conf)

if not cfg.has_section("LDAP"):
    print(f"No [LDAP] section found in {conf}.", file=sys.stderr)
    sys.exit(1)

section = cfg["LDAP"]

def get(name, default=""):
    for key in (name, name.lower(), name.upper()):
        if key in section:
            return section.get(key, default).strip()
    return default

host = get("HOST")
base = get("BASE")
bind_dn = get("USER_DN")
password = get("PASSWORD")
login_attr = get("LOGIN_ATTR", "mail")
ldap_filter = get("FILTER")

if not host or not base:
    print("HOST and BASE are required in [LDAP].", file=sys.stderr)
    sys.exit(1)

if ldap_filter:
    if not ldap_filter.startswith("("):
        ldap_filter = f"({ldap_filter})"
else:
    ldap_filter = f"({login_attr}=*)"

args = ["ldapsearch", "-x", "-LLL", "-H", host, "-b", base, "-z", "1"]
passfile = None

if bind_dn:
    args.extend(["-D", bind_dn])
    if password:
        fd, passfile = tempfile.mkstemp(prefix="syncwerk-ldap-bind.")
        with os.fdopen(fd, "w") as handle:
            handle.write(password)
        os.chmod(passfile, 0o600)
        args.extend(["-y", passfile])
    else:
        print("USER_DN is set but PASSWORD is empty.", file=sys.stderr)
        sys.exit(1)

args.extend([ldap_filter, "dn", login_attr])

try:
    result = subprocess.run(args, check=False)
finally:
    if passfile:
        os.unlink(passfile)

if result.returncode != 0:
    print(f"LDAP validation failed for host={host}, base={base}, filter={ldap_filter}.", file=sys.stderr)
    sys.exit(result.returncode)

print(f"LDAP validation OK: host={host}, base={base}, filter={ldap_filter}, login_attr={login_attr}")
PY
}

function setup-samba-ad-dc {
local dns_domain
local domain
local realm
local adminpass
local base
local usergroup="Syncwerk"
local username="aduser1"
local userpass
local smb_backup

dns_domain="$(hostname -d)"
if [ -z "${dns_domain}" ] ; then
  echo "hostname -d returned an empty domain. Set a proper FQDN before provisioning Samba AD." >&2
  return 1
fi

domain="$(printf '%s' "${dns_domain}" | awk -F '.' '{ print $1 }' | tr '[:lower:]' '[:upper:]')"
realm="$(printf '%s' "${dns_domain}" | tr '[:lower:]' '[:upper:]')"
adminpass="${SYNCWERK_SAMBA_AD_ADMIN_PASSWORD:-}"
base="CN=Users,$(printf '%s' "${dns_domain}" | sed 's/^/DC=/ ; s/\./,DC=/g')"

if [ -f /etc/samba/smb.conf ] && grep -Eiq 'server role *= *active directory domain controller' /etc/samba/smb.conf ; then
  echo "Existing Samba AD DC configuration detected. Provisioning is skipped."
  if [ -z "${adminpass}" ] ; then
    echo "Set SYNCWERK_SAMBA_AD_ADMIN_PASSWORD before configuring Syncwerk against the existing local Samba AD." >&2
    return 1
  fi
else
  if [ -f /etc/samba/smb.conf ] && [ "${SYNCWERK_SAMBA_AD_FORCE:-0}" != "1" ] ; then
    echo "Existing /etc/samba/smb.conf found. Set SYNCWERK_SAMBA_AD_FORCE=1 to replace it for a fresh local AD." >&2
    return 1
  fi

  apt-get update
  apt-get install samba samba-ad-dc samba-dsdb-modules samba-vfs-modules krb5-user winbind smbclient ldap-utils pwgen -y
  systemctl stop smbd nmbd winbind samba-ad-dc || true
  systemctl disable smbd nmbd winbind || true
  systemctl unmask samba-ad-dc

  if [ -f /etc/samba/smb.conf ] ; then
    smb_backup="/etc/samba/smb.conf.${TIMESTAMP}.syncwerk-backup"
    mv /etc/samba/smb.conf "${smb_backup}"
    echo "Moved existing Samba configuration to ${smb_backup}"
  fi

  if [ -z "${adminpass}" ] ; then
    adminpass="$(pwgen -ns 20 1)"
  fi
  samba-tool domain provision --server-role=dc --use-rfc2307 --dns-backend=SAMBA_INTERNAL --realm="${realm}" --domain="${domain}" --adminpass="${adminpass}"
  grep -q "ldap server require strong auth = no" /etc/samba/smb.conf || sed -i '/.*rfc2307.*/a \        ldap server require strong auth = no' /etc/samba/smb.conf

  if [[ -f /etc/krb5.conf || -h /etc/krb5.conf ]] && [ ! -h /etc/krb5.conf ] ; then
    mv /etc/krb5.conf "/etc/krb5.conf.${TIMESTAMP}.syncwerk-backup"
  fi
  if [ ! -e /etc/krb5.conf ] ; then
    ln -s /var/lib/samba/private/krb5.conf /etc/krb5.conf
  fi

  systemctl enable samba-ad-dc
  systemctl start samba-ad-dc
  echo "Waiting 15 seconds for samba-ad-dc to start"
  sleep 15
fi

samba-tool group list | grep -q "^${usergroup}$" || samba-tool group add "${usergroup}"
if ! samba-tool user list | grep -q "^${username}$" ; then
  userpass="$(pwgen -ns 20 1)"
  samba-tool user create "${username}" "${userpass}"
  samba-tool group addmembers "${usergroup}" "${username}"
  echo "Created test AD user: ${username}"
  echo "Password: ${userpass}"
fi

configure-ldap \
  --force \
  --host ldap://127.0.0.1/ \
  --base "${base}" \
  --bind-dn "administrator@${dns_domain}" \
  --bind-password "${adminpass}" \
  --login-attr sAMAccountName \
  --filter "memberOf=CN=${usergroup},${base}" \
  --follow-referrals false \
  --cache-ttl "${SYNCWERK_LDAP_CACHE_TTL:-300}" \
  --restart

echo "Samba AD bind user: administrator@${dns_domain}"
echo "Samba AD administrator password: ${adminpass}"
echo "Syncwerk LDAP login attribute: sAMAccountName"
}


function setup-clamav-virus-scanner {
if ! clamdscan -V  > /dev/null 2>&1 ; then
grep -iq "\[.*virus.*\]" /etc/syncwerk/restapi_settings.py && (echo "Found existing virus scanning settings in /etc/syncwerk/restapi_settings.py. Aborting.." ; exit 1)
systemctl stop clamav-daemon
systemctl stop clamav-freshclam
apt-get update
apt-get install clamav clamav-daemon -y
cp /etc/clamav/clamd.conf /etc/clamav/clamd.conf.bak
sed -i 's/^User .*/User root/g' /etc/clamav/clamd.conf
sed -i 's/^LocalSocketGroup .*/LocalSocketGroup root/g' /etc/clamav/clamd.conf
systemctl start clamav-daemon
systemctl start clamav-freshclam
cat >> /etc/syncwerk/restapi_settings.py <<EOF

# Enable virus scanning or not. Default to "False"
ENABLE_VIRUS_SCANNING = True
# The command line used for virus scanning.
VIRUS_SCAN_COMMAND = 'clamdscan'
# Enter the status code for infected files
VIRUS_SCAN_RESULT_INFECTED_CODE = [1]
# Enter the status code for clean files
VIRUS_SCAN_RESULT_SAFE_CODE = [0]
# Set virus scan interval
VIRUS_SCAN_INTERVAL = 60
# Exclude extension
VIRUS_SCAN_SKIP_EXT = ['bmp', 'gif', 'ico', 'png', 'jpg', 'mp3', 'mp4', 'wav', 'avi', 'rmvb', 'mkv']
# Exlude files larger bigger than this (set in bytes). Default to 1GB
VIRUS_SCAN_FILE_SIZE_LIMIT = 1024*1024*1024
# Check virus scanner availability
VIRUS_SCAN_CHECK_SCAN_COMMAND_READY = ['which clamdscan', 'systemctl status clamav-daemon.service']

EOF
restart
else
echo "Virus scanner is already installed. Aborting.."
fi
}



function run-virus-scanner {
if grep -iqv "\[.*virus.*\]" /etc/syncwerk/restapi_settings.py ; then
sudo -E -u syncwerk /usr/share/python/syncwerk/restapi/manage.py trigger_virus_scanning
echo "Finished virus scanning"
else
echo "Virus scanner is not installed. Aborting.."
fi
}


function support {
if  [[ ${LANG} = *"de"* ]] ; then
cat <<EOF

  Anwendung  Syncwerk Server

  Anbieter   Syncwerk GmbH, Wiesentheid (Deutschland)
             https://www.syncwerk.com

  Telefon    +49 (0) 9383 33337-0


  Hilfe      support@syncwerk.com

  Handbuch   https://support.syncwerk.de

EOF
else
cat <<EOF

  Program    Syncwerk Server

  Vendor     Syncwerk GmbH, Wiesentheid (Germany)
             https://www.syncwerk.com

  Phone      +49 (0) 9383 33337-0


  Support    support@syncwerk.com

  Manual     https://support.syncwerk.de

EOF
fi
}


function setup {
stop
backup-databases
install-python-tools
setup-nginx
setup-virtualenv
setup-user
start-database
create-database
setup-ccnet-conf
setup-my-key-peer
setup-ccnet-database
setup-file-server-conf
setup-gunicorn-conf
setup-restapi-settings-py
update-database
setup-services
migrate-avatars
fix-permissions
update-static-files
start
stop
start
support
syncwerk-server start
if [ ! -f "/etc/syncwerk/admin.txt" ] ; then
  sleep 12
  create-admin
fi
}


function manage.py {
sudo -E -u syncwerk /usr/share/python/syncwerk/restapi/manage.py ${1} ${2}
}


function change-email-address {
local CURRENTEMAIL=""
local NEWEMAIL=""
local MODE="--apply"
local ALLOW_UNMAPPED=()

while [[ $# -gt 0 ]] ; do
  case "$1" in
    -c|--current)
      CURRENTEMAIL="${2:-}"
      shift 2
      ;;
    -n|--new)
      NEWEMAIL="${2:-}"
      shift 2
      ;;
    --dry-run)
      MODE="--dry-run"
      shift
      ;;
    --apply)
      MODE="--apply"
      shift
      ;;
    --allow-unmapped)
      ALLOW_UNMAPPED=("--allow-unmapped")
      shift
      ;;
    *)
      echo "Unknown option: $1"
      echo "Usage: ${0} change-email-address --current old@email.tld --new new@email.tld [--dry-run|--apply] [--allow-unmapped]"
      exit 2
      ;;
  esac
done

if [ -z "${CURRENTEMAIL}" ] || [ -z "${NEWEMAIL}" ] ; then
  echo "Usage: ${0} change-email-address --current old@email.tld --new new@email.tld [--dry-run|--apply] [--allow-unmapped]"
  exit 2
fi

if [ "${MODE}" = "--apply" ] ; then
  backup-databases
fi

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PYTHONPATH=/usr/share/python/syncwerk/restapi:/usr/share/python/syncwerk
export CONFIG_DIR=/etc/syncwerk
export CCNET_CONF_DIR=/etc/syncwerk
export SYNCWERK_CONF_DIR=/etc/syncwerk
export SYNCWERK_CENTRAL_CONF_DIR=/etc/syncwerk
export RESTAPI_DIR=/usr/share/python/syncwerk/restapi
export DJANGO_SETTINGS_MODULE=restapi.settings
export RESTAPI_LOG_DIR=/var/log/syncwerk
export OBJECT_STORAGE_PATH=/var/lib/syncwerk

cd /usr/share/python/syncwerk/restapi
sudo -E -u syncwerk /usr/bin/python3 manage.py change_user_email \
  "${MODE}" \
  --current "${CURRENTEMAIL}" \
  --new "${NEWEMAIL}" \
  "${ALLOW_UNMAPPED[@]}"
}


function create-file-list {
epoch=$(date +%s)
mkdir -p /mnt/syncwerk-server-mount /var/log/syncwerk/filetree
syncwerk-server-admin umount /mnt/syncwerk-server-mount
syncwerk-server-admin mount /mnt/syncwerk-server-mount
cd /mnt/syncwerk-server-mount
for user in $(ls | xargs) ; do
    printf "Char\tSize\tFile\n" | awk -F'\t' '{printf("%20s %30s   %s \n", $1, $2, $3)}' | tee /var/log/syncwerk/filetree/${epoch}_${user}.log
    printf '%s\n' ------------------------------------------------------------------------------------------------------------------------------------------------------- | tee -a /var/log/syncwerk/filetree/${epoch}_${user}.log
    find ${user}/ -type f | while read file ; do
        filelength=$(echo -n "${file}" | wc -m)
        userlength=$(echo -n "${user}" | wc -m)
        prefixlength=37
        pathlength=$((filelength-userlength-prefixlength))
        filesize=$(du -sb "${file}" | cut -f -1)
        filesizeinmb=$(echo ${filesize} | awk '{ byte =$1 /1024/1024 ; print byte " MB" }')
        printf "${pathlength}\t${filesizeinmb}\t\"${file}\"\n" | awk -F'\t' '{printf("%20s %30s   %s \n", $1, $2, $3)}'
        files=$((files+1))
        echo -n ${files} > /var/log/syncwerk/filetree/${epoch}_${user}_files.txt
    done | sort --numeric-sort | tee -a /var/log/syncwerk/filetree/${epoch}_${user}.log
    totalusage=$(du -sb "${user}" | cut -f -1 | awk '{ byte =$1 /1024/1024 ; print byte " MB" }')
    printf '%s\n' ------------------------------------------------------------------------------------------------------------------------------------------------------- | tee -a /var/log/syncwerk/filetree/${epoch}_${user}.log
    printf "$(if [ -f "/var/log/syncwerk/filetree/${epoch}_${user}_files.txt" ]; then cat /var/log/syncwerk/filetree/${epoch}_${user}_files.txt; fi)\t${totalusage}\t\n" | awk -F'\t' '{printf("%20s %30s   %s \n", $1, $2, $3)}' | tee -a /var/log/syncwerk/filetree/${epoch}_${user}.log
    rm -f /var/log/syncwerk/filetree/${epoch}_${user}_files.txt
    printf '\n\n'
done
echo "Your file list was saved to /var/log/syncwerk/filetree/${epoch}_${user}.log"
}


# Get options and execute task
while true ; do
    case "$1" in
        start) start ; break ;;
        stop) stop ; break ;;
        restart) restart ; break ;;
        setup) setup ; break ;;
        update-translations) update-translations ; break ;;
        update-static-files) update-static-files ; break ;;
        purge) purge ; break ;;
        fsck) fsck ${2} ${3} ${4} ${5} ${6} ${7} ${8} ; break ;;
        mount) mount ${2} ; break ;;
        umount) umount ; break ;;
        gc) gc ${2} ${3} ${4} ${5} ${6} ${7} ${8} ; break ;;
        backup-databases) backup-databases ; fix-permissions ; break ;;
        backup-object-storage) backup-object-storage ; fix-permissions ; break ;;
        backup-configurations) backup-configurations ; fix-permissions ; break ;;
        backup-server-applications) backup-server-applications ; fix-permissions ; break ;;
        backup-all) backup-all ; fix-permissions ; break ;;
        fix-permissions) fix-permissions ; break ;;
        setup-onlyoffice) setup-onlyoffice || support ; break ;;
        harden-nginx-dotfiles) harden-nginx-dotfiles || support ; break ;;
        setup-letsencrypt-certificate) shift ; setup-letsencrypt-certificate "$@" || support; break ;;
        configure-ldap) shift ; configure-ldap "$@" || support ; break ;;
        test-ldap) test-ldap "${2:-}" || support ; break ;;
        setup-samba-ad-dc) setup-samba-ad-dc || support ; break ;;
        setup-clamav-virus-scanner) setup-clamav-virus-scanner || support ; break ;;
        run-virus-scanner) run-virus-scanner || support ; break ;;
        report) report ; break ;;
        support) support ; break ;;
        manage.py) manage.py ${2} ${3} ; break ;;
        change-email-address) shift ; change-email-address "$@" ; break ;;
        create-file-list) create-file-list ; break ;;
        *) help ; break ;;
    esac
done
