#!/bin/sh

po_domain="alterator-control++"
alterator_api_version=1

. alterator-sh-functions
. shell-config

CONTROLPP="${CONTROLPP:-/usr/bin/control++}"
CONTROLPP_ROOT="${CONTROLPP_ROOT:-/var/lib/alterator-control++}"
CONTROLPP_DBFILE="${CONTROLPP_DBFILE:-$CONTROLPP_ROOT/lists.db}"
CONTROLPP_LISTS_DIR="${CONTROLPP_LISTS_DIR:-$CONTROLPP_ROOT/lists}"
CONTROLPP_PERMS_DIR="${CONTROLPP_PERMS_DIR:-/etc/control++/permissions}"
CONTROLPP_PAGE_LIMIT=25
CONTROLPP_CONFIG="${CONTROLPP_CONFIG:-/etc/control++/control++.conf}"
CONTROLPP_MODULE_CONFDIR="${CONTROLPP_MODULE_CONFDIR:-/etc/alterator-control++}"
CONTROLPP_MODULE_CONFIG="${CONTROLPP_MODULE_CONFIG:-$CONTROLPP_MODULE_CONFDIR/controlpp.conf}"
CONTROLPP_MODULE_WARNLIST="${CONTROLPP_MODULE_WARNLIST:-$CONTROLPP_MODULE_CONFDIR/warnlist}"

log()
{
  echo "DEBUG: $1" >&2
}

check_macro_mode()
{
  local mmode="$1"
  while read -r mode || [[ -n $mode ]]; do
    if [[ "$mmode" == "$mode" ]]; then
      return 1
    fi
  done <<< "$(get_macro_modes)"
  return 0
}

check_permissions_mode()
{
  local pmode="$1"
  while read -r mode || [[ -n $mode ]]; do
    if [[ "$pmode" == "$mode" ]]; then
      return 1
    fi
  done <<< "$(get_permissions_modes)"
  return 0
}

init()
{
  local wmacro="$1"
  local wperms="$2"
  local bmacro="$3"
  local bperms="$4"
  # TODO check if control++ initialized

  log "Initializing module..."
  mkdir -p "$CONTROLPP_MODULE_CONFDIR"
  touch "$CONTROLPP_MODULE_CONFIG"
  touch "$CONTROLPP_DBFILE"
  mkdir -p "$CONTROLPP_LISTS_DIR"
  touch "$CONTROLPP_LISTS_DIR/empty"

  # create perms configs
  cat > "$CONTROLPP_PERMS_DIR/$wperms" << EOL
[whitelist]
path="${CONTROLPP_LISTS_DIR}/empty"
base_dir="/"
excluded_paths="boot", "dev", "etc", "lost+found", "media", "mnt", "proc", "run", "selinux", "srv", "sys", "var"
mode_for_dirs=*********
EOL

  cat > "$CONTROLPP_PERMS_DIR/$bperms" << EOL
[blacklist]
path="${CONTROLPP_LISTS_DIR}/empty"
base_dir="/"
EOL

  shell_config_set "$CONTROLPP_MODULE_CONFIG" "wmacro" "$wmacro"
  shell_config_set "$CONTROLPP_MODULE_CONFIG" "wperms" "$wperms"
  shell_config_set "$CONTROLPP_MODULE_CONFIG" "bmacro" "$bmacro"
  shell_config_set "$CONTROLPP_MODULE_CONFIG" "bperms" "$bperms"

  printf "\n\n[%s]\npermissions=%s\n" "$wmacro" "$wperms" >> "$CONTROLPP_CONFIG"
  printf "\n[%s]\npermissions=%s\n" "$bmacro" "$bperms" >> "$CONTROLPP_CONFIG"

  log "Module initialized"
}

check_status()
{
  local cfg="$CONTROLPP_MODULE_CONFIG"
  local res=0
  
  log "Checking status..."
  
  if [ -f "$CONTROLPP_MODULE_CONFIG" ]; then
    log "Found $CONTROLPP_MODULE_CONFIG"
    
    local wmacro="$(shell_config_get "$cfg" "wmacro")"
    local bmacro="$(shell_config_get "$cfg" "bmacro")"

    local wperms="$(shell_config_get "$cfg" "wperms")"
    local bperms="$(shell_config_get "$cfg" "bperms")"

    # if macro mode not found set error
    macro_modes=("$wmacro" "$bmacro")
    for mode in "${macro_modes[@]}"; do
      if check_macro_mode "$mode" ; then
        res=1
      fi
    done

    # if permissions mode not found set error
    pmodes=("$wperms" "$bperms")
    for mode in "${pmodes[@]}"; do
      if check_permissions_mode "$mode" ; then
        res=1
      fi
    done
  else
    res=1
  fi

  [ $res != 1 ] &&
      write_bool_param is_active true ||
      write_bool_param is_active false
}

