#!/bin/bash

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

## Strict mode. msgcollector reads many optional CLI fields (icon, titlecli,
## typecli, progressbaridx, ...) that are legitimately unset; they are defaulted
## to '' below so nounset does not abort on an unset read, and parse_cmd_options
## overwrites them when the flag is actually given.

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

scriptname="$(basename -- "${BASH_SOURCE[0]}")"

## 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")

## Every optional CLI field, defaulted so a read before its flag is parsed is
## safe under nounset. Also the two error_handler reads (my_command_line_
## parameters, write_to_file). Color variables are NOT defaulted here: colors()
## calls get_colors (which always defines every one, '' when disabled) and runs
## before parse_cmd_options in main().
cli=""
done=""
forceactive=""
forget=""
forgetwaitcli=""
icon=""
identifier=""
message=""
messagecli=""
messagex=""
newlinecli=""
nonewlinex=""
onlyecho=""
parentpid=""
parenttty=""
passivepopupqueuex=""
passivepopupqueuextitle=""
progressbaridx=""
progressbartitlex=""
progressbarx=""
progressbarxprogresstxtexisting=""
progressbarxrunning=""
progressx=""
status=""
titlecli=""
titlex=""
typecli=""
typecli_chosen=""
typex=""
typex_chosen=""
verbose=""
waitmessagecli=""
my_command_line_parameters=""
write_to_file=""

error_handler() {
   local exit_code="$?"

   local my_pstree
   my_pstree="$(pstree -p $$)" || true
   local ps_p_parentpid=""

   if [ ! "${parentpid}" = "" ]; then
      if [ ! "${parentpid}" = "0000000000" ]; then
         ps_p_parentpid="$(ps -p "${parentpid}")" || true
      fi
   fi

   local msg="\
###############################################################################
## ${scriptname} script bug.
## No panic. Nothing is broken. Just some rare condition has been hit.
## Try again later. There is likely a solution for this problem.
## Please see Whonix News, Whonix Blog and Whonix User Help Forum.
## Please report this bug!
##
## scriptname: '${scriptname}'
## identifier: '${identifier}'
## my_command_line_parameters: '${my_command_line_parameters}'
## write_to_file: '${write_to_file}'
## parentpid: '${parentpid}'
## ps_p_parentpid: '${ps_p_parentpid}'
## my_pstree: '${my_pstree}'
## BASH_COMMAND: '${BASH_COMMAND}'
## exit_code: '${exit_code}'
###############################################################################\
"
   printf '%s\n' "${msg}" >&2

   local home_var
   home_var=~/

   if ! test -w ~/ ; then
      printf '%s\n' "${scriptname}: skipping log to ~/.msgcollector/msgdispatcher-error.log since not writeable home_var: ${home_var}" >&2
      exit 1
   fi

   if [ ! -d ~/".msgcollector" ]; then
      mkdir --parents ~/".msgcollector"
   fi
   ## Owner-execute so the directory is traversable: a umask that masks the 0100
   ## bit leaves ~/.msgcollector non-traversable and the write below fails with
   ## EACCES. Kicksecure's default umask (027) does not mask it, so this is
   ## defensive -- for a dir pre-created without owner-x. Every error path that
   ## writes this log does the same (msgprogress, msgprogressbar, msgdispatcher).
   "${timeout_command[@]}" chmod u+rwx -- ~/".msgcollector" || true

   if ! touch -- ~/".msgcollector/msgdispatcher-error.log" ; then
      printf '%s\n' "${scriptname}: skipping log to ${home_var}/.msgcollector/msgdispatcher-error.log since failed to 'touch' file." >&2
      exit 1
   fi

   if [ ! -w  ~/".msgcollector/msgdispatcher-error.log" ]; then
      printf '%s\n' "${scriptname}: skipping log to ~/.msgcollector/msgdispatcher-error.log since not writeable file: ${home_var}/.msgcollector" >&2
      exit 1
   fi

   "${timeout_command[@]}" append ~/".msgcollector/msgdispatcher-error.log" "\
scriptname: ${scriptname}
identifier: ${identifier}
my_command_line_parameters: ${my_command_line_parameters}
parentpid: ${parentpid}
ps_p_parentpid: ${ps_p_parentpid}
my_pstree: ${my_pstree}
BASH_COMMAND: ${BASH_COMMAND}
exit_code: ${exit_code}
" >/dev/null || true

   true "INFO: end of $0 error_handler, ok."

   exit 1
}

