#!/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.

set -o errexit
set -o nounset
set -o errtrace
set -o pipefail
shopt -s inherit_errexit
shopt -s shift_verbose

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'
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=''

## Browser-specific settings inherited from the wrapper in /usr/bin (e.g.
## update-mullvadbrowser, update-i2pbrowser). Fall back to Tor Browser
## defaults when unset so running sandbox-update-torbrowser directly, or via
## update-torbrowser with no wrapper, keeps its historical behavior.
tb_install_folder="${tb_install_folder:-tb}"
tb_install_folder_dot="${tb_install_folder_dot:-.tb}"
tb_browser_name="${tb_browser_name:-tor-browser}"
tb_global_binary_dir="${tb_global_binary_dir:-/var/cache/tb-binary}"
root_browser_dir="${tb_global_binary_dir}"

## Resume-stash: holds the partial tarball + detached signature across
## failed runs so '--resume' can pick up where curl left off without
## leaving the rest of tb_updater_home populated with possibly-stale
## state from a failed run. Lives outside tb_updater_home so the wipe
## of tb_updater_home does not touch it. Per-browser subdirectory so
## update-torbrowser, update-mullvadbrowser and update-i2pbrowser do
## not stomp on each other.
tb_updater_stash_root='/var/lib/tb-updater/resume-stash'
tb_updater_stash="${tb_updater_stash_root}/${tb_install_folder}"

tb_downloaded_files_folder="${tb_updater_home}/.cache/${tb_install_folder}/files"
TBB_SANDBOX_RESUME_HINT_FILENAME="${tb_downloaded_files_folder}/resume_hint"

## Scripts like update-mullvadbrowser simply export some environment
## variables, then call update-torbrowser. If sandbox-update-torbrowser is
## called because one of these wrappers was executed, we can either re-execute
## the wrapper, or we can pass through the necessary environment variables and
## execute update-torbrowser instead. The former is both easier (since setpriv
## --reset-env clears the environment before executing the target script) and
## more reliable (since it means we don't have to keep multiple environment
## variable lists in sync).
##
## Accept only a whitelisted set of names since the value is derived from an
## environment variable and is about to be interpolated into an executable
## path.
updater_cmd='/usr/bin/update-torbrowser'
case "${SCRIPTNAME:-}" in
  update-torbrowser|update-mullvadbrowser|update-i2pbrowser)
    updater_cmd="/usr/bin/${SCRIPTNAME}"
    ;;
esac

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
}

ensure_stash_dir() {
  ## Always re-apply ownership and mode, not just on create: an
  ## earlier run that crashed between mkdir and chown would leave
  ## a root-owned directory on disk, and the next stash/restore/purge
  ## (which runs under setpriv-tb-updater) would then fail.
  mkdir --parents -- "${tb_updater_stash}"
  chown --recursive -- tb-updater:tb-updater "${tb_updater_stash_root}"
  chmod 0700 -- "${tb_updater_stash_root}" "${tb_updater_stash}"
}

stash_tarball_to_stash() {
  local untrusted_hint_line hint_key untrusted_hint_val TBB_SIG_FILENAME \
    TBB_PACKAGE_FILENAME

  if ! [ -d "${tb_downloaded_files_folder}" ]; then
    return 0
  fi
  if ! [ -s "${TBB_SANDBOX_RESUME_HINT_FILENAME}" ]; then
    return 0
  fi

  ## Carefully validate the hint file dropped by update-torbrowser before
  ## trusting it to tell us what files to stash.
  unicode-show "${TBB_SANDBOX_RESUME_HINT_FILENAME}" &>/dev/null || return 0
  while read -r untrusted_hint_line; do
    case "${untrusted_hint_line}" in
      TBB_SIG_FILENAME=*) ;;
      TBB_PACKAGE_FILENAME=*) ;;
      *) return 0 ;;
    esac
    [[ "${untrusted_hint_line}" =~ ([^=]*)=(.*) ]] || return 0
    hint_key="${BASH_REMATCH[1]:-}"
    untrusted_hint_val="${BASH_REMATCH[2]:-}"
    [ -n "${hint_key}" ] || return 0
    [ -n "${untrusted_hint_val}" ] || return 0
    [[ "${untrusted_hint_val}" =~ ^[-a-zA-Z0-9_.]+\.tar\.xz(\.asc)?$ ]] \
      || return 0
    ## At this point 'untrusted_hint_val' has been validated and is now
    ## trusted.
    [ "${hint_key}" = 'TBB_SIG_FILENAME' ] \
      && TBB_SIG_FILENAME="${untrusted_hint_val}"
    [ "${hint_key}" = 'TBB_PACKAGE_FILENAME' ] \
      && TBB_PACKAGE_FILENAME="${untrusted_hint_val}"
  done < "${TBB_SANDBOX_RESUME_HINT_FILENAME}"

  ensure_stash_dir
  log notice "Stashing partial archive from '${tb_downloaded_files_folder}' to '${tb_updater_stash}'."

  if [ -n "${TBB_SIG_FILENAME:-}" ]; then
    /usr/libexec/tb-updater/setpriv-tb-updater \
      mv --target-directory="${tb_updater_stash}" \
      -- "${tb_downloaded_files_folder}/${TBB_SIG_FILENAME}"
  fi
  if [ -n "${TBB_PACKAGE_FILENAME:-}" ]; then
    /usr/libexec/tb-updater/setpriv-tb-updater \
      mv --target-directory="${tb_updater_stash}" \
      -- "${tb_downloaded_files_folder}/${TBB_PACKAGE_FILENAME}"
  fi
}

