#!/bin/bash

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

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

## Option targets, assigned only when their flag is given and read
## unconditionally later -- 'verbose' by dispatch_x_active in particular.
## Empty is what they expanded to before strict mode.
verbose=""

## sets: has
## Sourced here rather than with the other helper-scripts near the bottom of
## this file: preparation() and the passive-popup dispatch below both run
## before that point.
# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh
## Sibling repo: absent in an isolated CI checkout. No inline SC1091 disable
## needed -- the CI shellcheck excludes SC1091, and a full-tree run resolves the
## source= path above.
source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/has.sh"
#trap 'sleep 1' DEBUG

## Bound the best-effort filesystem ops in error_handler, so a stuck home dir
## cannot hang the error path. Matches msgprogressbar.
timeout_command=("timeout" "--kill-after" "1" "2")

error_handler() {
   local last_exit_code="$?"
   trap - ERR
   ## '${1:-}': 'trap "error_handler" ERR' passes NO arguments.
   if [ ! "${1:-}" = "" ]; then
      error_text="$1"
   else
      error_text="${BASH_COMMAND}"
   fi

   local msg="<p>
<br></br>$0 script bug.
<br></br>
<br></br>No panic. Nothing is broken. Just some rare condition has been hit.
<br></br>Try again later. There is likely a solution for this problem.
<br></br>Please see News, Blog and User Help Forum.
<br></br>Please report this bug!
<br></br>
<br></br>msgdispatcher_identifier: '<code>${msgdispatcher_identifier:-}</code>'
<br></br>msgdispatcher_appendix: '<code>${msgdispatcher_appendix:-}</code>'
<br></br>
<br></br>error_text: '<code>${error_text}</code>'
<br></br>last_exit_code: '<code>${last_exit_code}</code>'
</p>"

   local stripped_msg
   stripped_msg="$(sanitize-string -- nolimit "${msg}")"
   printf '%s\n' "${stripped_msg}"

   if [ ! -d ~/".msgcollector" ]; then
      mkdir --parents -- ~/".msgcollector"
   fi
   ## Owner-execute for traversal (see msgcollector's error_handler): defensive
   ## against a dir pre-created under a umask that masks the 0100 bit.
   "${timeout_command[@]}" chmod u+rwx -- ~/".msgcollector" || true
   "${timeout_command[@]}" append ~/".msgcollector/msgdispatcher-error.log" "$0: BASH_COMMAND: ${BASH_COMMAND} | exit_code: ${last_exit_code}" >/dev/null || true

   ## Popup window with the message above. Message body on stdin (not argv):
   ## msgdispatcher_dispatch_x.py reads it from stdin so no ARG_MAX cap applies.
   ## /usr/share/icons/icon-pack-dist/whonix.ico might not exist, but
   ## fortunately msgdispatcher_dispatch_x works anyway.
   printf '%s' "${msg}" | "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgdispatcher_dispatch_x.py" -- "warning" "$0" "0" "/usr/share/icons/icon-pack-dist/whonix.ico" &

   true
}

trap "error_handler" ERR

parse_cmd_options() {
   ## Thanks to:
   ## http://mywiki.wooledge.org/BashFAQ/035

   while true; do
       case "${1:-}" in
           --verbose)
               set -x
               verbose="1"
               shift
               ;;
           --)
               shift
               break
               ;;
           -*)
               ## sanitize-echo: $1 is an untrusted caller argument; printing it
               ## raw would let a crafted option inject terminal escapes.
               sanitize-echo -- "$0: unknown option: $1" >&2
               exit 1
               ;;
           *)
               break
               ;;
       esac
   done
}