require_option_value() {
   ## Fail cleanly when a value-taking option is given with no value (it is the
   ## last argument). Otherwise the following 'shift 2' would shift past the end
   ## under 'set -o errexit' + 'shopt -s shift_verbose', trip the ERR trap and
   ## report a bogus "script bug" instead of this clear message.
   ## $1 - option name (for the message).
   ## $2 - remaining argument count ("$#" at the call site, which counts the
   ##      option itself); a value is present only when it is >= 2.
   if [ "${2}" -lt "2" ]; then
      printf '%s\n' "${BASH_SOURCE[0]}: ERROR: option ${1} requires a value."
      exit 1
   fi
}

parse_cmd_options() {
   trap "error_handler" ERR

   ## Thanks to:
   ## http://mywiki.wooledge.org/BashFAQ/035

   true "${cyan}INFO:${reset} parse_cmd_options msgcollector"

   while true; do
       case "${1:-}" in
           --verbose)
               set -x
               verbose="1"
               shift
               ;;
           --debug)
               ## Accepted for compatibility; no debug behaviour is wired up.
               shift
               ;;
           --identifier)
               require_option_value "--identifier" "$#"
               identifier="${2:-}"
               shift 2
               ;;
           --icon)
               require_option_value "--icon" "$#"
               icon="${2:-}"
               if [ "${icon}" = "" ]; then
                  printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable icon is empty."
                  exit 1
               fi
               shift 2
               ;;
           --parentpid)
               require_option_value "--parentpid" "$#"
               parentpid="${2:-}"
               shift 2
               ;;
           --typecli)
               require_option_value "--typecli" "$#"
               cli="1"
               typecli="${2:-}"
               shift 2
               if [ "${typecli}" = "" ]; then
                  printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable typecli is empty."
                  exit 1
               fi
               ;;
           --typex)
               require_option_value "--typex" "$#"
               typex="${2:-}"
               shift 2
               if [ "${typex}" = "" ]; then
                  printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable typex is empty."
                  exit 1
               fi
               case "${typex}" in
                  info|warning|error)
                     ;;
                  *)
                     printf '%s\n' "${BASH_SOURCE[0]}: ERROR: invalid typex '${typex}' (expected info, warning, or error)."
                     exit 1
                     ;;
               esac
               ;;
           --typexstatus)
               ## for --status
               shift
               typex_chosen="1"
               ;;
           --typeclistatus)
               ## for --status
               shift
               typecli_chosen="1"
               ;;
           --message)
               require_option_value "--message" "$#"
               message="${2:-}"
               shift 2
               if [ "${message}" = "" ]; then
                  printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable message is empty."
                  exit 1
               fi
               ;;
           --titlecli)
               require_option_value "--titlecli" "$#"
               cli="1"
               titlecli="${2:-}"
               shift 2
               ;;
           --titlex)
               require_option_value "--titlex" "$#"
               titlex="${2:-}"
               shift 2
               ;;
           --progressbartitlex)
               require_option_value "--progressbartitlex" "$#"
               progressbartitlex="${2:-}"
               shift 2
               ;;
           --passivepopupqueuextitle)
               require_option_value "--passivepopupqueuextitle" "$#"
               passivepopupqueuextitle="${2:-}"
               shift 2
               ;;
           --messagex)
               messagex="1"
               shift
               ;;
           --messagecli)
               cli="1"
               messagecli="1"
               shift
               ;;
           --waitmessagecli)
               waitmessagecli="1"
               shift
               ;;
           --passivepopupqueuex)
               passivepopupqueuex="1"
               shift
               ;;
           --progressx)
               require_option_value "--progressx" "$#"
               progressx="${2:-}"
               shift 2
               if [ "${progressx}" = "" ]; then
                  printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable progressx is empty."
                  exit 1
               fi
               ;;
           --progressbarx)
               progressbarx="1"
               shift
               ;;
           --progressbarxrunning)
               progressbarxrunning="1"
               shift
               ;;
           --progressbarxprogresstxtexisting)
               progressbarxprogresstxtexisting="1"
               shift
               ;;
           --nonewlinex)
               nonewlinex="1"
               shift
               ;;
           --newlinecli)
               newlinecli="1"
               shift
               ;;
           --forceactive)
               forceactive="1"
               shift
               ;;
           --parenttty)
               require_option_value "--parenttty" "$#"
               parenttty="${2:-}"
               shift 2
               ;;
           --done)
               done="1"
               shift
               ;;
           --forget)
               forget="1"
               shift
               ;;
           --forgetwaitcli)
               forgetwaitcli="1"
               shift
               ;;
           --progressbaridx)
               require_option_value "--progressbaridx" "$#"
               progressbaridx="${2:-}"
               shift 2
               ;;
           --status)
               status="1"
               shift
               ;;
           --onlyecho)
               onlyecho="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 -- "${scriptname}: unknown option: $1" >&2
               exit 1
               ;;
           *)
               break
               ;;
       esac
   done

   ## If there are input files (for example) that follow the options, they
   ## will remain in the "$@" positional parameters.

   if (( $# != 0 )); then
      ## sanitize-echo: $1 is an untrusted caller argument (see above).
      sanitize-echo -- "${scriptname}: unknown option: $1" >&2
      exit 3
   fi
}

preparation() {
   trap "error_handler" ERR
}

colors() {
   trap "error_handler" ERR

   if [ ! "${TERM:-}" = "" ]; then
      ASSUME_TERM_PRESENT=true
   fi
   source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/get_colors.sh"
   get_colors
}

cli_links_to_footnotes() {
   ## Convert GUI hyperlinks in a CLI message to footnotes: each
   ## <a href="url">text</a> becomes "text[N]" inline, and the URLs are listed
   ## under a trailing "Links:" section. This keeps URLs out of the flow text
   ## while preserving them (strip-markup would otherwise drop the href).
   ## An anchor whose text is already the URL is the exception: it is emitted
   ## as the bare URL inline with no footnote, since a footnote would only
   ## repeat that same URL. Echoes the transformed message. Handles quoted or
   ## unquoted href.
   local rest="$1" footer="" n=0 whole url text before after
   local re='<a href="?([^">]*)"?>([^<]*)</a>'
   while [[ "${rest}" =~ ${re} ]]; do
      whole="${BASH_REMATCH[0]}"
      url="${BASH_REMATCH[1]}"
      text="${BASH_REMATCH[2]}"
      before="${rest%%"${whole}"*}"   # text before the first occurrence
      after="${rest#*"${whole}"}"     # text after it
      if [ "${text}" = "${url}" ]; then
         ## Link text is the URL itself, so just include verbatim
         rest="${before}${url}${after}"
         continue
      fi
      n=$(( n + 1 ))
      rest="${before}${text}[${n}]${after}"
      footer="${footer}[${n}] ${url}"$'\n'
   done
   if [ "${n}" -gt 0 ]; then
      ## Format kept to '%s'/'%s\n' only (R-030): rest, blank line, "Links:"
      ## header, then the footer with no trailing newline.
      printf '%s\n' "${rest}"
      printf '%s\n' ''
      printf '%s\n' 'Links:'
      printf '%s' "${footer}"
   else
      ## No trailing newline: a plain CLI message must render verbatim
      ## (test_cli_rendering asserts 'See: <url>' stays exactly that). The n>0
      ## path's trailing newline comes from $footer, not a deliberate terminator.
      printf '%s' "${rest}"
   fi
}

cli_translate_gui_markup() {
   ## Translate a GUI (HTML) message into its CLI equivalent, in the order that
   ## must run BEFORE strip-markup: color tags, hyperlinks, line breaks. Echoes
   ## the transformed message. The color codes come from get_colors and are ''
   ## when color is disabled, so nothing is inserted in that case.
   trap "error_handler" ERR

   local text
   text="$1"

   ## Translate GUI color tags (<font color="...">) to terminal ANSI colors so a
   ## single HTML message renders colored in BOTH the GUI and the CLI, in ONE
   ## sed process. Safe here without regex escaping: the search tags are fixed
   ## and carry no regex metacharacter, and the replacements come from get_colors
   ## as raw SGR escapes ('\033[..m', no '&'/backslash/delimiter), so neither
   ## side is reinterpreted. The delimiter is '#' for the '</font>' expression
   ## because that tag contains '/'. Empty when color is disabled -> tag removed.
   text="$(printf '%s' "${text}" | sed \
      -e "s/<font color=\"green\">/${green:-}/g" \
      -e "s/<font color=\"orange\">/${yellow:-}/g" \
      -e "s/<font color=\"yellow\">/${yellow:-}/g" \
      -e "s/<font color=\"red\">/${red:-}/g" \
      -e "s#</font>#${reset:-}#g")"
   ## Translate GUI hyperlinks (<a href="url">text</a>) to footnote form.
   text="$(cli_links_to_footnotes "${text}")"
   ## Translate GUI line breaks (<br>, <br/>, <br />) to real newlines, so a
   ## single HTML message keeps its line breaks on the CLI too. strip-markup
   ## would otherwise drop <br> and run the lines together. The <br></br>
   ## closing half is left for strip-markup to remove.
   ##
   ## Callers build multi-line HTML strings that carry BOTH a literal source
   ## newline AND a <br> on each line (the newline is insignificant HTML
   ## whitespace a browser collapses; the <br> is the actual break). Absorb the
   ## whitespace on both sides of each <br> into the single newline it becomes,
   ## so one logical break renders as one newline, not a blank line. -z (NUL
   ## record separator) slurps the whole message into one pattern space so
   ## [[:space:]] spans the source newlines; without it sed works line-by-line
   ## and cannot see the newline adjacent to a line-leading <br>. An intentional
   ## blank line (<br><br>, no whitespace between) still yields two newlines, and
   ## a CLI-native message with literal newlines but no <br> is untouched. This
   ## is a regex (all <br> spellings in one pass), so sed -zE stays here.
   text="$(printf '%s' "${text}" | sed -zE -e 's#[[:space:]]*<br ?/?>[[:space:]]*#\n#g')"

   printf '%s' "${text}"
}

