]> git.plutz.net Git - cgilite/blobdiff - users.sh
new function RXLITERAL() for escaping regex characters
[cgilite] / users.sh
index 7721def9c5c07927502309a9a11ed8e60af523b9..32299ff1e3f97f26f7cb2c7d3860d56c1a3e83d2 100755 (executable)
--- a/users.sh
+++ b/users.sh
@@ -1,46 +1,94 @@
 #!/bin/sh
 
 #!/bin/sh
 
+# Copyright 2021 - 2024 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
 [ -n "$include_users" ] && return 0
 include_users="$0"
 
 [ -n "$include_users" ] && return 0
 include_users="$0"
 
-. "${_EXEC}/cgilite/session.sh"
-. "${_EXEC}/cgilite/storage.sh"
+. "${_EXEC:-.}/cgilite/session.sh"
+. "${_EXEC:-.}/cgilite/storage.sh"
+
+SENDMAIL=${SENDMAIL-sendmail}
 
 
-USER_REGISTRATION="${USER_REGISTRATION:-true}"
-USER_REQUIREEMAIL="${USER_REQUIREEMAIL:-true}"
+USER_REGISTRATION="${USER_REGISTRATION-true}"
+USER_REQUIREEMAIL="${USER_REQUIREEMAIL-true}"
+USER_ACCOUNTPAGE="${USER_ACCOUNTPAGE}"
+
+USER_ACCOUNTEXPIRE="${USER_ACCOUNTEXPIRE:-$((86400 * 730))}"
+USER_CONFIRMEXPIRE="${USER_CONFIRMEXPIRE:-86400}"
 
 HTTP_HOST="$(HEADER Host)"
 
 HTTP_HOST="$(HEADER Host)"
-MAILFROM="${MAILDOMAIN:-noreply@${HTTP_HOST%:*}}"
+MAILFROM="noreply@${HTTP_HOST%:*}"
+
+[ "$HTTPS" ] && SCHEMA=https || SCHEMA=http
+
+# == FILE FORMAT ==
+# UID  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+#              (pending|active|deleted)
+
+# == GLOBALS ==
+UNSET_USER='unset \
+  USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+  USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+'
+
+LOCAL_USER='local \
+  USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+  USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+'
 
 
-user_db="${_DATA}/users.db"
-unset USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
-      USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+# == TRANSLATIONS ==
+# override all functions marked with "TRANSLATION"
+# sed -n '/TRANSLATION$/,/^}/p;' <cgilite/users.sh
 
 
-# USER DB
-# UID  UNAME   STATUS (pending|active|deleted) EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+unset USER_IDMAP
+eval "$UNSET_USER"
 
 
-user_id="$(SESSION_VAR user_id)"
-if [ "$user_id" -a -f "$user_db" -a -r "$user_db" ]; then
-   read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
-           USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF
-       $(grep "^${user_id}     " "$user_db")
+user_db="${user_db:-${_DATA}/users.db}"
+
+read_user() {
+  local user="$1"
+
+  # Global exports
+  USER_ID='' USER_NAME='' USER_STATUS='' USER_EMAIL='' USER_PWSALT=''
+  USER_PWHASH='' USER_EXPIRE='' USER_DEVICES='' USER_FUTUREUSE=''
+
+  if [ $# -eq 0 ]; then
+    read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+            USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+  elif [ "$user" -a -f "$user_db" -a -r "$user_db" ]; then
+    read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+            USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF
+       $(grep "^${user}        " "${user_db}")
        EOF
        EOF
-  if [ "$USER_ID" -a "$USER_STATUS" = active -a "$USER_EXPIRE" -gt "$_DATE" ]; then
+  fi
+  if [ "$USER_ID" -a "${USER_EXPIRE:-0}" -gt "$_DATE" ]; then
        USER_NAME="$(UNSTRING "$USER_NAME")"
       USER_EMAIL="$(UNSTRING "$USER_EMAIL")"
     USER_DEVICES="$(UNSTRING "$USER_DEVICES")"
     unset USER_PWSALT USER_PWHASH
   else
        USER_NAME="$(UNSTRING "$USER_NAME")"
       USER_EMAIL="$(UNSTRING "$USER_EMAIL")"
     USER_DEVICES="$(UNSTRING "$USER_DEVICES")"
     unset USER_PWSALT USER_PWHASH
   else
-    unset USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
-          USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+    eval "$UNSET_USER"
+    return 1
   fi
   fi
-fi
-unset user_id
+}
 
 
-user_update_user() {
+update_user() {
   # internal function for user update
   local uid="$1" uname status email pwsalt pwhash expire devices futureuse
   # internal function for user update
   local uid="$1" uname status email pwsalt pwhash expire devices futureuse
-  local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
+  local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
   local arg
 
   for arg in "$@"; do case $arg in
   local arg
 
   for arg in "$@"; do case $arg in
@@ -48,27 +96,28 @@ user_update_user() {
     status=*) status="${arg#*=}";;
     email=*) email="${arg#*=}";;
     password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";;
     status=*) status="${arg#*=}";;
     email=*) email="${arg#*=}";;
     password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";;
