#!/bin/sh

# Turn of auto expansion
set -f

po_domain="alterator-ports-access"
alterator_api_version=1

. alterator-sh-functions
. shell-config
. shell-quote
. shell-var
. alterator-ports-access-lib.sh
. alterator-service-functions

readonly USB_IDS="/usr/share/misc/usb.ids"

# APPLY CHANGES
apply_changes() {
  echo "[backend-portsctrl] apply_changes." 1>&2
  alterator-ports-access -u -s
}

extract_pre_access() {
    local desc="${1%;}"

    case "$desc" in
        *\;[*])
            echo "${desc%;*}"
            ;;
    esac
}

extract_access() {
    local desc="${1%;}"
    local access=

    case "$desc" in
        *\;[*])
            access="${desc##*;[}"
            echo "${access%]}"
            ;;
    esac
}

yesno_label() {
    if shell_var_is_yes "$1"; then
        echo "`_ 'Yes'`"
    else
        echo "`_ 'No'`"
    fi
}

access_label() {
    echo "${1:-`_ 'Default'`}"
}

# LIST CONFIGURED SERIAL PORTS
_list_serial() {
    printf "(\"%s\" label_serial_port \"%s\" label_serial_enabled \"%s\" label_serial_access \"%s\")\n" "$1" "$1" "$(yesno_label "$2")" "$(access_label "$3")" >&3
}
list_serial() {
    portsctrl_iterate_serial _list_serial
}

# LIST USB WHITE LIST
_list_usb_rule() {
    printf "(\"%s\" label_usb_rule_number \"%s\" label_usb_vendor \"%s\" label_usb_prodid \"%s\" label_usb_serial \"%s\" label_usb_info \"%s\" label_usb_access \"%s\")\n" "$1" "$1" "$2" "$3" "$4" "$5" "$(access_label "$6")" >&3
}
list_usb_rules() {
    portsctrl_iterate_usb _list_usb_rule
}

find_usb_dir() {
    local vendor_id="$1"
    local product_id="${2:-}"
    local serial="${3:-}"

    for i in $(find /sys -name authorized); do
        local dir="$(dirname "$i")"
        local devclass="$(cat "$dir/bDeviceClass")"

        # Ignore USB hubs
        [ "$devclass" = "09" ] && continue
        [ -z "$devclass" ] && continue

        [ "$(cat "$dir/idVendor")" = "$vendor_id" ] || continue
        [ -z "$product_id" -o "$(cat "$dir/idProduct")" = "$product_id" ] || continue
        [ -z "$serial" -o "$(cat "$dir/serial")" = "$serial" ] || continue

        [ -e "$dir/manufacturer" ] || continue

        echo "$dir"
        break
    done
}

get_usb_manufacturer() {
    if [ -r "$USB_IDS" ]; then
        local manufacturer="$(sed -n -e "/^$1/ s/....[[:space:]]\+//p" "$USB_IDS")"
        if [ -z "$manufacturer" ]; then
            local dir="$(find_usb_dir "$1")"
            if [ -n "$dir" ]; then
                cat "$dir/manufacturer" 2>/dev/null
            fi
        else
            echo "$manufacturer"
        fi
    fi
}