preparation() {
   ## Sanity test.
   has flock

   XDG_SESSION_TYPE="${XDG_SESSION_TYPE:-}"
   XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-}"
   DISPLAY="${DISPLAY:-}"
   WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-}"

   true "XDG_SESSION_TYPE: ${XDG_SESSION_TYPE}"
   true "DISPLAY: ${DISPLAY}"
   true "WAYLAND_DISPLAY: ${WAYLAND_DISPLAY}"
   ## Declared before the chain, not just in the tty arm: the tty arm set only
   ## cli, so msgdispatcher_handler's unconditional '${gui}' read aborted.
   gui=0
   cli=0

   if [ "${XDG_SESSION_TYPE}" = "x11" ]; then
      gui=1
      cli=0
   elif [ "${XDG_SESSION_TYPE}" = "wayland" ]; then
      gui=1
      cli=0
   elif [ "${XDG_CURRENT_DESKTOP}" = "X-QUBES" ]; then
      gui=1
      cli=0
   elif [ ! "${DISPLAY}" = "" ]; then
      gui=1
      cli=0
   elif [ ! "${WAYLAND_DISPLAY}" = "" ]; then
      gui=1
      cli=0
   elif [ "${XDG_SESSION_TYPE}" = "tty" ]; then
      cli=1
      ## Do this only in /dev/tty1.
      if [ ! "$(tty)" = "/dev/tty1" ]; then
         printf '%s\n' "$0: INFO: Skip, because not running in /dev/tty1."
         exit 0
      fi
   else
      printf '%s\n' "$0: INFO: XDG_SESSION_TYPE is neither x11 nor wayland nor tty, DISPLAY and WAYLAND_DISPLAY are not set either, exiting."
      exit 0
   fi

   if [ "${cli}" = "1" ]; then
      inotifywait_subshell_fifo="${msgcollector_run_dir}/msgdispatcher_cli_subshell_fifo"
   elif [ "${gui}" = "1" ]; then
      inotifywait_subshell_fifo="${msgcollector_run_dir}/msgdispatcher_x_subshell_fifo"
   else
      printf '%s\n' "$$" | sponge -- "${msgcollector_run_dir}/msgdispatcher_piderror" >/dev/null
      exit 3
   fi

   inotifywait_folder="${msgcollector_run_dir}"
   test -d "${inotifywait_folder}"
}

## {{ Small wrapper to use either 'kdialog', 'notify-send' or nothing.
msgdispatcher_passive_type_icon() {
   ## Map a passive-popup type to a freedesktop icon name, so the popup shows
   ## its severity like the active dialog does. Unknown types fall back to info.
   case "$1" in
      error)
         printf '%s' 'dialog-error'
         ;;
      warning)
         printf '%s' 'dialog-warning'
         ;;
      *)
         printf '%s' 'dialog-information'
         ;;
   esac
}

passive_popup_tool() {
   local time title text icon icon_opts
   time="$1"
   ## Both guarded: under nounset an unset $3 aborts the same as an unset $2.
   title="${2:-}"
   text="${3:-}"
   icon="${4:-}"

   ## Fallback.
   ## 'notify-send' does not work if $title is unset.
   if [ "${title}" = "" ]; then
      title="${msgdispatcher_identifier}"
   fi

   ## Pass --icon only when set, so an empty icon does not reach the tool.
   icon_opts=()
   if [ -n "${icon}" ]; then
      icon_opts=("--icon" "${icon}")
   fi

   ## 'notify-send' timeout in milliseconds.

   if has "qubesdb-read" ; then
      if has "notify-send" ; then
         notify-send "${icon_opts[@]}" --expire-time "${time}000" -- "${title}" "${text}"
         return 0
      fi
   fi

   ## check if 'kdialog', 'notify-send' or no passive popup tool is installed
   ## - that is not the case for CLI Custom-Workstation users
   ## - that may not be the case for Gnome users
   if has "kdialog" ; then
      ## 'kdialog' does not support end-of-options.
      kdialog "${icon_opts[@]}" --title "${title}" --passivepopup "${text}" "${time}"
   elif has "notify-send" ; then
      notify-send "${icon_opts[@]}" --expire-time "${time}000" -- "${title}" "${text}"
   fi
}
## }}

dispatch_cli() {
   local msg
   msg="$1"

   if [ -f "${msgcollector_run_dir}/${msgdispatcher_identifier}_parenttty" ]; then
      local parenttty
      parenttty="$(cat "${msgcollector_run_dir}/${msgdispatcher_identifier}_parenttty")" || true
      if [ "${parenttty}" = "/dev/tty1" ]; then
         ## When for example 'systemcheck' was run in 'tty1', then messages were
         ## already echoed by 'msgcollector'. No need to dispatch them again.
         true "Skipping, because parenttty is /dev/tty1."
         return 0
      fi
   fi

   printf '%s\n' "${msg}"
}

dispatch_x_active() {
   local icon
   if [ -f "${msgcollector_run_dir}/${msgdispatcher_identifier}_icon" ]; then
      icon="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_icon")" || true
   else
      ## Fallback.
      if [ -f "/usr/share/icons/gnome/24x24/status/info.png" ]; then
         icon="/usr/share/icons/gnome/24x24/status/info.png"
      else
         icon=""
      fi
   fi

   ## Fallback.
   if [ "${type}" = "" ]; then
      type="info"
   fi

   ## The message body is passed on STDIN, not argv, so no "Argument list too
   ## long" (ARG_MAX) limit caps its length: the full accumulated message must
   ## render (e.g. systemcheck's verbose run, whose large journal dump would
   ## otherwise push later tests past an argv cap and out of the GUI).
   if [ "${verbose}" = "1" ]; then
      printf '%s' "${msg}" | "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgdispatcher_dispatch_x.py" -- "${type}" "${title}" "0" "${icon}"
   else
      ## Launching into background, so it doesn't block msgdispatcher until
      ## msgdispatcher_dispatch_x exits.
      printf '%s' "${msg}" | "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgdispatcher_dispatch_x.py" -- "${type}" "${title}" "0" "${icon}" &
   fi
}