enable_list()
{
  local id="$1"
  local force="$2"
  # local ltype="$(typech2name "$(id2ltype "$id")")"
  local ltype="$(id2ltype "$id")"
  local ldir="$CONTROLPP_LISTS_DIR"
  local permsmode="$(shell_config_get "$CONTROLPP_MODULE_CONFIG" "bperms")"
  local macromode="$(shell_config_get "$CONTROLPP_MODULE_CONFIG" "bmacro")"

  # TODO handle unknown ltype
  if [ "$ltype" == "w" ]; then
    permsmode="$(shell_config_get "$CONTROLPP_MODULE_CONFIG" "wperms")"
    macromode="$(shell_config_get "$CONTROLPP_MODULE_CONFIG" "wmacro")"
  fi

  local permsfile="$CONTROLPP_PERMS_DIR/$permsmode"

  # sed -i "s/^path.*$/path=\"${ldir//\//\\/}\/${id}\"/" "$permsfile"
  shell_config_set "$permsfile" path "\"$ldir/$id\""

  # activate the list
  "$CONTROLPP" "$macromode" -p -f
}

reset()
{
  # local al="$(get_active_list)"
  # if [ -n "$al" ]; then
    "$CONTROLPP" reset -p -f
  # fi
}

get_macro_modes()
{
  printf "$("$CONTROLPP" list -p -f | sed  -n '/Available macro modes:/,/Available ulimits modes:/p' | sed 's/ \[current\]//g' | head -n-1 | tail -n+2)"
}

get_permissions_modes()
{
  printf "$("$CONTROLPP" list -p -f | sed  -n '/Available permissions modes:/,/Available scripts modes:/p' | sed 's/ \[current\]//g' | head -n-1 | tail -n+2)"
}

get_active_permissions_mode()
{
  printf "$("$CONTROLPP" list -p -f | sed -n '/Available permissions modes:/,/Available scripts modes:/p' | grep "\[current\]" | sed 's/ \[current\]//g')"
  # printf "$("$CONTROLPP" status -p -f | grep 'permissions:' | grep -o "'.*'" | sed "s/'//g")"
}

get_active_list()
{
  local active_mode="$(get_active_permissions_mode)"
  local active_list=""
  if [ -n "$active_mode" ]; then
    active_list="$(cat "$CONTROLPP_PERMS_DIR/$active_mode" | grep '^path' | sed -n 's/.*"\(.*\)"/\1/p')"
  else
    active_list=""
  fi
  printf "$active_list"
}

is_active_list()
{
  local lpath="$CONTROLPP_LISTS_DIR/$1"
  local al="$(get_active_list)"
  if [ "$lpath" == "$al" ]; then
    return 1
  else
    return 0
  fi
}

is_dangerous_list()
{
  local id="$1"
  local listpath="$CONTROLPP_LISTS_DIR/$id"
  local warnlistpath="$CONTROLPP_MODULE_WARNLIST"
  local ltype="$(id2ltype "$id")"
  local danger=1

  if [ "$ltype" == "w" ]; then
    while read -r path || [[ -n $path ]]; do
      if ! grep -qe "^${path}$" "$listpath" ; then
        log "not in white list: $path"
        echo "$path"  # to stdout
        danger=0
      fi
    done <<< $(cat "$warnlistpath")
  else
    while read -r path || [[ -n $path ]]; do
      # grep prints matched paths to stdout
      if grep -oe "^${path}$" "$listpath" ; then
        danger=0
      fi
    done <<< $(cat "$warnlistpath")
  fi

  # 0 - danger, 1 - not
  return "$danger"
}

id2name()
{
  grep -E "^$1\s" "$CONTROLPP_DBFILE" | cut -f3
}

id2ltype()
{
  grep -E "^$1\s" "$CONTROLPP_DBFILE" | cut -f2
}

dbinsert()
{
  local type="$1"
  local name="$2"

  local nextid="$(nextid)"
  printf "$nextid\t$type\t$name\n" >> "$CONTROLPP_DBFILE"
  printf "$nextid"
}

dbupdate()
{
  local id="$1"
  local type="$2"
  local summary="$3"

  sed -i.bak "s/^$id.*$/$id\t$2\t${summary//\//\\/}/" "$CONTROLPP_DBFILE"
}

nextid()
{
  maxid="$(sort -rnk1 "$CONTROLPP_DBFILE" | head -n1 | cut -f1)"
  # maxid="$(./max.sh)"
  nextid=$(($maxid + 1))
  echo "$nextid"
}