get_usb_product_name() {
    if [ -r "$USB_IDS" ]; then
        local prodname="$(sed -n -e "/^$1/,/^[^[:space:]]/{ s/^[[:space:]]\+$2[[:space:]]\+//p }" "$USB_IDS")"
        if [ -z "$prodname" ]; then
            local dir="$(find_usb_dir "$1" "$2")"
            if [ -n "$dir" ]; then
                cat "$dir/product" 2>/dev/null
            fi
        else
            echo "$prodname"
        fi
    fi
}

# LIST PRESENT USB DEVICES
list_prsnt_devices() {
    # Force scan, temporary disable USB control.
    if shell_var_is_yes "$in_force"; then
        echo "[backend-portsctrl] Temporary disable USB control." 1>&2
        alterator-ports-access -e
    fi

    for i in $(find /sys -name authorized); do
        local dir="$(dirname "$i")"
        local devclass="$(cat "$dir/bDeviceClass")"

        # Ignore USB hubs
        [ "$devclass" = "09" ] && continue
        [ -z "$devclass" ] && continue

        local idProduct="$(cat "$dir/idProduct")"
        local idVendor="$(cat "$dir/idVendor")"
        local serial="$(cat "$dir/serial" 2>/dev/null)"

        [ -z "$devclass" ] && continue
        [ -z "$idVendor" ] && continue

        if [ -n "$serial" ]; then
            serial_msg="$serial"
        else
            serial_msg="`_ 'None'`"
        fi

        local manufacturer="$(get_usb_manufacturer "$idVendor")"
        local product="$(get_usb_product_name "$idVendor" "$idProduct")"

        [ -n "$manufacturer" -a -z "${manufacturer##*unauthorized*}" ] && continue
        [ -n "$product" -a -z "${product##*unauthorized*}" ] && continue

        printf "(\"%s\" label_prsnt_usb_vendor \"%s\" label_prsnt_usb_product \"%s\" label_prsnt_usb_idvendor \"%s\" label_prsnt_usb_idproduct \"%s\" label_prsnt_usb_serial \"%s\")\n" \
               "$idVendor:$idProduct:$serial" "$manufacturer" "$product" "$idVendor" "$idProduct" "$serial_msg" >&3
    done

    # Renew USB control.
    if shell_var_is_yes "$in_force"; then
        echo "[backend-portsctrl] renew USB control" 1>&2
        alterator-ports-access -u
    fi
}

# READ SERIAL PORT STATUS
_print_serial_access() {
    write_string_param "input_serial_owner" "$1"
    write_string_param "input_serial_group" "$2"
    write_string_param "list_serial_mode" "${3:-default}"
}
_print_serial() {
    if [ "$1" = "$4" ]; then
        write_string_param "label_serial_selected" "$1"
        write_string_param "list_serial_enabled" "$2"
        portsctrl_parse_access _print_serial_access "$3"
    fi
}
print_serial() {
    if [ -z "$in_port" ]; then
        write_error "Serial port name isn't specified."
        return 1
    fi

    # Check config file exists:
    if ! portsctrl_config_exists; then
        _print_serial "$in_port" 'yes' '' "$in_port"
        return
    fi

    portsctrl_iterate_serial _print_serial "$in_port"
}

# READ USB DEVICE STATUS
_print_usb_access() {
    write_string_param "input_usb_owner" "$1"
    write_string_param "input_usb_group" "$2"
    write_string_param "list_usb_mode" "${3:-default}"
}
_print_usb() {
    if [ "$1" = "$7" ]; then
        write_string_param "input_usb_vendor" "$2"
        write_string_param "input_usb_productid" "$3"
        write_string_param "input_usb_serial" "$4"
        write_string_param "input_usb_info" "$5"
        portsctrl_parse_access _print_usb_access "$6"
    fi
}
print_usb() {
    if [ -z "$in_num" ]; then
        write_error "USB rule number isn't specified."
        return 1
    fi

    # Check config file exists:
    if ! portsctrl_config_exists; then
        _print_usb "$in_num" '' '' '' '' '' "$in_num"
        return
    fi

    portsctrl_iterate_usb _print_usb "$in_num"
}

# READ CONTROL STATUS FOR USB & SERIAL
print_status () {
    # Check config file exists:
    if ! portsctrl_config_exists; then
        write_bool_param "serial_ctrl_on" 0
        write_bool_param "usb_ctrl_on" 0
        return
    fi

    # Create subshell & include config file.
    (
        . "$CONFIG"

        if shell_var_is_yes "$SERIAL_CONTROL"; then
            write_bool_param "serial_ctrl_on" 1
        else
            write_bool_param "serial_ctrl_on" 0
        fi

        if shell_var_is_yes "$USB_CONTROL"; then
            write_bool_param "usb_ctrl_on" 1
        else
            write_bool_param "usb_ctrl_on" 0
        fi

        if shell_var_is_yes "$USB_ALLOW_HID"; then
            write_bool_param "usb_hid" 1
        else
            write_bool_param "usb_hid" 0
        fi
    )
}

# CHECK CONFIG EVAILABLE
assert_config () {
    if ! portsctrl_config_exists; then
        # Create config:
        if ! touch "$CONFIG"; then
            write_error "Can't create config file: ($CONFIG)"
            return 1
        else
            echo "[backend-portsctrl] new config file created ($CONFIG)" 1>&2
        fi
    fi
}

# TURN ON/OFF CONTROL
control_ports() {
    # Validate invocation.
    if [ -z "$in_subsys" -o -z "$in_enabled" ]; then
        write_error "Subsystem and the control flag should be specified."
        return 1
    fi

    local status=
    if [ "$in_enabled" = "#t" ]; then
        status="yes"
    else
        status="no"
    fi

    # Check config file exists:
    assert_config || return $?

    # Update config
    local subsys="$(echo "$in_subsys" | tr '[:lower:]' '[:upper:]')"
    shell_config_set "$CONFIG" "${subsys}_CONTROL" "\"$(quote_shell "$status")\""

    # Apply changes:
    apply_changes

    # Enforce changes in udisks2:
    service_control 'udevd' stop
    service_control 'udisks2' restart
    service_control 'udevd' start
}


# NEW SERIAL PORT PARAMETERS
write_serial_options() {
    # Validate the invocation:
    if [ -z "$in_port" -o -z "$in_enabled" ]; then
        write_error "Port and the control flag should be specified."
        return 1
    fi

    # Check config file exists:
    assert_config || return $?

    # Create subshell & include config file
    (
        . "$CONFIG"

        # Remove any occurrence of the port:
        shell_config_set "$CONFIG" 'SERIAL_DISABLED' "($(for i in "${SERIAL_DISABLED[@]}"; do [ "$in_port" = "${i%%;*}" ] && continue; echo -n "\"$(quote_shell "$i")\" "; done))"
        shell_config_set "$CONFIG" 'SERIAL_ENABLED' "($(for i in "${SERIAL_ENABLED[@]}"; do [ "$in_port" = "${i%%;*}" ] && continue; echo -n "\"$(quote_shell "$i")\" "; done))"

        # Re-read config:
        . "$CONFIG"

        desc="$in_port"
        if [ -n "$in_owner" -o -n "$in_group" -o -n "$in_mode" ]; then
            desc="$desc;[$in_owner:$in_group:$in_mode]"
        fi

        # Add port to the list:
        if shell_var_is_yes "$in_enabled"; then
            shell_config_set "$CONFIG" 'SERIAL_ENABLED' "($(for i in "${SERIAL_ENABLED[@]}" "$desc"; do echo -n "\"$(quote_shell "$i")\" "; done))"
            echo "Enabled access to serial port(s) $desc." | logger -p user.warning
        elif shell_var_is_no "$in_enabled"; then
            shell_config_set "$CONFIG" 'SERIAL_DISABLED' "($(for i in "${SERIAL_DISABLED[@]}" "$desc"; do echo -n "\"$(quote_shell "$i")\" "; done))"
            echo "Disabled access to serial port(s) $desc." | logger -p user.warning
        fi
    )

    # Apply the changes:
    apply_changes
}