dispatch_x_passive() {
   local icon
   ## Give the passive popup a per-type icon, so its severity is visible like
   ## the active dialog's. ${type} is the caller's type read from the dedicated
   ## passive-popup type file (info when unset).
   icon="$(msgdispatcher_passive_type_icon "${type:-}")"
   passive_popup_tool "20" "${title}" "${msg}" "${icon}"
}

inotifywait_setup() {
   safe-rm --force -- "${inotifywait_subshell_fifo}"
   mkfifo -- "${inotifywait_subshell_fifo}"
   ## Start inotifywait in a sub process to continuously monitor the directory.
   inotifywait --quiet --monitor --event close_write --format "%w%f" -- "${inotifywait_folder}" > "${inotifywait_subshell_fifo}" &
   inotifywait_main_pid="$!"
}

parse_existing_files() {
   for file_name in "${inotifywait_folder}/"*; do
      true "parse_existing_files: file_name: ${file_name}"
      msgdispatcher_handler || true
   done
}

msgdispatcher_handler() {
   true "msgdispatcher_handler: file_name: ${file_name}"
   file_extension="${file_name##*_}"
   if [ ! "${file_extension}" = "done" ]; then
      true "msgdispatcher_handler: Not a done file. Stop processing, ok."
      true "----------"
      return 0
   fi
   true "msgdispatcher_handler: Done file. Continue processing..."

   ## Remove everything after the identifier.
   temp_item="${file_name%%_*}"
   ## Remove "${msgcollector_run_dir}/".
   msgdispatcher_identifier="${temp_item##*/}"

   ## Validate early (before using in file paths). Use conditional to avoid
   ## triggering error_handler in this long-running daemon.
   if ! check_is_alpha_numeric "msgdispatcher_identifier" 2>/dev/null; then
      true "msgdispatcher_handler: identifier is not alphanumeric, skipping."
      true "----------"
      return 0
   fi

   if [ "${gui}" = "1" ]; then
      if [ "${file_name}" = "${msgcollector_run_dir}/${msgdispatcher_identifier}_messagex_done" ]; then
         if [ -f "${msgcollector_run_dir}/${msgdispatcher_identifier}_messagex" ]; then
            true "INFO: messagex file exists."
            msg="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_messagex")" || true
            title="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_titlex")" || true
            type="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_typex")" || true
            dispatch_x_active "${type}" "${msg}"
            msgdispatcher_delete_wrapper "messagex_done"
            msgdispatcher_delete_wrapper "titlex"
            msgdispatcher_delete_wrapper "messagex"
            msgdispatcher_delete_wrapper "typex"
         else
            true "INFO: messagex file does NOT exist."
            ## Not using rm outside the if, to prevent race conditions.
            ## Not always using rm, without if to prevent forking.
            msgdispatcher_delete_wrapper "messagex_done"
         fi
         true "----------"
         return 0
      fi
      if [ "${file_name}" = "${msgcollector_run_dir}/${msgdispatcher_identifier}_passivepopupqueuex_done" ]; then
         if [ -f "${msgcollector_run_dir}/${msgdispatcher_identifier}_passivepopupqueuex" ]; then
            msg="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_passivepopupqueuex")" || true
            ## 2>/dev/null: the title and type files are optional (written only
            ## when the caller gives --passivepopupqueuextitle / --typex), so a
            ## missing one must not spam this daemon's journal with 'cat: ... No
            ## such file'. The || true keeps the read non-fatal.
            title="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_passivepopupqueuextitle" 2>/dev/null)" || true
            ## Read the caller's type from the dedicated passive-popup type file,
            ## NOT _typex, so a concurrent messagex for the same identifier cannot
            ## clobber it. dispatch_x_passive maps it to a per-type icon only.
            type="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_passivepopupqueuextype" 2>/dev/null)" || true
            if [ "${type}" = "" ]; then
               type="info"
            fi
            if [ -f "${msgcollector_run_dir}/${msgdispatcher_identifier}_forceactive" ]; then
               #dispatch_x_active "${type}" "${msg}"
               dispatch_x_passive "${type}" "${title}" "${msg}" || true
            else
               dispatch_x_passive "${type}" "${title}" "${msg}" || true
            fi
            msgdispatcher_delete_wrapper "forceactive"
            msgdispatcher_delete_wrapper "passivepopupqueuex_done"
            msgdispatcher_delete_wrapper "passivepopupqueuex"
            msgdispatcher_delete_wrapper "passivepopupqueuextitle"
            msgdispatcher_delete_wrapper "passivepopupqueuextype"
         else
            msgdispatcher_delete_wrapper "passivepopupqueuex_done"
         fi
         true "----------"
         return 0
      fi
   elif [ "${cli}" = "1" ]; then
      if [ "${file_name}" = "${msgcollector_run_dir}/${msgdispatcher_identifier}_waitmessagecli_done" ]; then
         if [ -f "${msgcollector_run_dir}/${msgdispatcher_identifier}_waitmessagecli" ]; then
            true "INFO: waitmessagecli file exists."
            msg="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_waitmessagecli")" || true
            type="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_typecli")" || true
            dispatch_cli "${msg}"
            msgdispatcher_delete_wrapper "waitmessagecli_done"
            msgdispatcher_delete_wrapper "waitmessagecli"
            msgdispatcher_delete_wrapper "typecli"
         else
            true "INFO: waitmessagecli file does NOT exist."
            msgdispatcher_delete_wrapper "waitmessagecli_done"
         fi
         true "----------"
         return 0
      fi
      if [ "${file_name}" = "${msgcollector_run_dir}/${msgdispatcher_identifier}_messagecli_done" ]; then
         if [ -f "${msgcollector_run_dir}/${msgdispatcher_identifier}_messagecli" ]; then
            msg="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_messagecli")" || true
            type="$(cat -- "${msgcollector_run_dir}/${msgdispatcher_identifier}_typecli")" || true
            dispatch_cli "${msg}"
            msgdispatcher_delete_wrapper "messagecli_done"
            msgdispatcher_delete_wrapper "messagecli"
            msgdispatcher_delete_wrapper "typecli"
         else
            msgdispatcher_delete_wrapper "messagecli_done"
         fi
         true "----------"
         return 0
      fi
   else
      true "----------"
      exit 1
   fi
}

