#!/bin/bash

## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## NOTE: Do NOT use `sudo` in this script! It will be run by tb-updater's
## postinst, which may run under a torsocks'd apt due to uwtwrapper. torsocks
## breaks sudo. Use setpriv-tb-updater instead, as this works with torsocks.

## TODO: Make this work with more than just Tor Browser.

set -o errexit
set -o nounset
set -o errtrace
set -o pipefail

who_ami="$(whoami)"

## shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/wc-test.sh
source /usr/libexec/helper-scripts/wc-test.sh

## shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh
source /usr/libexec/helper-scripts/has.sh

## shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/log_run_die.sh
source /usr/libexec/helper-scripts/log_run_die.sh

## shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/light_sleep.bsh
source /usr/libexec/helper-scripts/light_sleep.bsh

## shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/lockfile.sh
source /usr/libexec/helper-scripts/lockfile.sh

has setfacl

tb_updater_home='/var/lib/tb-updater/work'
root_browser_dir='/var/cache/tb-binary'
tb_updater_run_dir="/run/user/$(id -u tb-updater)"
wl_sock="${tb_updater_run_dir}/wayland-0"
use_x11_gui='false'
tb_updater_cgroup=''
tb_updater_cgroup_name=''

cleanup_dir_as_tb_updater() {
  local target_dir
  target_dir="$1"

  if ! [ -d "${target_dir}" ]; then
    ## Non-existent directory, skip
    log notice "Skipping cleanup of non-existent directory '${target_dir}'."
    return 0
  fi
  if [ "$(find -- "${target_dir}" | wc -l)"  = '1' ]; then
    ## Empty directory, skip
    log notice "Skipping cleanup of empty directory '${target_dir}'."
    return 0
  fi

  log notice "Deleting contents of directory '${target_dir}' using account 'tb-updater'."
  shopt -s dotglob
  shopt -s nullglob
  /usr/libexec/tb-updater/setpriv-tb-updater \
    safe-rm --recursive --force --one-file-system \
    -- "${target_dir}"/*
  shopt -u dotglob
  shopt -u nullglob
}

cleanup() {
  local xhost_output cgroup_empty_str inotifywait_subshell_pid \
    inotifywait_fd

  if [ -n "${tb_updater_cgroup}" ] && validate_uuid "${tb_updater_cgroup_name}"; then
    cgroup_empty_str="populated 0
frozen 0"
    (
      true "exec {inotifywait_fd}<"
      exec {inotifywait_fd}< <(inotifywait --quiet --monitor --event modify "${tb_updater_cgroup}/cgroup.events")
      # shellcheck disable=SC2064
      trap "kill -s KILL $!" EXIT INT TERM
      while IFS=$'\n' read -r _; do
        if [ "$(cat "${tb_updater_cgroup}/cgroup.events")" = "${cgroup_empty_str}" ]; then
          rmdir "${tb_updater_cgroup}" \
            || log warn "Cannot remove tb-updater cgroup at '${tb_updater_cgroup}'!"
          break
        fi
      done <&"${inotifywait_fd}"
    ) &
    inotifywait_subshell_pid="$!"
    ## inotifywait itself requires some time to initialize. 1/20th of a second
    ## seems to be enough on rather slow hardware. so use 1/10th of a second
    ## for good measure.
    light_sleep 0.1
    if printf '%s\n' '1' > "${tb_updater_cgroup}/cgroup.kill"; then
      wait "${inotifywait_subshell_pid}"
    else
      log warn "Cannot kill tb-updater cgroup at '${tb_updater_cgroup}'!"
      kill "${inotifywait_subshell_pid}"
    fi
  fi

  if [ -S "${wl_sock}" ] \
    && ! setfacl --remove u:tb-updater -- "${wl_sock}"; then
    log warn "Cannot remove ACL on '${wl_sock}' to revoke account 'tb-updater' access!"
  fi

  if mountpoint "${wl_sock}" >/dev/null 2>&1 && ! umount "${wl_sock}"; then
    log warn "Cannot unmount '${wl_sock}'!"
  fi

  if [ "${use_x11_gui}" = 'true' ] \
    && ! xhost_output="$(xhost -si:localuser:tb-updater 2>&1)"; then
    log warn "Could not revoke X11 access from account 'tb-updater'! (Command 'xhost -si:localuser:tb-updater' failed.)"
    log warn "xhost output: '${xhost_output}'"
  fi

  ## Dropping permissions before deletion so that there isn't any way for this
  ## to delete files that 'tb-updater' doesn't have permission to delete.
  cleanup_dir_as_tb_updater "${tb_updater_home}"
  if [ -d "${tb_updater_run_dir}" ]; then
    cleanup_dir_as_tb_updater "${tb_updater_run_dir}"
    rmdir -- "${tb_updater_run_dir}" >/dev/null 2>&1
  fi
}

# shellcheck disable=SC2317
error_handler() {
  local error_code="$?"
  trap '' EXIT INT TERM
  cleanup
  log error "END: Failed."
  exit "${error_code}"
}

trap error_handler EXIT INT TERM

exit_clean() {
  local exit_code
  exit_code="${1:-}"
  [ -z "${exit_code}" ] && exit_code='0'
  trap '' EXIT INT TERM
  if [ "$exit_code" = "0" ]; then
    log notice "END: Success."
  else
    log error "END: Failure."
  fi
  exit "${exit_code}"
}

is_gui_enabled() {
  while [ -n "${1:-}" ]; do
    case "$1" in
      --input)
        if [ -n "${2:-}" ]; then
          TB_INPUT="$2"
        fi
        shift 2
        ;;
      --)
        break
        ;;
      *)
        shift
        ;;
    esac
  done

  if [ "${TB_INPUT:-}" = 'gui' ]; then
    printf '%s\n' 'true'
    return
  fi
  printf '%s\n' 'false'
}

validate_uuid() {
  if [[ "$1" \
    =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
    return 0
  fi
  return 1
}

sandbox_update_torbrowser() {
  local _ gui_enabled orig_wl_sock gui_ready sock_owner xhost_output

  log notice "Start."

  if [ "$(id -u)" != '0' ]; then
    log error "This script requires root privileges!"
    exit_clean 1
  fi

  if ! [ -d "${tb_updater_home}" ]; then
     log error "'${tb_updater_home}' does not exist or is not a directory!"
     exit_clean 1
  fi

  orig_wl_sock=''
  gui_enabled="$(is_gui_enabled "$@")"
  gui_ready='false'
  [ -n "${SUDO_USER:-}" ] || SUDO_USER=''

  ## Run cleanup before starting, so that if there are any problematic
  ## leftovers from a previous interrupted run, they are removed.
  cleanup

  mkdir --parents -- "${tb_updater_run_dir}"
  chown -R -- tb-updater:tb-updater "${tb_updater_run_dir}"
  chmod 0700 -- "${tb_updater_run_dir}"

  ## One-run loop makes it easy to skip "everything else" in the block by
  ## using 'break'.
  # shellcheck disable=SC2043
  for _ in 1; do
    if [ "${gui_enabled}" != 'true' ]; then
      break
    fi
    if [ -z "$SUDO_USER" ]; then
      log notice "Cannot determine calling user, skipping GUI setup, ok."
      break
    fi

    ## Note that if the user is using a nested Wayland compositor within an X
    ## session, we will end up displaying the UI in that nested compositor.
    ## This most likely is not a concern, since this is a rare setup we don't
    ## support.
    if ! orig_wl_sock="$(/usr/libexec/helper-scripts/find_wl_compositor)"; then
      log notice "Cannot find Wayland socket, assuming the use of X11."

      if ! has xhost; then
        log notice "Cannot find xhost executable, skipping GUI setup, ok."
        break
      fi

      local xserver_id_calc xserver_id

      ## This finds *an* X server corresponding to the calling user. That
      ## isn't necessarily the X server on the active TTY. However, this only
      ## needs to support Qubes, and Qubes only has one X server running per
      ## qube by default, so this should be good enough.
      xserver_id_calc="$(who \
        | grep -- "^${SUDO_USER}" \
        | grep -- '(:[0-9]\+)$' \
        | head -n1)" || true
      if [[ "${xserver_id_calc}" =~ \((:[0-9]+)\)$ ]]; then
        xserver_id="${BASH_REMATCH[1]}"
      else
        log notice "Cannot find X server, skipping GUI setup, ok."
        break
      fi

      export DISPLAY="${xserver_id}"
      xhost_output="$(xhost si:localuser:tb-updater 2>&1)" || {
        log error "Could not grant X11 access to account 'tb-updater'! (Command 'xhost si:localuser:tb-updater' failed.)"
        log error "xhost output: '${xhost_output}'"
        exit 1
      }
      use_x11_gui='true'
      gui_ready='true'

      break
    fi

    ## If we get to this point, Wayland is in use.
    ##
    ## /run/user/ORIG_UID is unreadable to all except the owner, so we can't
    ## simply point to the existing Wayland socket (either with an environment
    ## variable or a symlink).
    ##
    ## We also can't just bind-mount the socket into the proper location,
    ## because the socket's permissions are also restrictive and prevent
    ## non-owners from connecting.
    ##
    ## We don't want to change the permissions on the socket to make it
    ## world-readable and world-writable, but we also cannot change the
    ## socket's ownership. For this reason, we use an ACL to temporarily allow
    ## the tb_updater user to connect to the socket. The cleanup routine
    ## revokes the ACL once it's no longer in use.

    if ! sock_owner="$(stat --format='%U' -- "${orig_wl_sock}")"; then
      log notice "Cannot get owner of Wayland socket, skipping GUI setup, ok."
      break
    fi
    if [ "${sock_owner}" != "${SUDO_USER}" ]; then
      log notice "Calling user is different than Wayland socket owner, skipping GUI setup, ok."
      break
    fi

    if [ -f "${wl_sock}" ] || [ -S "${wl_sock}" ]; then
      if mountpoint -- "${wl_sock}" >/dev/null 2>&1 \
        && ! umount -- "${wl_sock}"; then
        log warn "'${wl_sock}' is a bind mount, but cannot be unmounted! GUI will be disabled."
        break
      fi

    elif [ -e "${wl_sock}" ]; then
      log warn "'${wl_sock}' exists but is not a file or socket! GUI will be disabled."
      break

    ## In order to bind-mount a socket somewhere, you need to have a file to
    ## bind-mount over the top of. (One can bind-mount over the top of a
    ## socket also, which this code allows. This isn't expected to ever
    ## happen, but it should be harmless if it does.)
    elif ! touch -- "${wl_sock}"; then
      log warn "Cannot create file '${wl_sock}'! GUI will be disabled."
      break
    fi

    if ! mount --bind -- "${orig_wl_sock}" "${wl_sock}"; then
      log warn "Cannot bind-mount Wayland socket from '${orig_wl_sock}' to '${wl_sock}'! GUI will be disabled."
      break
    fi

    if ! setfacl --modify u:tb-updater:rw -- "${wl_sock}"; then
      log warn "Cannot set ACL on '${wl_sock}' to permit account 'tb-updater' access! GUI will be disabled."
      umount -- "${wl_sock}" \
        || log warn "Cannot clean up bind-mount of Wayland socket from '${orig_wl_sock}' to '${wl_sock}'!"
      break
    fi

    gui_ready='true'
  done

  if [ "${gui_ready}" = 'true' ]; then
    if [ "${use_x11_gui}" = 'true' ]; then
      tb_updater_cgroup="$(/usr/libexec/helper-scripts/run-in-cgroup \
        --detach \
        -- \
        /usr/libexec/tb-updater/setpriv-tb-updater \
        env \
        XDG_RUNTIME_DIR="${tb_updater_run_dir}" \
        DISPLAY="${DISPLAY}" \
        QT_QPA_PLATFORM='xcb' \
        GDK_BACKEND='x11' \
        /usr/libexec/msgcollector/msgdispatcher)"
      tb_updater_cgroup_name="${tb_updater_cgroup##*/}"
      if ! validate_uuid "${tb_updater_cgroup_name}"; then
        log error "Invalid UUID '${tb_updater_cgroup_name}' returned by '/usr/libexec/helper-scripts/run-in-cgroup'!"
        return 1
      fi
      /usr/libexec/helper-scripts/run-in-cgroup \
        --cgroup-name "${tb_updater_cgroup_name}" \
        -- \
        /usr/libexec/tb-updater/setpriv-tb-updater \
        env \
        XDG_RUNTIME_DIR="${tb_updater_run_dir}" \
        DISPLAY="${DISPLAY}" \
        QT_QPA_PLATFORM='xcb' \
        GDK_BACKEND='x11' \
        /usr/bin/update-torbrowser "$@"
    else
      tb_updater_cgroup="$(/usr/libexec/helper-scripts/run-in-cgroup \
        --detach \
        -- \
        /usr/libexec/tb-updater/setpriv-tb-updater \
        env \
        XDG_RUNTIME_DIR="${tb_updater_run_dir}" \
        WAYLAND_DISPLAY='wayland-0' \
        QT_QPA_PLATFORM='wayland' \
        GDK_BACKEND='wayland' \
        /usr/libexec/msgcollector/msgdispatcher)"
      tb_updater_cgroup_name="${tb_updater_cgroup##*/}"
      if ! validate_uuid "${tb_updater_cgroup_name}"; then
        log error "Invalid UUID '${tb_updater_cgroup_name}' returned by '/usr/libexec/helper-scripts/run-in-cgroup'!"
        return 1
      fi
      /usr/libexec/helper-scripts/run-in-cgroup \
        --cgroup-name "${tb_updater_cgroup_name}" \
        -- \
        /usr/libexec/tb-updater/setpriv-tb-updater \
        env \
        XDG_RUNTIME_DIR="${tb_updater_run_dir}" \
        WAYLAND_DISPLAY='wayland-0' \
        QT_QPA_PLATFORM='wayland' \
        GDK_BACKEND='wayland' \
        /usr/bin/update-torbrowser "$@"
    fi
  else
    /usr/libexec/tb-updater/setpriv-tb-updater /usr/bin/update-torbrowser "$@"
  fi

  if [ -d "${tb_updater_home}/.tb/tor-browser" ] && [ -d "${tb_updater_home}/.cache/tb" ]; then
    log notice "Found browser installation in '${tb_updater_home}', moving to '${root_browser_dir}'."
    mkdir --parents -- "${root_browser_dir}"
    shopt -s dotglob
    shopt -s nullglob
    safe-rm --recursive --force -- "${root_browser_dir}"/*
    shopt -u dotglob
    shopt -u nullglob
    cp --recursive -- "${tb_updater_home}/.tb" "${root_browser_dir}/"
    mkdir --parents -- "${root_browser_dir}/.cache"
    cp --recursive -- "${tb_updater_home}/.cache/tb" "${root_browser_dir}/.cache/"

    ## By default, the browser's files will end up with restrictive
    ## permissions such as '0600' and '0700', preventing other users from
    ## copying the browser to their home directory. To fix this, ensure that
    ## all users have read permissions, all directories are readable and
    ## "executable"
    ## (https://superuser.com/questions/168578/why-must-a-folder-be-executable),
    ## and all executables can be executed by the owning user. Do not open up
    ## write permissions to any user other than the owner, and make sure the
    ## system-wide location is owned by root to prevent tampering.
    chmod --recursive o+r -- "${root_browser_dir}"
    chmod --recursive g+r -- "${root_browser_dir}"
    find "${root_browser_dir}" -type d -print0 | xargs -0 chmod 0775
    chmod --recursive -- u+X "${root_browser_dir}"
    chown --recursive -- root:root "${root_browser_dir}"
  fi

  log notice "Cleaning up '${tb_updater_home}'."

  cleanup
  exit_clean 0
}

sandbox_update_torbrowser "$@"