# REMOVE ENTRY FROM WHITE LIST
_usb_rm_rule() {
    local num="$1"

    portsctrl_config_exists || return

    # Create subshell & include the config file:
    (
        . "$CONFIG"

        USB_WHITE_LIST[$num]=
        shell_config_set "$CONFIG" 'USB_WHITE_LIST' "($(for i in "${USB_WHITE_LIST[@]}"; do test -z "$i" && continue; echo -n "\"$(quote_shell "$i")\" "; done))"
    )
}
usb_rm_rule() {
    # Validate the invocation:
    if [ -z "$in_rule_num" ]; then
        write_error "Rule number should be specified."
        return 1
    fi

    # Check config file exists:
    assert_config || return $?

    # Remove the rule:
    _usb_rm_rule "$in_rule_num"

    # Apply the changes:
    apply_changes
}


# ADD NEW ENTRY TO WHITE LIST
_usb_compare_rule() {
    if [ "$2" = "${7:-}" -a "$3" = "${8:-}" -a "$4" = "${9:-}" ]
    then
        return 1
    fi
}
_usb_rm_matching_rule() {
    if ! _usb_compare_rule "$@"; then
        _usb_rm_rule "$1"
        return 1
    fi
}
usb_save_rule() {
    # Validate the invocation:
    if [ -z "$in_vendor" -a -z "$in_productid" -a -z "$in_serial" ]; then
        write_error "Vendor, product IDs or a serial should be specified."
        return 1
    fi

    # Check config file exists:
    assert_config || return $?

    # Check if the rule already exists:
    while ! portsctrl_iterate_usb _usb_rm_matching_rule "$in_vendor" "$in_productid" "$in_serial"; do :; done

    # Create subshell & include the config file:
    (
        . "$CONFIG"

        if [ "$in_mode" = 'default' ]; then
            in_mode=
        fi

        rule="$in_vendor;$in_productid;$in_serial;$in_info"
        if [ -n "$in_owner" -o -n "$in_group" -o -n "$in_mode" ]; then
            rule="$rule;[$in_owner:$in_group:$in_mode]"
        fi

        shell_config_set "$CONFIG" 'USB_WHITE_LIST' "($(for i in "${USB_WHITE_LIST[@]}" "$rule"; do echo -n "\"$(quote_shell "$i")\" "; done))"

        echo "(Re)Configured USB port: $rule." | logger -p user.warning
    )

    # Apply the changes:
    apply_changes
}