dbremove()
{
  local id="$1"
  line="$(grep -nE "^$id\s" "$CONTROLPP_DBFILE" | cut -f1 -d:)"
  sed -i.bak "${line}d" "$CONTROLPP_DBFILE"
}

typech2name()
{
  local typechar="$1"
  
  case "$typechar" in
  w)
    echo "$(_ "whitelist")"
    ;;
  b)
    echo "$(_ "blacklist")"
    ;;
  *)
    echo "$(_ "unknown_list_type")"
    ;;
  esac
}

name2typech()
{
  local ltname="$1"
  case "$ltname" in
  whitelist)
    echo "w"
    ;;
  blacklist)
    echo "b"
    ;;
  *)
    echo "_"  # unknown
    ;;
  esac
}

list_lists()
{
  local active_list="$(get_active_list)"
  cat "$CONTROLPP_DBFILE" | sort -k3 | \
  while IFS=$'\t' read -r id typechar name; do
    [ "$CONTROLPP_LISTS_DIR/$id" == "$active_list" ] &&
      status="$(_ "active")" || status="$(_ "inactive")"
    write_table_item \
      name "$id" \
      summary "$name" \
      ltype "$(typech2name "$typechar")" \
      status "$status"
  done
}

list_paths()
{
  local id="$1"
  local offset="$(($2 + 1))"
  local filter="$3"
  local limit=$CONTROLPP_PAGE_LIMIT-1  # -1 for sed

  local status=""

  sort "$CONTROLPP_LISTS_DIR/$id" | grep "$filter" |\
  sed -n "${offset},$(($offset+$limit))p" | while read -r path; do
    status="$(_ "on")"
    if [ "$path" == "" ]; then
      continue
    elif  [[ "$path" =~ ^//.*$ ]]; then
      path="$(trim "${path:2}")"
      status="$(_ "off")"
    fi

    write_table_item name "$path" path "$path" status "$status"
  done
}

trim()
{
  local var="$*"
  # remove leading whitespace characters
  var="${var#"${var%%[![:space:]]*}"}"
  # remove trailing whitespace characters
  var="${var%"${var##*[![:space:]]}"}"   
  echo -n "$var"
}

page_count()
{
  local id="$1"
  local filter="$2"
  local lines="$(cat "$CONTROLPP_LISTS_DIR/$id" | grep "$filter" | wc -l | cut -d " " -f 1)"
  local lim="$CONTROLPP_PAGE_LIMIT"

  echo "$(( ($lines + $lim - 1)/$lim ))"
}

read_list()
{
  local id="$1"
  local filter="$2"

  write_string_param id "$id"
  write_string_param summary "$(id2name "$id")"
  $(is_active_list "$id")
  [ $? -eq 1 ] &&
    write_bool_param active true ||
    write_bool_param active false
  write_string_param ltype "$(typech2name "$(id2ltype "$id")")"
  write_string_param pages "$(page_count "$id" "$filter")"
  
  local warnlist=$(is_dangerous_list "$id") res="$?"
  [ $res -eq 0 ] &&
    write_bool_param danger true ||
    write_bool_param danger false
  write_string_param warnlist "$warnlist"
}

create_empty_file()
{
  local fpath="$1"
  if [ -f "$fpath" ]; then
    truncate -s 0 "$fpath"
  else
    touch "$fpath"
  fi
}

create_black_list()
{
  local summary="$1"
  local id="$(dbinsert b "$summary")"

  create_empty_file "$CONTROLPP_LISTS_DIR/$id"
  write_string_param id "$id"

  log "Black list $id created"
}

create_white_list()
{
  local summary="$1"
  local autogen="$2"
  local id="$(dbinsert w "$summary")"

  if [ "$autogen" == "on" ]; then
    log "Generating white list..."
    find / -type f -executable  > "$CONTROLPP_LISTS_DIR/$id"
  else
    create_empty_file "$CONTROLPP_LISTS_DIR/$id"
  fi

  write_string_param id "$id"

  log "White list $id created"
}

update_list()
{
  local id="$1"
  local summary="$2"
  local ltype="$(id2ltype "$id")"
  
  dbupdate "$id" "$ltype" "$summary"
  write_string_param id "$id"

  log "List $id updated"
}

append_path()
{
  local id="$1"
  local path="$2"

  echo "$path" >> "$CONTROLPP_LISTS_DIR/$id"
  log "appended path $path"
}

remove_path()
{
  local id="$1"
  local paths="$2"

  cp -f "$CONTROLPP_LISTS_DIR/$id" "$CONTROLPP_LISTS_DIR/$id.bak"
  IFS=';' read -ra PATHS <<< "$paths";
  for path in "${PATHS[@]}"; do
    local p="${path//\//\\/}"
    sed -i "/^\(\/\/ \?\)\?$p$/d" "$CONTROLPP_LISTS_DIR/$id"
    log "removed $path"
  done
}

enable_path()
{
  local id="$1"
  local paths="$2"

  cp -f "$CONTROLPP_LISTS_DIR/$id" "$CONTROLPP_LISTS_DIR/$id.bak"
  IFS=';' read -ra PATHS <<< "$paths";
  for path in "${PATHS[@]}"; do
    local p="${path//\//\\/}"
    sed -i "s/^\(\/\/ \?\)\?$p$/$p/" "$CONTROLPP_LISTS_DIR/$id"
    log "enabled $path"
  done
}

disable_path()
{
  local id="$1"
  local paths="$2"

  cp -f "$CONTROLPP_LISTS_DIR/$id" "$CONTROLPP_LISTS_DIR/$id.bak"
  IFS=';' read -ra PATHS <<< "$paths";
  for path in "${PATHS[@]}"; do
    local p="${path//\//\\/}"
    sed -i "s/^\(\/\/ \?\)\?$p$/\/\/ $p/" "$CONTROLPP_LISTS_DIR/$id"
    log "disabled $path"
  done
}

validate_modename()
{
  local name="$1"
  echo "$name" | grep -qE ^[A-Za-z_0-9]+$
}

on_message()
{
  # TODO validate in_params
  case "$in_action" in
  read)
    case "$in__objects" in
    status)
      check_status
      ;;
    *)
      read_list "$in_id" "$in_filter"
      ;;
    esac
    ;;
  init)
    modes=("$in_wmacro" "$in_wperms" "$in_bmacro" "$in_bperms")
    for mode in "${modes[@]}"; do
      validate_modename "$mode"
      if [ $? -eq 1 ]; then
        write_error "$(_ "Invalid mode name"): $mode"
        return
      fi
    done

    macro_modes=("$in_wmacro" "$in_bmacro")
    for mode in "${macro_modes[@]}"; do
      if ! check_macro_mode "$mode" ; then
        write_error "$(_ "Macro mode already exists"): $mode"
        return
      fi
    done

    permissions_modes=("$in_wperms" "$in_bperms")
    for mode in "${permissions_modes[@]}"; do
      if ! check_permissions_mode "$mode" ; then
        write_error "$(_ "Permissions mode already exists"): $mode"
        return
      fi
    done

    init "$in_wmacro" "$in_wperms" "$in_bmacro" "$in_bperms"
    ;;
  create)
    case "$in_ltype" in
    whitelist)
      create_white_list "$in_summary" "$in_autogen"
      ;;
    blacklist)
      create_black_list "$in_summary"
      ;;
    *)
      write_error "$(_ "Unknown list type")"
      ;;
    esac
    ;;
  write)
    if [ "$in_id" == "#f" -o -z "$in_id" ]; then
      write_error "$(_ "The list is required")"
      return
    fi
    update_list "$in_id" "$in_summary"
    ;;
  append_path)
    if ! pathchk "$in_path" 2>/dev/null; then
      write_error "$(_ "Invalid path")"
      return
    fi
    append_path "$in_id" "$in_path"
    ;;
  remove_path)
    remove_path "$in_id" "$in_path"
    ;;
  enable_path)
    enable_path "$in_id" "$in_path"
    ;;
  disable_path)
    disable_path "$in_id" "$in_path"
    ;;
  list)
    case "$in__objects" in
    lists)
      list_lists
      ;;
    *)
      if [ "$in_id" == "#f" -o -z "$in_id" ]; then
        write_error "$(_ "The list is required")"
        return
      fi
      # TODO validate in_offset
      # TODO validate in_filter
      list_paths "$in_id" "$in_offset" "$in_filter"
      ;;
    esac
    ;;
  apply)
    if [ "$in_id" == "#f" -o -z "$in_id" ]; then
      write_error "$(_ "The list is required")"
      return
    fi
    
    is_active_list "$in_id"
    if [ $? -eq 1 ]; then
      write_error "$(_ "The list is already active")"
      return
    fi

    if ! enable_list "$in_id" 0; then
      write_error "$(_ "Error occured")"
      return
    fi
    ;;
  remove)
    if [ "$in_id" == "#f" -o -z "$in_id" ]; then
      write_error "$(_ "The list is required")"
      return
    fi
    dbremove "$in_id"
    ;;
  reset)
    if [ "$in_id" == "#f" -o -z "$in_id" ]; then
      write_error "$(_ "The list is required")"
      return
    fi
    
    is_active_list "$in_id"
    if [ $? -eq 0 ]; then
      write_error "$(_ "The list is not active")"
      return
    fi

    reset
    ;;
  *)
    ;;
  esac
}

message_loop