restore_tarball_from_stash() {
  ## Feeds curl's '--continue-at -' so a '--resume' run picks up
  ## where the prior failed run stopped. mkdir runs under
  ## setpriv-tb-updater so the cache hierarchy ends up owned by
  ## tb-updater like the rest of tb_updater_home.

  if ! [ -d "${tb_updater_stash}" ]; then
    log notice "No stash at '${tb_updater_stash}'; nothing to restore for '--resume'."
    return 0
  fi
  if [ "$(find -- "${tb_updater_stash}" -maxdepth 1 -type f | wc -l)" = '0' ]; then
    log notice "Stash '${tb_updater_stash}' is empty; nothing to restore for '--resume'."
    return 0
  fi

  log notice "Restoring stashed archive from '${tb_updater_stash}' to '${tb_downloaded_files_folder}'."
  /usr/libexec/tb-updater/setpriv-tb-updater \
    mkdir --parents -- "${tb_downloaded_files_folder}"

  ## 'find' cannot operate as user 'tb-updater' when the cwd is /root.
  ## Run this in a subshell that has been changed to some other
  ## directory.
  (
    cd /
    /usr/libexec/tb-updater/setpriv-tb-updater \
      find "${tb_updater_stash}" -maxdepth 1 -type f \
        \( -name '*.tar.xz' -o -name '*.tar.xz.asc' \) \
        -exec mv --target-directory="${tb_downloaded_files_folder}" -- '{}' +
  )
}

purge_stash() {
  ## Called on startup of a non-'--resume' run (so the user is not
  ## silently resumed against their wishes) and at the end of a
  ## successful run (so a stale partial does not outlive the install
  ## that consumed it).
  if ! [ -d "${tb_updater_stash}" ]; then
    return 0
  fi
  cleanup_dir_as_tb_updater "${tb_updater_stash}"
}

cleanup() {
  local xhost_output cgroup_empty_str cgroup_drained poll_count

  if [ -n "${tb_updater_cgroup}" ] && validate_uuid "${tb_updater_cgroup_name}"; then
    cgroup_empty_str="populated 0
frozen 0"
    if printf '%s\n' '1' > "${tb_updater_cgroup}/cgroup.kill"; then
      ## Poll cgroup.events until the cgroup is empty, rather than using
      ## inotifywait. This avoids consuming inotify watches, which are more
      ## resource-constrained than CPU power in this scenario.
      cgroup_drained='false'
      poll_count=0
      while (( poll_count < 50 )); 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}'!"
          cgroup_drained='true'
          break
        fi

        light_sleep 0.1
        poll_count=$((poll_count + 1))
      done

      if [ "${cgroup_drained}" = 'false' ]; then
        log warn "Timed out waiting for tb-updater cgroup at '${tb_updater_cgroup}' to drain!"
      fi
    else
      log warn "Cannot kill tb-updater cgroup at '${tb_updater_cgroup}'!"
    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
  ## Stash before cleanup so the partial archive survives the
  ## full-wipe; the rest of tb_updater_home (half-extracted browser
  ## trees etc.) is intentionally wiped so a failed run can't leak
  ## state into the next one.
  if ! stash_tarball_to_stash; then
    log warn "Could not stash partial archive; continuing with cleanup."
  fi
  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'
}

is_resume_enabled() {
  while [ -n "${1:-}" ]; do
    case "${1}" in
      --resume)
        printf '%s\n' 'true'
        return
        ;;
      --)
        break
        ;;
      *)
        shift
        ;;
    esac
  done
  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 resume_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 "${@}")"
  resume_enabled="$(is_resume_enabled "${@}")"
  gui_ready='false'
  [ -n "${SUDO_USER:-}" ] || SUDO_USER=''

  ## cleanup wipes tb_updater_home unconditionally so each run
  ## starts known-good. The resume-stash lives OUTSIDE
  ## tb_updater_home, so the prior run's stashed partial (written
  ## by error_handler) survives and is either restored or purged
  ## below based on '--resume'.
  cleanup
  if [ "${resume_enabled}" = 'true' ]; then
    restore_tarball_from_stash
  else
    purge_stash
  fi

  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' \
        "${updater_cmd}" "${@}"
    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' \
        "${updater_cmd}" "${@}"
    fi
  else
    /usr/libexec/tb-updater/setpriv-tb-updater "${updater_cmd}" "${@}"
  fi

  if [ -d "${tb_updater_home}/${tb_install_folder_dot}/${tb_browser_name}" ] \
    && [ -d "${tb_updater_home}/.cache/${tb_install_folder}" ]; 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_install_folder_dot}" "${root_browser_dir}/"
    mkdir --parents -- "${root_browser_dir}/.cache"
    cp --recursive -- "${tb_updater_home}/.cache/${tb_install_folder}" "${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.
    chown --recursive -- root:root "${root_browser_dir}"
    chmod --recursive og+rX -- "${root_browser_dir}"
    chmod --recursive u+rwX -- "${root_browser_dir}"
  fi

  ## Drop the stash on success so a later '--resume' run doesn't
  ## try to resume against a download that's already been installed.
  purge_stash

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

  cleanup
  exit_clean 0
}

sandbox_update_torbrowser "${@}"