pretty_type_cli() {
   trap "error_handler" ERR

   if [ "$1" = "info" ]; then
      p_type="${green}INFO${reset:-}"
   elif [ "$1" = "warning" ]; then
      p_type="${red}WARNING${reset:-}"
   elif [ "$1" = "error" ]; then
      p_type="${red}${bold}ERROR${reset:-}"
   else
      p_type="${red}???${reset:-}"
   fi
}

pretty_type_x() {
   trap "error_handler" ERR

   local msgINFO msgWARNING msgERROR
   msgINFO="<p><span style=color:#008000>INFO</span>:"
   msgWARNING="<p><span style=color:#c00000>WARNING</span>:"
   msgERROR="<p><span style=font-weight:600;color:#ff0000>ERROR</span>:"

   local first_three
   first_three="${message:0:3}"
   if [ "${first_three}" = "<p>" ]; then
      local message_length remaining_chars message_without_p
      message_length="${#message}"
      remaining_chars="$(( message_length - 3 ))"
      message_without_p="${message:3:${remaining_chars}}"
      if [ "$1" = "info" ]; then
         message="${msgINFO} ${message_without_p}"
      elif [ "$1" = "warning" ]; then
         message="${msgWARNING} ${message_without_p}"
      elif [ "$1" = "error" ]; then
         message="${msgERROR} ${message_without_p}"
      fi
   fi
}