msgdispatcher_delete_wrapper() {
   local file_name full_path

   msgdispatcher_appendix="$1"

   check_is_alpha_numeric "msgdispatcher_identifier"
   check_is_alpha_numeric "msgdispatcher_appendix"

   file_name="${msgdispatcher_identifier}_${msgdispatcher_appendix}"
   validate_safe_filename "file_name"

   full_path="${msgcollector_run_dir}/${msgdispatcher_identifier}_${msgdispatcher_appendix}"
   check_is_not_empty_and_only_one_line "full_path"

   if test -f "${full_path}" ; then
      safe-rm -f -- "${full_path}"
   else
      true "INFO: file_name ${full_path} does not exist."
   fi

   ## In case above errors, do not additionally error out from this function.
   true
}

inotifywait_loop() {
   true "$0: Starting loop."

   ## Launching inotifywait_loop into the background.
   while read -r -- file_name; do
      true "file_name: ${file_name}"
      msgdispatcher_handler || true
   done < "${inotifywait_subshell_fifo}" &
   inotifywait_subshell_pid="$!"
}

msgdispatcher_loop() {
   preparation
   fallbacks ## provided by /usr/libexec/msgcollector/msgfallbacks
   inotifywait_setup
   parse_existing_files

   ## Wait for system to be ready so 'systemd-notify' can be used.
   ## Wait until systemctl at least reports 'degraded'.
   ## But overwrite with '|| true' because msgdispatcher should still start.
   ## This might lead to an infinite wait because 'msgcollector-gui.service'
   ## is a systemd user unit.
   #systemctl --user --wait is-system-running &>/dev/null || true

   ## systemd-notify sometimes exists non-zero. Unknown how to reproduce.
   while ! "${systemd_notify[@]}" --pid="$$" --ready &>/dev/null; do
      light_sleep 1
   done
   inotifywait_loop

   while true; do
      "${systemd_notify[@]}" --pid="$$" WATCHDOG=1 &>/dev/null || true
      light_sleep 10
      ## Check that pids are still running.
      kill -0 -- "${inotifywait_main_pid}"
      kill -0 -- "${inotifywait_subshell_pid}"
   done
}

source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/strings.bsh"

source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/light_sleep.bsh"

## sets: systemd_notify
source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/systemd-notify.bsh"

## sets: ${msgcollector_run_dir}
# shellcheck source=./usr/libexec/msgcollector/msgcollector_shared
source "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgcollector_shared"
folder_init

# shellcheck source=./usr/libexec/msgcollector/msgfallbacks
source "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgfallbacks"

parse_cmd_options "$@"
msgdispatcher_loop