+    expire=*) expire="${arg#*=}";;
     devices=*) devices="${arg#*=}";;
   esac; done
 
   if LOCK "$user_db"; then
     devices=*) devices="${arg#*=}";;
   esac; done
 
   if LOCK "$user_db"; then
-    while read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
+    while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
                   FUTUREUSE; do
                   FUTUREUSE; do
-    if [ "$UID" = "$uid" ]; then
-      printf '%s       %s      %s      %s      %s      %s      %s      %s      %s\n' \
+    if [ "$UID_" = "$uid" ]; then
+      printf '%s       %s      %s      %s      %s      %s      %i      %s      %s\n' \
              "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \
              "${status:-${status-${STATUS}}${status+\\}}" \
              "${email:-${email-${EMAIL}}${email+\\}}" \
              "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \
              "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \
              "${status:-${status-${STATUS}}${status+\\}}" \
              "${email:-${email-${EMAIL}}${email+\\}}" \
              "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \
-             "$((_DATE + 86400 * 730))" \
+             "${expire:-$((_DATE + USER_ACCOUNTEXPIRE))}" \
              "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \
              "${FUTUREUSE:-\\}"
     elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then
       # omit expired invitations from output
       :
     else
              "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \
              "${FUTUREUSE:-\\}"
     elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then
       # omit expired invitations from output
       :
     else
-      printf '%s       %s      %s      %s      %s      %s      %s      %s      %s\n' \
-             "$UID" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
+      printf '%s       %s      %s      %s      %s      %s      %i      %s      %s\n' \
+             "$UID_" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
              "$EXPIRE" "$DEVICES" "$FUTUREUSE"
     fi
     done <"$user_db" >"${user_db}.$$"
              "$EXPIRE" "$DEVICES" "$FUTUREUSE"
     fi
     done <"$user_db" >"${user_db}.$$"
@@ -79,6 +128,66 @@ user_update_user() {
   fi
 }
 
   fi
 }
 