write_typecli_to_file() {
   # $1 - type
   # $2 - file
   if [ "${onlyecho}" = "1" ]; then
      return 0
   fi
   printf '%s\n' "$1" > "$2"
}

escalate_type_file() {
   ## Write $2 (info|warning|error) to the type file $1 without DOWNGRADING an
   ## existing higher severity (info < warning < error). A passive popup queue
   ## aggregates several messages under one identifier, so -- like the messagex
   ## _typex escalation -- the worst severity must win, not the last write.
   ## Concurrent same-identifier writers are expected (the passive queue and
   ## messagex both aggregate under one identifier), so the read-modify-write
   ## runs under an exclusive flock on a per-type-file '<file>.lock' -- otherwise
   ## two writers can both read the old value and the last to write downgrades.
   # $1 - type file, $2 - new type
   local type_file new
   type_file="$1"
   new="$2"
   (
      flock --exclusive 9
      old=""
      if [ -f "${type_file}" ]; then
         old="$(cat -- "${type_file}" 2>/dev/null)" || true
      fi
      case "${old}" in
         error)
            ## already the worst; never downgrade
            exit 0
            ;;
         warning)
            [ "${new}" = "error" ] || exit 0
            ;;
         info)
            { [ "${new}" = "error" ] || [ "${new}" = "warning" ]; } || exit 0
            ;;
      esac
      printf '%s\n' "${new}" > "${type_file}"
   ) 9>>"${type_file}.lock"
}

stdisplay_message() {
   ## Sanitize a CLI message from stdin with stdisplay (stcat): drop non-ASCII
   ## and every dangerous (non-SGR) escape, keep only a safe SGR subset. Honor
   ## msgcollector's OWN color decision: get_colors (via colors()'s
   ## ASSUME_TERM_PRESENT) can enable color where stdisplay's own NO_COLOR /
   ## TERM=dumb defaults would disable SGR. If they disagreed, stdisplay would
   ## replace only the ESC of a color we just inserted and leave visible '[..m'
   ## debris. So when color is on (${green} set) force stdisplay to allow SGR
   ## too. Non-SGR escapes (cursor moves, clears, OSC) are neutralized either
   ## way -- that is the security guarantee, independent of the color policy.
   if [ -n "${green:-}" ]; then
      env --unset=NO_COLOR COLORTERM=truecolor stcat
   else
      stcat
   fi
}

