Fifth menu migrated onto lib/menu.sh + lib/config.sh: menus/lockout.sh covers enable/disable, changing the password, inactivity timeout, daily lock time, and boot password. The password is SHA-256 hashed before it's ever assigned to LOCKOUT_PASSWORD (matching main.js's comparison logic) - verified by test that the stored value is the correct hash and never plaintext. Rewrote the legacy configure_password_protection's linear "ask everything, confirm save at the end" wizard as the same immediate-save pattern used by every other migrated menu: each action (change password, change timeout, toggle boot password, ...) is a complete, standalone change, consistent with Sites/Display/Timezone/Hidden PIN. LOCKOUT_ACTIVE_START/END are deliberately left untouched - per the Readme they're inert leftover fields the app ignores, so lib/config.sh just carries whatever is already in config.json through unchanged. Testing this menu surfaced a real gap before it ever shipped: lib/menu.sh never had ask_time/validate_time at all (only validate_integer/ask_integer, ask_url, etc were ported when the framework was first built) - "set a daily lock time" would have failed for every single user with "ask_time: command not found". Ported both from the legacy script. Also promoted the ON/OFF toggle-label helper (previously private to menus/display.sh as display_onoff) to a shared onoff() in lib/menu.sh, since menus/lockout.sh needed the same thing and menu files should only ever depend on lib/, never on each other. Bumped SCRIPT_VERSION to 2.2.0 with matching changelog entries in the script header and Readme, and updated "Modular Management" to list the new menu and drop Password Protection & Lockout from the "not yet migrated" list. Verified: - Full regression: re-ran the Sites, Display, Timezone/PIN scratch-config suites after every change in this round (the onoff refactor, and again after adding ask_time) - all still clean. - New scratch-config test for lockout.sh: enable (password+timeout+daily lock+boot toggle), independently recomputed the expected SHA-256 hash and confirmed it matches config.json exactly, change password, change timeout, clear daily lock, toggle boot password, disable (confirmed every field clears), and that the menu builder's options correctly differ between the enabled and disabled states. - End-to-end: ran the real install.sh as a genuine non-root, non-"kiosk" user - Lockout menu -> enable protection with a real password entered via the masked prompt -> set 20m timeout, 23:00 daily lock, boot password on -> confirmed the menu redraws with the new state -> clean exit (code 0). Checked the resulting config.json and file permissions on disk.
282 lines
7.4 KiB
Bash
282 lines
7.4 KiB
Bash
#!/bin/bash
|
|
################################################################################
|
|
# lib/menu.sh - Reusable numbered-menu framework + validated input helpers.
|
|
#
|
|
# Goal: menu *behavior* (numbering, "0 to exit/return", input validation)
|
|
# lives here once. Individual menus/*.sh files only supply their content
|
|
# (labels + handler functions) and never re-implement the loop/echo/case
|
|
# boilerplate that made the old single-file installer hard to change safely.
|
|
#
|
|
# Usage:
|
|
# my_menu_builder() {
|
|
# MENU_LABELS=("Do thing A" "Do thing B")
|
|
# MENU_HANDLERS=(action_a action_b)
|
|
# }
|
|
# run_menu "MY MENU TITLE" my_menu_builder [my_status_func]
|
|
#
|
|
# The builder runs fresh on every redraw, so labels/handlers can change
|
|
# based on current state (e.g. "no sites yet" vs "5 sites configured").
|
|
################################################################################
|
|
|
|
################################################################################
|
|
# Logging
|
|
################################################################################
|
|
|
|
log_info() {
|
|
echo "[INFO] $*"
|
|
}
|
|
|
|
log_error() {
|
|
echo "[ERROR] $*" >&2
|
|
}
|
|
|
|
log_success() {
|
|
echo "✓ $*"
|
|
}
|
|
|
|
log_warning() {
|
|
echo "⚠ $*"
|
|
}
|
|
|
|
# Shared "true"/"false" -> "ON"/"OFF" label for status lines and menu
|
|
# entries showing a boolean setting's current value.
|
|
onoff() {
|
|
[[ "$1" == "true" ]] && echo "ON" || echo "OFF"
|
|
}
|
|
|
|
pause() {
|
|
read -r -p "Press Enter to continue..."
|
|
}
|
|
|
|
################################################################################
|
|
# Validated input helpers
|
|
################################################################################
|
|
|
|
validate_yes_no() {
|
|
local answer="$1"
|
|
case "${answer,,}" in
|
|
y|yes|yeah|yep|yup|sure|ok|okay) return 0 ;;
|
|
n|no|nope|nah) return 1 ;;
|
|
*) return 2 ;; # invalid
|
|
esac
|
|
}
|
|
|
|
ask_yes_no() {
|
|
local prompt="$1"
|
|
local default="${2:-n}"
|
|
local answer
|
|
|
|
while true; do
|
|
read -r -p "$prompt (y/n) [$default]: " answer
|
|
answer="${answer:-$default}"
|
|
|
|
validate_yes_no "$answer"
|
|
local result=$?
|
|
|
|
if [[ $result -eq 0 ]]; then
|
|
return 0
|
|
elif [[ $result -eq 1 ]]; then
|
|
return 1
|
|
else
|
|
echo "❌ Invalid input. Please enter 'y' for yes or 'n' for no"
|
|
echo
|
|
fi
|
|
done
|
|
}
|
|
|
|
validate_integer() {
|
|
local value="$1"
|
|
local min="${2:--2147483648}"
|
|
local max="${3:-2147483647}"
|
|
|
|
if [[ $value =~ ^-?[0-9]+$ ]]; then
|
|
if [[ $value -ge $min && $value -le $max ]]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
ask_integer() {
|
|
local prompt="$1"
|
|
local default="$2"
|
|
local min="${3:--2147483648}"
|
|
local max="${4:-2147483647}"
|
|
local value
|
|
|
|
while true; do
|
|
read -r -p "$prompt [$default]: " value
|
|
value="${value:-$default}"
|
|
|
|
if validate_integer "$value" "$min" "$max"; then
|
|
echo "$value"
|
|
return 0
|
|
else
|
|
echo "❌ Invalid number. Please enter an integer between $min and $max" >&2
|
|
echo >&2
|
|
fi
|
|
done
|
|
}
|
|
|
|
validate_time() {
|
|
local time="$1"
|
|
[[ $time =~ ^([0-1][0-9]|2[0-3]):([0-5][0-9])$ ]]
|
|
}
|
|
|
|
ask_time() {
|
|
local prompt="$1"
|
|
local default="$2"
|
|
local time
|
|
|
|
while true; do
|
|
read -r -p "$prompt [$default]: " time
|
|
time="${time:-$default}"
|
|
|
|
if validate_time "$time"; then
|
|
echo "$time"
|
|
return 0
|
|
else
|
|
echo "❌ Invalid time format. Please use HH:MM (00:00 to 23:59)" >&2
|
|
echo >&2
|
|
fi
|
|
done
|
|
}
|
|
|
|
validate_url() {
|
|
local url="$1"
|
|
if [[ $url =~ ^(https?|file|data)://.*$ ]] || [[ $url =~ ^about: ]]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
ask_url() {
|
|
local prompt="$1"
|
|
local default="$2"
|
|
local url
|
|
|
|
while true; do
|
|
read -r -p "$prompt [$default]: " url
|
|
url="${url:-$default}"
|
|
|
|
if validate_url "$url"; then
|
|
echo "$url"
|
|
return 0
|
|
else
|
|
echo "❌ Invalid URL. Must start with http://, https://, file://, data:, or about:" >&2
|
|
echo >&2
|
|
fi
|
|
done
|
|
}
|
|
|
|
ask_text() {
|
|
local prompt="$1"
|
|
local default="${2:-}"
|
|
local value
|
|
|
|
read -r -p "$prompt [$default]: " value
|
|
echo "${value:-$default}"
|
|
}
|
|
|
|
validate_menu_choice() {
|
|
local choice="$1"
|
|
local max="$2"
|
|
validate_integer "$choice" 0 "$max"
|
|
}
|
|
|
|
ask_menu_choice() {
|
|
local max="$1"
|
|
local choice
|
|
|
|
while true; do
|
|
read -r -p "Choose [0-$max]: " choice
|
|
|
|
if validate_menu_choice "$choice" "$max"; then
|
|
echo "$choice"
|
|
return 0
|
|
else
|
|
echo "❌ Invalid choice. Please enter a number between 0 and $max" >&2
|
|
echo >&2
|
|
fi
|
|
done
|
|
}
|
|
|
|
################################################################################
|
|
# Menu framework
|
|
################################################################################
|
|
|
|
print_menu_header() {
|
|
local title="$1"
|
|
echo "══════════════════════════════════════════════════════════"
|
|
printf " %s\n" "$title"
|
|
echo "══════════════════════════════════════════════════════════"
|
|
echo
|
|
}
|
|
|
|
# run_menu TITLE BUILDER_FUNC [STATUS_FUNC] [EXIT_LABEL]
|
|
#
|
|
# BUILDER_FUNC must set the globals MENU_LABELS and MENU_HANDLERS (parallel
|
|
# indexed arrays). It is called once per redraw, so it can reflect current
|
|
# state. STATUS_FUNC, if given, is called right after the header to print
|
|
# read-only context (current settings, current list, etc).
|
|
#
|
|
# Entries are auto-numbered 1..N. "0" always returns from run_menu - no
|
|
# menu file needs to hand-roll its own exit case.
|
|
#
|
|
# The handler is called as `handler "$choice"` (the 1-based number picked),
|
|
# so a data-driven list (e.g. a set of timezones) can share one handler
|
|
# instead of needing a distinct wrapper function per entry. Handlers that
|
|
# don't care can just ignore the argument.
|
|
run_menu() {
|
|
local title="$1"
|
|
local builder="$2"
|
|
local status_func="${3:-}"
|
|
local exit_label="${4:-Return}"
|
|
|
|
while true; do
|
|
clear
|
|
print_menu_header "$title"
|
|
|
|
if [[ -n "$status_func" ]]; then
|
|
"$status_func"
|
|
echo
|
|
fi
|
|
|
|
local -a MENU_LABELS=()
|
|
local -a MENU_HANDLERS=()
|
|
"$builder"
|
|
|
|
if [[ "${#MENU_LABELS[@]}" -eq 0 ]]; then
|
|
log_warning "Nothing to do here yet."
|
|
echo " 0. $exit_label"
|
|
echo
|
|
ask_menu_choice 0 >/dev/null
|
|
return 0
|
|
fi
|
|
|
|
local i=1
|
|
for label in "${MENU_LABELS[@]}"; do
|
|
printf " %2d. %s\n" "$i" "$label"
|
|
i=$((i + 1))
|
|
done
|
|
echo " 0. $exit_label"
|
|
echo
|
|
|
|
local choice
|
|
choice=$(ask_menu_choice "${#MENU_LABELS[@]}")
|
|
|
|
if [[ "$choice" == "0" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# `|| true`: this whole tool runs under `set -e`. A handler that
|
|
# legitimately fails (invalid input, a guard clause, etc) and
|
|
# returns non-zero as its last statement must not be allowed to
|
|
# take the entire session down - it should just redraw the menu.
|
|
# Absorbing that here means no menus/*.sh file has to think about
|
|
# set -e at all.
|
|
"${MENU_HANDLERS[$((choice - 1))]}" "$choice" || true
|
|
done
|
|
}
|