+new_user(){
+  local user="${1:-$(timeid)}"
+  shift 1
+
+  if LOCK "$user_db"; then
+    if grep -q "^${user}       " "$user_db"; then
+      RELEASE "$user_db"
+      return 1
+    fi
+    printf '%s \\      %s      \\      \\      \\      %i      \\      \\\n' \
+           "$user" "pending" "$(( _DATE + USER_CONFIRMEXPIRE ))" >>"$user_db"
+  else
+    return 1
+  fi
+
+  if [ $# -eq 0 ]; then
+    RELEASE "$user_db"
+    return 0
+  elif update_user "$user" "$@"; then
+    return 0
+  else
+    RELEASE "$user_db"
+    return 1
+  fi
+}
+
+user_idmap(){
+  local uid="$1" ret
+  eval "$LOCAL_USER"
+
+  if [ ! "$USER_IDMAP" ]; then
+    while read_user; do
+      USER_IDMAP="${USER_IDMAP}${USER_ID}      ${USER_NAME}${BR}"
+    done <"$user_db"
+  fi
+  if [ "$uid" -a "$USER_IDMAP" != "${USER_IDMAP##*${uid}       }" ]; then
+    ret="${USER_IDMAP##*${uid} }"; ret="${ret%%${BR}*}";
+    printf '%s\n' "$ret"
+    return 0
+  elif [ "$uid" ]; then
+    return 1
+  else
+    printf '%s' "$USER_IDMAP"
+    return 0
+  fi
+}
+
+user_idof(){
+  local name="$(STRING "$1")" ret
+  [ "$USER_IDMAP" ] || user_idmap >/dev/null
+
+  if [ "${name%\\}" -a "$USER_IDMAP" != "${USER_IDMAP% ${name}${BR}*}" ]; then
+    ret="${USER_IDMAP% ${name}${BR}*}"; ret="${ret##*${BR}}"
+    printf '%s\n' "$ret"
+    return 0
+  else
+    return 1
+  fi
+}
+
 user_checkname(){
   { [ $# -gt 0 ] && printf %s "$*" || cat; } \
   | sed -nE '
 user_checkname(){
   { [ $# -gt 0 ] && printf %s "$*" || cat; } \
   | sed -nE '
@@ -102,9 +211,9 @@ user_checkemail(){
 
 user_nameexist(){
   local uname="$(STRING "$1")"
 
 user_nameexist(){
   local uname="$(STRING "$1")"
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
   [ -f "$user_db" -a -r "$user_db" ] \
   [ -f "$user_db" -a -r "$user_db" ] \
-  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
     [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
   done <"$user_db"
   return 1
     [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
   done <"$user_db"
   return 1
@@ -112,9 +221,9 @@ user_nameexist(){
 
 user_emailexist(){
   local email="$(STRING "$1")"
 
 user_emailexist(){
   local email="$(STRING "$1")"
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
   [ -f "$user_db" -a -r "$user_db" ] \
   [ -f "$user_db" -a -r "$user_db" ] \
-  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
     [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
   done <"$user_db"
   return 1
     [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
   done <"$user_db"
   return 1
@@ -126,33 +235,8 @@ user_pwhash(){
   printf '%s\n' "${hash%% *}"
 }
 
   printf '%s\n' "${hash%% *}"
 }
 
-user_register(){
-  # reserve account, send registration mail
-  # preliminary uid, expiration, signature
-  local uid="$(timeid)"
-  local uname="$(POST uname |user_checkname)"
-  local email="$(POST email |user_checkemail)"
-  local pwsalt="$(randomid)"
-  local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
-
-  if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
-    REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
-  fi
-
-  if   [ "$USER_REQUIREEMAIL" = true ]; then
-    if [ ! "email" ]; then
-      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
-    elif user_emailexist "$email"; then
-      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
-    elif LOCK "$user_db"; then
-      printf '%s       \\      pending %s      \\      \\      %i      \\      \\\n' \
-             "$uid" "$(STRING "$email")" "$(( $_DATE + 86400 ))" \
-             >>"$user_db"
-      RELEASE "$user_db"
-
-      debug "Sending Activation Link:" \
-            "https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
-      sendmail -t -f "$MAILFROM" <<-EOF
+user_register_email() {  # TRANSLATION
+  "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
        From: ${MAILFROM}
        To: ${email}
        Subject: Your account registration at ${HTTP_HOST%:*}
        From: ${MAILFROM}
        To: ${email}
        Subject: Your account registration at ${HTTP_HOST%:*}
@@ -161,9 +245,9 @@ user_register(){
 
        You can activate your account using this link:
 
 
        You can activate your account using this link:
 
-           https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+           ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
 
 
-       This registration link will expire after 24 hours.
+       This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
 
        If you did not request an account at ${HTTP_HOST%:*}, then someone else
        probably entered your email address by accident. In this case you shoud
 
        If you did not request an account at ${HTTP_HOST%:*}, then someone else
        probably entered your email address by accident. In this case you shoud
@@ -173,6 +257,30 @@ user_register(){
        This is an automatic email. Any direct reply will not be received.
        Your Account Registration Robot.
        EOF
        This is an automatic email. Any direct reply will not be received.
        Your Account Registration Robot.
        EOF
+}
+
+user_register(){
+  # reserve account, send registration mail
+  # preliminary uid, expiration, signature
+  local uid="$(timeid)"
+  local uname="$(POST uname |user_checkname)"
+  local email="$(POST email |user_checkemail)"
+  local pwsalt="$(randomid)"
+  local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
+
+  if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
+  fi
+
+  if   [ "$USER_REQUIREEMAIL" = true ]; then
+    if [ ! "$email" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
+    elif user_emailexist "$email"; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
+    elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
+      debug "Sending Activation Link:" \
+            "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+      user_register_email
       REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
     else
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
       REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
     else
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
@@ -187,41 +295,23 @@ user_register(){
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
     elif [ "$pw" != "$pwconfirm" ]; then
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
     elif [ "$pw" != "$pwconfirm" ]; then
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
-    elif LOCK "$user_db"; then
-      printf '%s       %s      active  %s      %s      %s      %i      \\      \\\n' \
-             "$uid" "$(STRING "$uname")" "$(STRING "$email")" \
-             "$pwsalt" "$(user_pwhash "$pwsalt" "$pw")" \
-             "$(( $_DATE + 86400 * 730 ))" \
-             >>"$user_db"
-      RELEASE "$user_db"
-
+    elif new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + USER_ACCOUNTEXPIRE))"; then
       SESSION_COOKIE new
       SESSION_BIND user_id "$uid"
 
       SESSION_COOKIE new
       SESSION_BIND user_id "$uid"
 
-      REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+      if [ "$USER_ACCOUNTPAGE" ]; then
+        REDIRECT "${USER_ACCOUNTPAGE}"
+      else
+        REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+      fi
     else
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
     fi
   fi
 }
 
     else
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
     fi
   fi
 }
 
-user_invite(){
-  local uid="$(timeid)"
-  local email="$(POST email |user_checkemail)"
-  local message="$(POST message)"
-
-  if [ ! "email" ]; then
-    REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
-  elif user_emailexist "$email"; then
-    REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
-  elif LOCK "$user_db"; then
-    printf '%s \\      pending %s      \\      \\      %i      \\      \\\n' \
-           "$uid" "$(STRING "$email")" "$(( $_DATE + 86400 ))" \
-           >>"$user_db"
-    RELEASE "$user_db"
-    debug "Sending Invitation Link:" \
-          "https://${HTTP_HOST}${BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
-    sendmail -t -f "$MAILFROM" <<-EOF
+user_invite_email(){  # TRANSLATION
+  "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
        From: ${MAILFROM}
        To: ${email}
        Subject: You have been invited to ${HTTP_HOST%:*}
        From: ${MAILFROM}
        To: ${email}
        Subject: You have been invited to ${HTTP_HOST%:*}
@@ -232,9 +322,9 @@ user_invite(){
 
        You can create your account using this link:
 
 
        You can create your account using this link:
 
-           https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+           ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
 
 
-       This registration link will expire after 24 hours.
+       This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
 
        If you do not know what this is about, then someone else probably
        entered your email address by accident. In this case you shoud
 
        If you do not know what this is about, then someone else probably
        entered your email address by accident. In this case you shoud
@@ -244,6 +334,21 @@ user_invite(){
        This is an automatic email. Any direct reply will not be received.
        Your Account Registration Robot.
        EOF
        This is an automatic email. Any direct reply will not be received.
        Your Account Registration Robot.
        EOF
+}
+
+user_invite(){
+  local uid="$(timeid)"
+  local email="$(POST email |user_checkemail)"
+  local message="$(POST message)"
+
+  if [ ! "$email" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
+  elif user_emailexist "$email"; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
+  elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
+    debug "Sending Invitation Link:" \
+          "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+    user_invite_email
     REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
   else
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
     REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
   else
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
@@ -252,16 +357,14 @@ user_invite(){
 
 user_confirm(){
   # enable account
 
 user_confirm(){
   # enable account
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
-  local uid="$(POST uid |checkid)"
+  eval "$LOCAL_USER"
+  local uid="$(POST uid |checkid || printf invalid)"
   local signature="$(POST signature)"
   local uname="$(POST uname |user_checkname)"
   local pwsalt="$(randomid)"
   local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
 
   local signature="$(POST signature)"
   local uname="$(POST uname |user_checkname)"
   local pwsalt="$(randomid)"
   local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
 
-  read -r UID  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
-       $(grep "^${uid:-invalid}        " "$user_db")
-       EOF
+  read_user "${uid}"
 
   if [ "$signature" != "$(session_mac "$uid")" ]; then
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
 
   if [ "$signature" != "$(session_mac "$uid")" ]; then
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
@@ -273,12 +376,16 @@ user_confirm(){
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
   elif [ "$pw" != "$pwconfirm" ]; then
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
   elif [ "$pw" != "$pwconfirm" ]; then
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
-  elif [ "$STATUS" != pending -o \! "$EXPIRE" -gt "$_DATE" ]; then
+  elif [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
-  elif user_update_user "$UID" uname="$uname" status=active password="$pw"; then
+  elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then
     SESSION_COOKIE new
     SESSION_COOKIE new
-    SESSION_BIND user_id "$UID"
-    REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
+    SESSION_BIND user_id "$USER_ID"
+    if [ "$USER_ACCOUNTPAGE" ]; then
+      REDIRECT "${USER_ACCOUNTPAGE}"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
+    fi
   else
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
   fi
   else
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
   fi
@@ -288,15 +395,15 @@ user_login(){
   # set cookie
   # keep logged in - device cookie?
   # initialize new session!
   # set cookie
   # keep logged in - device cookie?
   # initialize new session!
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
   local uname="$(POST uname |STRING)" pw="$(POST pw)"
 
   [ -f "$user_db" -a -r "$user_db" ] \
   local uname="$(POST uname |STRING)" pw="$(POST pw)"
 
   [ -f "$user_db" -a -r "$user_db" ] \
-  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
     if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
       if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
         SESSION_COOKIE new
     if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
       if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
         SESSION_COOKIE new
-        SESSION_BIND user_id "$UID"
+        SESSION_BIND user_id "$UID_"
         REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
       fi
     fi
         REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
       fi
     fi
@@ -314,9 +421,35 @@ user_logout(){
 }
 
 user_update(){
 }
 
 user_update(){
-  # passphrase, email
-  :
+  # todo: username update, email update / email confirm
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  # local uname="$(POST uname |STRING)"
+  local uid oldpw pw pwconfirm
+
+        uid="$(POST uid)"
+      oldpw="$(POST oldpw)"
+         pw="$(POST pw |grep -m1 -xE '.{6,}')"
+  pwconfirm="$(POST pwconfirm)"
+
+
+  read -r UID_ UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${uid} " "$user_db")
+       EOF
+
+  if [ "$UID_" = "$USER_ID" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$oldpw")" ]; then
+    if [ "$pw" -a "$pw" = "$pwconfirm" ]; then
+      update_user "${uid}" password="$pw"
+      REDIRECT "${_BASE}${PATH_INFO}#UPDATE_SUCCESS"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
+    fi
+  elif [ "$UID_" = "$USER_ID" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_AUTH_PASSWORD"
+  else
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_NOTLOGGEDIN"
+  fi
 }
 }
+
 user_recover(){
   # send recover link
   :
 user_recover(){
   # send recover link
   :
@@ -325,31 +458,56 @@ user_disable(){
   :
 }
 
   :
 }
 
+read_user "$(SESSION_VAR user_id)"
+[ "$USER_STATUS" -a "$USER_STATUS" != active ] && eval $UNSET_USER
+
 [ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
   user_register) user_register ;;
   user_confirm)  user_confirm ;;
   user_invite)   user_invite ;;
   user_login)    user_login ;;
   user_logout)   user_logout ;;
 [ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
   user_register) user_register ;;
   user_confirm)  user_confirm ;;
   user_invite)   user_invite ;;
   user_login)    user_login ;;
   user_logout)   user_logout ;;
-  user_update)
-    :;;
+  user_update)   user_update ;;
   user_recover)
     :;;
   user_disable)
     :;;
 esac
 
   user_recover)
     :;;
   user_disable)
     :;;
 esac
 
-w_user_register(){
-  if [ "$(GET user_confirm)" ]; then
-    w_user_confirm
-  elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
+export USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+       USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+
+
+w_user_update(){
+  if [ ! "$USER_ID" ]; then
     cat <<-EOF
     cat <<-EOF
+       [div #user_update .nouser
+       This page can only be used by registered users
+       ]
+       EOF
+  else
+    cat <<-EOF
+       [form #user_update method=POST
+         [hidden "uid" "$USER_ID"]
+         [p .username Logged in as $USER_NAME]
+         [input type=password name=oldpw placeholder="Current Passphrase"]
+         [input type=password name=pw placeholder="New Passphrase" pattern=".{6,}"]
+         [input type=password name=pwconfirm placeholder="Confirm New Passphrase" pattern=".{6,}"]
+         [submit "action" "user_update" Update Passphrase]
+       ]
+       EOF
+  fi
+}
+
+w_user_register_disabled(){  # TRANSLATION
+  cat <<-EOF
        [div #user_register .disabled
        User Registration is disabled.
        ]
        EOF
        [div #user_register .disabled
        User Registration is disabled.
        ]
        EOF
-  elif [ "$USER_REQUIREEMAIL" = true ]; then
-    cat <<-EOF
+}
+w_user_register_sendmail(){  # TRANSLATION
+  cat <<-EOF
        [form #user_register .registeremail method=POST
          [p We will send an activation mail to your email address.
            You can continue the signup process when you click on the
        [form #user_register .registeremail method=POST
          [p We will send an activation mail to your email address.
            You can continue the signup process when you click on the
@@ -358,90 +516,146 @@ w_user_register(){
          [submit "action" "user_register" Sign Up]
        ]
        EOF
          [submit "action" "user_register" Sign Up]
        ]
        EOF
-  elif [ "$USER_REQUIREEMAIL" != true ]; then
-    cat <<-EOF
+}
+w_user_register_direct(){  # TRANSLATION
+  cat <<-EOF
        [form #user_register .registername method=POST
        [form #user_register .registername method=POST
-          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off]
+          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
          [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
          [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
          [submit "action" "user_register" Sign Up]
        ]
        EOF
          [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
          [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
          [submit "action" "user_register" Sign Up]
        ]
        EOF
-  fi
 }
 
 }
 
-w_user_confirm(){
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
-  local user_confirm="$(GET user_confirm)"
-  local uid="${user_confirm% *}" signature="${user_confirm#* }"
+w_user_register(){
+  if [ "$(GET user_confirm)" ]; then
+    w_user_confirm
+  elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
+    w_user_register_disabled
+  elif [ "$USER_REQUIREEMAIL" = true ]; then
+    w_user_register_sendmail
+  elif [ "$USER_REQUIREEMAIL" != true ]; then
+    w_user_register_direct
+  fi
+}
 
 
-  if [ "$signature" = "$(session_mac "$uid")" ]; then
-    read -r UID        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
-       $(grep "^${uid} " "$user_db")
-       EOF
-    if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
-      cat <<-EOF
+w_user_confirm_proceed(){  # TRANSLATION
+  cat <<-EOF
        [form #user_confirm method=POST
          [input type=hidden name=uid value="${uid}"]
          [input type=hidden name=signature value="${signature}"]
        [form #user_confirm method=POST
          [input type=hidden name=uid value="${uid}"]
          [input type=hidden name=signature value="${signature}"]
-         [input disabled=disabled value="$(HTML "$EMAIL")"]
-          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off]
+         $([ "$EMAIL" != '\' ] && printf \
+           '[input disabled=disabled value="%s" placeholder="Email"]' "$(UNSTRING "$EMAIL" |HTML)"
+         )
+          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
          [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
          [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
          [submit "action" "user_confirm" Finish Registration]
        ]
        EOF
          [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
          [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
          [submit "action" "user_confirm" Finish Registration]
        ]
        EOF
-    else
-      cat <<-EOF
+}
+w_user_confirm_expired(){  # TRANSLATION
+  cat <<-EOF
        [div #user_confirm .expired
          [p This activation link is not valid anymore.]
        ]
        EOF
        [div #user_confirm .expired
          [p This activation link is not valid anymore.]
        ]
        EOF
-    fi
-  else
-    cat <<-EOF
+}
+w_user_confirm_invalid(){  # TRANSLATION
+  cat <<-EOF
        [div #user_confirm .invalid
          [p This activation link is invalid. Make sure you copied the whole activation link from your email and be careful not to include any line breaks.]
        ]
        EOF
        [div #user_confirm .invalid
          [p This activation link is invalid. Make sure you copied the whole activation link from your email and be careful not to include any line breaks.]
        ]
        EOF
+}
+
+w_user_confirm(){
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local user_confirm="$(GET user_confirm)"
+  local uid="${user_confirm% *}" signature="${user_confirm#* }"
+
+  if [ "$signature" = "$(session_mac "$uid")" ]; then
+    read -r UID_       UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${uid} " "$user_db")
+       EOF
+    if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
+      w_user_confirm_proceed
+    else
+      w_user_confirm_expired
+    fi
+  else
+    w_user_confirm_invalid
   fi
 }
 
   fi
 }
 
-w_user_invite(){
-  if [ "$(GET user_confirm)" ]; then
-    w_user_confirm
-  elif [ "$USER_ID" ]; then
-    cat <<-EOF
+w_user_invite_email(){  # TRANSLATION
+  cat <<-EOF
        [form #user_invite method=POST
          [input placeholder="Email Recipient" name=email autocomplete=off]
          [textarea name="message" placeholder="Message to recipient" . ]
          [submit "action" "user_invite" Send Invitation]
        ]
        EOF
        [form #user_invite method=POST
          [input placeholder="Email Recipient" name=email autocomplete=off]
          [textarea name="message" placeholder="Message to recipient" . ]
          [submit "action" "user_invite" Send Invitation]
        ]
        EOF
-  else
-    cat <<-EOF
+}
+w_user_invite_link(){  # TRANSLATION
+  cat <<-EOF
+       [div #user_invite .link
+          [p An anonymous user account has been set up. Send the following link to the intended user, so they may claim their account. The link will remain valid for $((USER_CONFIRMEXPIRE / 3600)) hours.]
+          [a href="$(HTML "$invlink")" . $(HTML "$invlink")]
+
+          [p [a href="#" . Set up another account]]
+       ]
+       EOF
+}
+w_user_invite_deny(){  # TRANSLATION
+  cat <<-EOF
        [div #user_invite .notallowed
          Only registered users may send an invitation to another user.
        ]
        EOF
        [div #user_invite .notallowed
          Only registered users may send an invitation to another user.
        ]
        EOF
+}
+
+w_user_invite(){
+  local uid invlink
+
+  if [ "$(GET user_confirm)" ]; then
+    w_user_confirm
+  elif [ "$USER_ID" -a "$USER_REQUIREEMAIL" = true ]; then
+    w_user_invite_email
+  elif [ "$USER_ID" ]; then
+    uid="$(timeid)"
+    new_user "$uid" status=pending expire="$((_DATE + USER_CONFIRMEXPIRE))"
+    invlink="${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+    debug "New Invitation Link: $invlink"
+    w_user_invite_link
+  else
+    w_user_invite_deny
   fi
 }
 
   fi
 }
 
-w_user_login(){
-  if [ ! "$USER_ID" ]; then
-    cat <<-EOF
+w_user_login_logon(){  # TRANSLATION
+  cat <<-EOF
        [form #user_login .login method=POST
        [form #user_login .login method=POST
-         [input name=uname placeholder="Username or Email" autocomplete=off]
+         [input name=uname placeholder="Username or Email"]
          [input type=password name=pw placeholder="Passphrase"]
          [submit "action" "user_login" Login]
        ]
        EOF
          [input type=password name=pw placeholder="Passphrase"]
          [submit "action" "user_login" Login]
        ]
        EOF
-  elif [ "$USER_ID" ]; then
-    cat <<-EOF
+}
+w_user_login_logoff(){  # TRANSLATION
+  cat <<-EOF
        [form #user_login .logout method=POST
          [p Logged in as [span . $(HTML ${USER_NAME})]]
          [submit "action" "user_logout" Logout]
        ]
        EOF
        [form #user_login .logout method=POST
          [p Logged in as [span . $(HTML ${USER_NAME})]]
          [submit "action" "user_logout" Logout]
        ]
        EOF
+}
+
+w_user_login(){
+  if [ ! "$USER_ID" ]; then
+    w_user_login_logon
+  elif [ "$USER_ID" ]; then
+    w_user_login_logoff
   fi
 }
   fi
 }