collector() {
   trap "error_handler" ERR

   ## Input sanitization summary:
   ## - identifier, progressbaridx: validated alphanumeric via check() (path traversal prevention)
   ## - icon, titlex, passivepopupqueuextitle, progressbartitlex: sanitize-string (plain text fields)
   ## - messagecli: strip-markup only (preserves terminal colors, strips HTML)
   ## - waitmessagecli, passivepopupqueuex, progressbarx message: sanitize-string
   ## - messagex: NOT sanitized (intentional - contains caller-constructed HTML for setHtml())

   ## {{ --identifier
   if ! msgcollector_check "${identifier}"; then
      printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable 'identifier' is empty or invalid."
      exit 1
   fi
   ## }}

   ## {{ --icon
   if [ ! "${icon}" = "" ]; then
      ## Debugging.
      #ls -la "${msgcollector_run_dir}"
      #test -r "${msgcollector_run_dir}"
      #test -w "${msgcollector_run_dir}"
      #test -r "${msgcollector_run_dir}/${identifier}_icon" || true "not readable"
      #test -w "${msgcollector_run_dir}/${identifier}_icon" || true "not writeable"
      #whoami
      ## NOTE: If not writeable, AppArmor issues?
      icon="$(sanitize-string -- nolimit "${icon}")"
      printf '%s\n' "${icon}" > "${msgcollector_run_dir}/${identifier}_icon"
   fi
   ## }}

   ## {{ --parentpid
   if [ "${parentpid}" = "" ]; then
      #printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable parentpid is empty."
      #exit 1
      ## Small hack, so not all applications get killed when pressing the cancel button
      ## such as timesync.
      parentpid="0000000000"
   fi
   ## Not at this point to avoid useless files.
   #printf '%s\n' "$parentpid" > "${msgcollector_run_dir}/${identifier}_${progressbaridx}_parentpid"
   ## }}

   ## {{ --titlecli
   if [ -n "${titlecli}" ]; then
      ## --titlecli is parsed but not yet consumed by msgdispatcher.
      true
      #printf '%s\n' "${titlecli}" > "${msgcollector_run_dir}/${identifier}_titlecli"
   fi
   ## }}

   ## {{ --titlex
   if [ -n "${titlex}" ]; then
      ## variable $titlex exists
      titlex="$(sanitize-string -- nolimit "${titlex}")"
      printf '%s\n' "${titlex}" > "${msgcollector_run_dir}/${identifier}_titlex"
   fi
   ## }}

   ## {{ --passivepopupqueuextitle
   if [ -n "${passivepopupqueuextitle}" ]; then
      ## variable $passivepopupqueuextitle exists
      passivepopupqueuextitle="$(sanitize-string -- nolimit "${passivepopupqueuextitle}")"
      printf '%s\n' "${passivepopupqueuextitle}" > "${msgcollector_run_dir}/${identifier}_passivepopupqueuextitle"
   fi
   ## }}

   ## {{ --progressbartitlex
   if [ -n "${progressbartitlex}" ]; then
      ## variable $progressbartitlex exists
      progressbartitlex="$(sanitize-string -- nolimit "${progressbartitlex}")"
      printf '%s\n' "${progressbartitlex}" > "${msgcollector_run_dir}/${identifier}_${progressbaridx}_progressbartitlex"
   fi
   ## }}

   ## {{ --typecli
   if [ "${messagecli}" = "1" ]; then
      ## Serialize the read-modify-write against a concurrent same-identifier
      ## writer (see escalate_type_file): flock the type file, no truncate.
      (
         flock --exclusive 9
         old_typecli=""
         if [ -f "${msgcollector_run_dir}/${identifier}_typecli" ]; then
            old_typecli="$(cat -- "${msgcollector_run_dir}/${identifier}_typecli")"
         fi

         if [ "${old_typecli:-}" = "info" ]; then
            if [ "${typecli}" = "error" ]; then
               ## Upgrade from info to error.
               write_typecli_to_file "error" "${msgcollector_run_dir}/${identifier}_typecli"
            elif [ "${typecli}" = "warning" ]; then
               ## Upgrade from info to warning.
               write_typecli_to_file "warning" "${msgcollector_run_dir}/${identifier}_typecli"
            fi
         elif [ "${old_typecli:-}" = "warning" ]; then
            if [ "${typecli}" = "error" ]; then
               ## Upgrade from warning to error.
               write_typecli_to_file "error" "${msgcollector_run_dir}/${identifier}_typecli"
            fi
         elif [ "${old_typecli:-}" = "error" ]; then
            true
         else
            if [ ! "${typecli}" = "" ]; then
               write_typecli_to_file "${typecli}" "${msgcollector_run_dir}/${identifier}_typecli"
            fi
         fi
      ) 9>>"${msgcollector_run_dir}/${identifier}_typecli.lock"
   fi
   ## }}

   ## {{ --typex
   if [ "${messagex}" = "1" ]; then
      ## Serialize the read-modify-write against a concurrent same-identifier
      ## writer (see escalate_type_file): flock the type file, no truncate.
      (
         flock --exclusive 9
         old_typex=""
         if [ -f "${msgcollector_run_dir}/${identifier}_typex" ]; then
            old_typex="$(cat -- "${msgcollector_run_dir}/${identifier}_typex")"
         fi

         if [ "${old_typex:-}" = "info" ]; then
            if [ "${typex}" = "error" ]; then
               ## Upgrade from info to error.
               printf '%s\n' "error" > "${msgcollector_run_dir}/${identifier}_typex"
            elif [ "${typex}" = "warning" ]; then
               ## Upgrade from info to warning.
               printf '%s\n' "warning" > "${msgcollector_run_dir}/${identifier}_typex"
            fi
         elif [ "${old_typex:-}" = "warning" ]; then
            if [ "${typex}" = "error" ]; then
               ## Upgrade from warning to error.
               printf '%s\n' "error" > "${msgcollector_run_dir}/${identifier}_typex"
            fi
         elif [ "${old_typex:-}" = "error" ]; then
            true
         else
            printf '%s\n' "${typex}" > "${msgcollector_run_dir}/${identifier}_typex"
         fi
      ) 9>>"${msgcollector_run_dir}/${identifier}_typex.lock"
   fi
   ## }}

   ## {{ --messagex
   ## NOTE: $message is intentionally NOT sanitized here.
   ## messagex messages contain caller-constructed HTML (<p>, <span>, <br>, etc.)
   ## that is rendered via setHtml() in msgdispatcher_dispatch_x.
   ## sanitize-string would strip this HTML and break the GUI output.
   ## Callers are responsible for providing safe HTML content.
   if [ "${messagex}" = "1" ]; then
      pretty_type_x "${typex}"
      write_to_file="${msgcollector_run_dir}/${identifier}_messagex"
      append_to_file
   fi
   ## }}

   ## {{ --messagecli
   if [ "${messagecli}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_messagecli"
      if [ -n "${message}" ]; then
         ## variable $message exists
         ## Sanitize untrusted input up front: stdisplay (stcat) drops non-ASCII
         ## and dangerous escape sequences, keeping only a safe SGR subset, so a
         ## crafted string cannot confuse the markup translation or strip-markup
         ## below. Terminal colors a caller legitimately embedded (e.g. the login
         ## security check table) fall in that safe subset and survive.
         message="$(printf '%s' "${message}" | stdisplay_message)"
         ## Translate GUI markup (color tags, hyperlinks, line breaks) to CLI.
         message="$(cli_translate_gui_markup "${message}")"
         ## Strip the remaining HTML tags for CLI display.
         message="$(strip-markup "${message}")"
         ## Sanitize once more: strip-markup expands HTML entities, which can
         ## reintroduce escape sequences. This second pass neutralizes them while
         ## preserving the SGR the markup translation inserted. Combined with the
         ## first pass, the color translation and strip-markup, this is as strong
         ## as a 'sanitize-string' call, so unsanitized input is safe here.
         message="$(printf '%s' "${message}" | stdisplay_message)"
         pretty_type_cli "${typecli}"
         message="[${p_type}] [${identifier}] ${message}"
         printf '%s\n' "${message}"
      fi
      append_to_file
   fi
   ## }}

   ## {{ --waitmessagecli
   if [ "${waitmessagecli}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_waitmessagecli"
      if [ -n "${message}" ]; then
         ## variable $message exists
         message="$(sanitize-string -- nolimit "${message}")"
         pretty_type_cli "${typecli}"
         message="[${p_type}] [${identifier}] ${message}"
         printf '%s\n' "${message}"
      fi
      append_to_file
   fi
   ## }}

   ## {{ --passivepopupqueuex
   if [ "${passivepopupqueuex}" = "1" ]; then
      if [ "${message}" = "" ]; then
         message="message was empty."
      fi
      message="$(sanitize-string -- nolimit "${message}")"
      if [ "${message}" = "" ]; then
         message="message was empty after sanitize-string."
      fi
      if [ -n "${typex}" ]; then
         ## Dedicated passive-popup type file, kept separate from messagex's
         ## _typex so msgdispatcher's passive path reads the caller's type
         ## without colliding with a concurrent messagex for the same identifier.
         ## msgdispatcher only maps this to a per-type icon, never runs it.
         ## Escalate (never downgrade): a queued error keeps its error icon even
         ## if a later info message is appended to the same popup.
         escalate_type_file "${msgcollector_run_dir}/${identifier}_passivepopupqueuextype" "${typex}"
      fi
      write_to_file="${msgcollector_run_dir}/${identifier}_passivepopupqueuex"
      append_to_file
   fi
   ## }}

   ## {{ --progressx
   if [ ! "${progressx}" = "" ]; then
      if ! msgcollector_check "${progressbaridx}"; then
         error "Variable 'progressbaridx' is empty or invalid."
         exit 4
      fi

      ## Debugging.
      #caller="$(ps -p $PPID)" || true
      #printf '%s\n' "msgcollector: progressx: $progressx | caller: $PPID | $caller" >> /home/user/progresslog

      if [ "${verbose}" = "1" ]; then
         bash -x "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgprogress" --identifier "${identifier}" --progressbaridx "${progressbaridx}" --progress "${progressx}"
      else
         ## & disown to prevent long waiting.
         "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgprogress" --identifier "${identifier}" --progressbaridx "${progressbaridx}" --progress "${progressx}" & disown
      fi
   fi
   ## }}

   ## {{ --forceactive
   if [ "${forceactive}" = "1" ]; then
      touch -- "${msgcollector_run_dir}/${identifier}_forceactive"
   fi
   ## }}

   ## {{ --parenttty
   if [ -n "${parenttty}" ]; then
      printf '%s\n' "${parenttty}" > "${msgcollector_run_dir}/${identifier}_parenttty"
   fi
   ## }}

   ## {{ --progressbarx
   if [ "${progressbarx}" = "1" ]; then
      if ! msgcollector_check "${progressbaridx}"; then
         error "Variable 'progressbaridx' is empty or invalid."
         exit 4
      fi
      printf '%s\n' "${parentpid}" > "${msgcollector_run_dir}/${identifier}_${progressbaridx}_parentpid"
      message="$(sanitize-string -- nolimit "${message}")"
      write_to_file="${msgcollector_run_dir}/${identifier}_${progressbaridx}_progressbarx"
      append_to_file

      ## Cannot run from msgdispatcher due to user mismatch permission issues.
      "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgprogressbar" --identifier "${identifier}" --progressbaridx "${progressbaridx}" --progressbartitlex "${progressbartitlex}" --message "${message}" & disown
   fi
   ## }}
}

append_to_file() {
   trap "error_handler" ERR

   if [ "${onlyecho}" = "1" ]; then
      true "onlyecho"
      return 0
   fi

   if [ -n "${message}" ]; then
      ## variable $message exists
      if [ "${newlinecli}" = "1" ]; then
         printf '%s\n' "
${message}" >> "${write_to_file}"
      elif [ "${messagex}" = 1 ]; then
         if [ "${nonewlinex}" = "1" ]; then
            printf '%s\n' "${message}" >> "${write_to_file}"
         else
            printf '%s\n' "${message}
" >> "${write_to_file}"
         fi
      else
         printf '%s\n' "${message}" >> "${write_to_file}"
      fi
   fi

   ## {{ --done
   if [ "${done}" = "1" ]; then
      touch -- "${write_to_file}_done"
   fi
   ## }}
}

return_status() {
   trap "error_handler" ERR

   ## {{ --identifier
   if ! msgcollector_check "${identifier}"; then
      printf '%s\n' "${BASH_SOURCE[0]} ERROR: variable identifier is empty or invalid."
      exit 1
   fi
   ## }}

   ## {{ --messagex
   if [ "${messagex}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_messagex"
      return_status_file
   fi
   ## }}

   ## {{ --messagecli
   if [ "${messagecli}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_messagecli"
      return_status_file
   fi
   ## }}

   ## {{ --waitmessagecli
   if [ "${waitmessagecli}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_waitmessagecli"
      return_status_file
   fi
   ## }}

   ## {{ --passivepopupqueuex
   if [ "${passivepopupqueuex}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_passivepopupqueuex"
      return_status_file
   fi
   ## }}

   ## {{ --progressbarx
   if [ "${progressbarx}" = "1" ]; then
      if ! msgcollector_check "${progressbaridx}"; then
         error "Variable progressbaridx is empty or invalid."
         exit 4
      fi
      write_to_file="${msgcollector_run_dir}/${identifier}_${progressbaridx}_progressbarx"
      return_status_file
   fi
   ## }}

   ## {{ --progressbarxrunning
   if [ "${progressbarxrunning}" = "1" ]; then
      if [ -f "${msgcollector_run_dir}/${identifier}_${progressbaridx}_yadprogresspid" ]; then
         local pid
         ## || true to prevent race condition
         yad_progress_pid="$(cat -- "${msgcollector_run_dir}/${identifier}_${progressbaridx}_yadprogresspid")" || true
         if ! is_whole_number "${yad_progress_pid}"; then
            exit 1
         fi
         ## Check if still running.
         local ps__p_exit_code
         ps__p_exit_code="0"
         ps -p "${yad_progress_pid}" >/dev/null 2>/dev/null || { ps__p_exit_code="$?"; true; };
         if [ "${ps__p_exit_code}" = "0" ]; then
            exit 0
         else
            exit 1
         fi
      else
         exit 1
      fi
   fi
   ## }}

   ## {{ --progressbarxprogresstxtexisting
   if [ "${progressbarxprogresstxtexisting}" = "1" ]; then
      if [ -f "${msgcollector_run_dir}/${identifier}_${progressbaridx}_progresstxt" ]; then
         exit 0
      else
         exit 1
      fi
   fi
   ## }}

   ## {{ --typexstatus
   if [ "${typex_chosen}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_typex"
      if [ -f "${write_to_file}" ]; then
         local TYPE
         TYPE="$(cat -- "${write_to_file}")"
         printf '%s\n' "${TYPE}"
      fi
      exit 0
   fi
   ## }}

   ## {{ --typeclistatus
   if [ "${typecli_chosen}" = "1" ]; then
      write_to_file="${msgcollector_run_dir}/${identifier}_typecli"
      if [ -f "${write_to_file}" ]; then
         local TYPE
         TYPE="$(cat -- "${write_to_file}")"
         printf '%s\n' "${TYPE}"
      fi
      exit 0
   fi
   ## }}

   true "Fallback exit code, none of the above matched."
   exit 3
}

return_status_file() {
   trap "error_handler" ERR

   if [ -f "${write_to_file}" ]; then
      exit 0
   else
      exit 1
   fi
}

forget() {
   trap "error_handler" ERR

   ## & disown to prevent long waiting.
   ## Setting progress to 100, so any eventually still open progress bars get closed.
   ## Deactivated.
   ## Problematic if a new progress bar will be started quickly afterward.
   #/usr/libexec/msgcollector/msgprogress --identifier "$identifier" --progress "100" & disown

   local file_list=(
      "${msgcollector_run_dir}/${identifier}_icon"
      "${msgcollector_run_dir}/${identifier}_titlecli"
      "${msgcollector_run_dir}/${identifier}_titlex"
      "${msgcollector_run_dir}/${identifier}_passivepopupqueuextitle"
      "${msgcollector_run_dir}/${identifier}_passivepopupqueuextype"
      "${msgcollector_run_dir}/${identifier}_${progressbaridx}_progressbartitlex"
      "${msgcollector_run_dir}/${identifier}_typecli"
      "${msgcollector_run_dir}/${identifier}_typex"
      "${msgcollector_run_dir}/${identifier}_${progressbaridx}_progressbarx_done"
      "${msgcollector_run_dir}/${identifier}_${progressbaridx}_progressbarx"
      "${msgcollector_run_dir}/${identifier}_passivepopupqueuex_done"
      "${msgcollector_run_dir}/${identifier}_passivepopupqueuex"
      "${msgcollector_run_dir}/${identifier}_messagex_done"
      "${msgcollector_run_dir}/${identifier}_messagex"
      "${msgcollector_run_dir}/${identifier}_waitmessagecli_done"
      "${msgcollector_run_dir}/${identifier}_waitmessagecli"
      "${msgcollector_run_dir}/${identifier}_messagecli_done"
      "${msgcollector_run_dir}/${identifier}_messagecli"
      "${msgcollector_run_dir}/${identifier}_forceactive"
      "${msgcollector_run_dir}/${identifier}_parenttty"
   )

   ## Better not deleting
   ## "${msgcollector_run_dir}/${identifier}_${progressbaridx}_parentpid"
   ## "${msgcollector_run_dir}/${identifier}_${progressbaridx}_fifo"
   ## "${msgcollector_run_dir}/${identifier}_${progressbaridx}_progresstxt"
   ## "${msgcollector_run_dir}/${identifier}_${progressbaridx}_yadprogresspid"
   ## because that could confuse /usr/libexec/msgcollector/msgprogress.

   local file_name
   for file_name in "${file_list[@]}"; do
      if [ -e "${file_name}" ]; then
         safe-rm --force -- "${file_name}"
      fi
   done
   unset file_name
}

forgetwaitcli() {
   trap "error_handler" ERR

   local file_list=(
      "${msgcollector_run_dir}/${identifier}_waitmessagecli_done"
      "${msgcollector_run_dir}/${identifier}_waitmessagecli"
   )

   local file_name
   for file_name in "${file_list[@]}"; do
      if [ -e "${file_name}" ]; then
         safe-rm --force -- "${file_name}"
      fi
   done
   unset file_name
}

## Debugging.
#printf '%s\n' "${BASH_SOURCE[0]} $@
#" >> "/home/user/msgcollector"

main() {
   true "$0: START"
   trap "error_handler" ERR
   my_command_line_parameters="$*"

   ## Before parse: parse_cmd_options' trace reads ${cyan}/${reset}.
   colors

   parse_cmd_options "$@"

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

   ## provides: msgcollector_check, is_whole_number
   source "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/check"

   if [ "${forget}" = "1" ]; then
      forget
      exit 0
   fi

   if [ "${forgetwaitcli}" = "1" ]; then
      forgetwaitcli
      exit 0
   fi

   preparation

   if [ "${status}" = "1" ]; then
      return_status
   else
      collector
   fi

   true "$0: END"
}

if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
   main "$@"
fi