# Control USB HID devices
usb_hid() {
    # Check config file exists:
    assert_config || return $?

    # Update the config:
    if test_bool "$in_allow_hid"; then
        allow='yes'
    else
        allow='no'
    fi

    shell_config_set "$CONFIG" "USB_ALLOW_HID" "\"$allow\""

    # Apply the changes:
    apply_changes
}


# Add present USB device to whitelist
usb_add_prsnt() {
    local device_id="$in_device_id"
    local vendor="${device_id%%:*}"; device_id="${device_id#*:}"
    local product="${device_id%%:*}"; device_id="${device_id#*:}"
    local serial="$device_id"
    local info="$(get_usb_product_name "$vendor" "$product")"

    in_vendor="$vendor" in_productid="$product" in_serial="$serial" \
        in_info="$info" usb_save_rule
}


# MAIN LOOP
on_message() {
  case "$in_action" in

    read)

      case "$in__objects" in
        serial_port)
          # READ SERIAL PORT STATUS
          echo "[backend-portsctrl] Read serial port settings." >&2
          print_serial
          ;;

        usb_device)
          # READ USB DEVICE STATUS
          echo "[backend-portsctrl] Read USB device settings." >&2
          print_usb
          ;;

        status)
          # READ CONTROL STATUS FOR USB & SERIAL
          echo "[backend-portsctrl] Read the status." >&2
          print_status
          ;;

        *)
          # UNDEFINED READ REQUEST
          write_error "Undefined read request: $in__objects." >&2
          ;;
      esac
      ;;

    write)

      case "$in__objects" in
        ctrl)
          # TURN ON/OFF CONTROL
          echo "[backend-portsctrl] Erite ports control status." >&2
          control_ports
          ;;

        serial)
          # NEW SERIAL PORT PARAMETERS
          echo "[backend-portsctrl] Write down serial port options." >&2
          write_serial_options
          ;;

        usb_rm)
          # REMOVE ENTRY FROM WHITE LIST
          echo "[backend-portsctrl] Remove rule from the whitelist." >&2
          usb_rm_rule
          ;;

        usb_save)
          # ADD NEW ENTRY TO WHITE LIST
          echo "[backend-portsctrl] Add/save USB whitelist rule." >&2
          usb_save_rule
          ;;

        usb_add_prsnt)
          # ADD PRESENT USB DEVICE TO WHITE LIST
          echo "[backend-portsctrl] Add present USB device to the whitelist." >&2
          usb_add_prsnt
          ;;

        usb_hid)
          # CONTROL USB HID
          echo "[backend-portsctrl] Save USB HID status." >&2
          usb_hid
          ;;

        *)
          # UNDEFINED WRITE REQUEST
          write_error "Undefined write request: $in__objects." >&2
          ;;

      esac
      ;;

    list)

      case "$in__objects" in

        list_serial)
          # LIST SERIAL PORTS
          echo "[backend-portsctrl] List serial ports." >&2
          list_serial
          ;;

        list_usb_rules)
          # LIST USB WHITE LIST
          echo "[backend-portsctrl] List USB whitelist rules." >&2
          list_usb_rules
          ;;

        list_prsnt_devices)
          # LIST PRESENT USB DEVICES
          echo "[backend-portsctrl] List present devices." >&2
          list_prsnt_devices
          ;;

        *)
          # UNDEFINED LIST REQUEST
          write_error "Undefined list request: $in__objects." >&2
          ;;

      esac
      ;;

    *)
      # UNDEFINED ACTION
      write_error "Undefined list request: $in_action." >&2
      ;;
  esac
}

message_loop

# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=sh
