]> git.plutz.net Git - cgilite/blobdiff - users.sh
ordered list of mime types, additional pdf and text types
[cgilite] / users.sh
index 1959e9de4b757084fad3d850b1b7b771517cb3c3..6a6833e96c592043669006bbeec91b628a70667e 100755 (executable)
--- a/users.sh
+++ b/users.sh
@@ -6,31 +6,169 @@ include_users="$0"
 . "${_EXEC}/cgilite/session.sh"
 . "${_EXEC}/cgilite/storage.sh"
 
-USER_REGISTRATION="${USER_REGISTRATION:-true}"
-USER_REQUIREEMAIL="${USER_REQUIREEMAIL:-true}"
+SENDMAIL=${SENDMAIL-sendmail}
+
+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}"
+
+MAILFROM="${MAILDOMAIN-noreply@${HTTP_HOST%:*}}"
 
 HTTP_HOST="$(HEADER Host)"
-MAILFROM="${MAILDOMAIN:-noreply@${HTTP_HOST%:*}}"
 
-user_db="${_DATA}/users.db"
-unset USER_ID USER_NAME USER_EMAIL
+[ "$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
+'
+
+unset USER_IDMAP
+eval "$UNSET_USER"
+
+user_db="${user_db:-${_DATA}/users.db}"
 
-# USER DB
-# UID  UNAME   STATUS (pending|active|deleted) EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+read_user() {
+  local user="$1"
 
-user_init(){
-   local user_id="$(SESSION_VAR user_id)"
-   local UID   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
-   [ "$user_id" ] \
-   && read -r UID      UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
-       $(grep "^${user_id}     " "$user_db")
+  # 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
-   [ "$STATUS" -a "$EXPIRE" ] \
-   && if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" ]; then
-     USER_ID="$UID"
-     USER_NAME="$(UNSTRING "$UNAME")"
-     USER_EMAIL="$(UNSTRING "$EMAIL")"
-   fi
+  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
+    eval "$UNSET_USER"
+    return 1
+  fi
+}
+
+update_user() {
+  # 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 arg
+
+  for arg in "$@"; do case $arg in
+    uname=*) uname="${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
+    while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
+                  FUTUREUSE; do
+    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}}" \
+             "${expire:-$((_DATE + USER_ACCOUNTEXPIRE))}" \
+             "$(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      %i      %s      %s\n' \
+             "$UID_" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
+             "$EXPIRE" "$DEVICES" "$FUTUREUSE"
+    fi
+    done <"$user_db" >"${user_db}.$$"
+    mv -- "${user_db}.$$" "$user_db"
+    RELEASE "$user_db"
+  else
+    return 1
+  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(){
@@ -56,9 +194,9 @@ user_checkemail(){
 
 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" ] \
-  && 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
@@ -66,9 +204,9 @@ user_nameexist(){
 
 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" ] \
-  && 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
@@ -89,7 +227,7 @@ user_register(){
   local pwsalt="$(randomid)"
   local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
 
-  if [ "$USER_REGISTRATION" != true ]; then
+  if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
   fi
 
@@ -98,12 +236,10 @@ user_register(){
       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"
-      sendmail -t -f "$MAILFROM" <<-EOF
+    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")"
+      "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
        From: ${MAILFROM}
        To: ${email}
        Subject: Your account registration at ${HTTP_HOST%:*}
@@ -112,9 +248,9 @@ user_register(){
 
        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
@@ -138,33 +274,73 @@ user_register(){
       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"
 
-      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
 }
 
+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")"
+    "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
+       From: ${MAILFROM}
+       To: ${email}
+       Subject: You have been invited to ${HTTP_HOST%:*}
+
+       ${USER_NAME:-Someone} has offered an invitation to this email address.
+
+       ${message}
+
+       You can create your account using this link:
+
+           ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+
+       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
+       simply ignore this message and we will remove your email address from
+       our database within the next day.
+
+       This is an automatic email. Any direct reply will not be received.
+       Your Account Registration Robot.
+       EOF
+    REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+  else
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+  fi
+}
+
 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)"
 
+  read_user "${uid}"
+
   if [ "$signature" != "$(session_mac "$uid")" ]; then
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
   elif [ ! "$uname" ]; then
@@ -175,27 +351,15 @@ user_confirm(){
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
   elif [ "$pw" != "$pwconfirm" ]; then
     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
-  elif LOCK "$user_db"; then
-    read -r UID        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
-       $(grep "^${uid} " "$user_db")
-       EOF
-
-    if [ "$STATUS" != pending -o "$EXPIRE" -le "$_DATE" ]; then
-      RELEASE "$user_db"
-      REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
+  elif [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
+  elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then
+    SESSION_COOKIE new
+    SESSION_BIND user_id "$USER_ID"
+    if [ "$USER_ACCOUNTPAGE" ]; then
+      REDIRECT "${USER_ACCOUNTPAGE}"
     else
-      printf '%s       %s      active  %s      %s      %s      %i      %s      %s\n' \
-             "$UID" "$(STRING "$uname")" "$EMAIL" \
-             "$pwsalt" "$(user_pwhash "$pwsalt" "$pw")" \
-             "$(( $_DATE + 86400 * 730 ))" "$DEVICES" "$FUTUREUSE" \
-             >"${user_db}.$$"
-      grep -v "^${uid} " "$user_db" >>"${user_db}.$$"
-      mv "${user_db}.$$" "${user_db}"
-      RELEASE "$user_db"
-
-      SESSION_COOKIE new
-      SESSION_BIND user_id "$UID"
-      REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+      REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
     fi
   else
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
@@ -206,15 +370,15 @@ user_login(){
   # 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" ] \
-  && 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
-        SESSION_BIND user_id "$UID"
+        SESSION_BIND user_id "$UID_"
         REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
       fi
     fi
@@ -226,15 +390,41 @@ user_logout(){
   # destroy cookie, destroy session
   # keep device cookie
   new_session
-  SET_COOKIE 0 session=""
-  SET_COOKIE 0 user_id=""
+  SESSION_COOKIE new
+  SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly
   REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT"
 }
 
 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 -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_PWMISMATCH"
+    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
   :
@@ -243,25 +433,51 @@ user_disable(){
   :
 }
 
-user_init
+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 ;;
-  user_update)
-    :;;
+  user_update)   user_update ;;
   user_recover)
     :;;
   user_disable)
     :;;
 esac
 
+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
+       [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(){
   if [ "$(GET user_confirm)" ]; then
     w_user_confirm
-  elif [ "$USER_REGISTRATION" != true ]; then
+  elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
     cat <<-EOF
        [div #user_register .disabled
        User Registration is disabled.
@@ -280,7 +496,7 @@ w_user_register(){
   elif [ "$USER_REQUIREEMAIL" != true ]; then
     cat <<-EOF
        [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]
@@ -290,12 +506,12 @@ w_user_register(){
 }
 
 w_user_confirm(){
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  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
+    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
@@ -303,8 +519,10 @@ w_user_confirm(){
        [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]
@@ -326,6 +544,41 @@ w_user_confirm(){
   fi
 }
 
+w_user_invite(){
+  local uid invlink
+
+  if [ "$(GET user_confirm)" ]; then
+    w_user_confirm
+  elif [ "$USER_ID" -a "$SENDMAIL" ]; then
+    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
+  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"
+    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
+  else
+    cat <<-EOF
+       [div #user_invite .notallowed
+         Only registered users may send an invitation to another user.
+       ]
+       EOF
+  fi
+}
+
 w_user_login(){
   if [ ! "$USER_ID" ]; then
     cat <<-EOF
@@ -338,7 +591,7 @@ w_user_login(){
   elif [ "$USER_ID" ]; then
     cat <<-EOF
        [form #user_login .logout method=POST
-         [p You are currently logged in as "${USER_NAME}"]
+         [p Logged in as [span . $(HTML ${USER_NAME})]]
          [submit "action" "user_logout" Logout]
        ]
        EOF