]> git.plutz.net Git - cgilite/blob - users.sh
variable $UID is reserved in bash and cannot be used
[cgilite] / users.sh
1 #!/bin/sh
2
3 [ -n "$include_users" ] && return 0
4 include_users="$0"
5
6 . "${_EXEC}/cgilite/session.sh"
7 . "${_EXEC}/cgilite/storage.sh"
8
9 SENDMAIL=${SENDMAIL-sendmail}
10
11 USER_REGISTRATION="${USER_REGISTRATION-true}"
12 USER_REQUIREEMAIL="${USER_REQUIREEMAIL-true}"
13 USER_ACCOUNTPAGE="${USER_ACCOUNTPAGE}"
14
15 MAILFROM="${MAILDOMAIN-noreply@${HTTP_HOST%:*}}"
16
17 HTTP_HOST="$(HEADER Host)"
18
19 # == FILE FORMAT ==
20 # UID   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
21 #               (pending|active|deleted)
22
23 # == GLOBALS ==
24 UNSET_USER='unset \
25   USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
26   USER_EXPIRE USER_DEVICES USER_FUTUREUSE
27 '
28
29 LOCAL_USER='local \
30   USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
31   USER_EXPIRE USER_DEVICES USER_FUTUREUSE
32 '
33
34 unset USER_IDMAP
35 eval "$UNSET_USER"
36
37 user_db="${user_db:-${_DATA}/users.db}"
38
39 read_user() {
40   local user="$1"
41
42   # Global exports
43   USER_ID='' USER_NAME='' USER_STATUS='' USER_EMAIL='' USER_PWSALT=''
44   USER_PWHASH='' USER_EXPIRE='' USER_DEVICES='' USER_FUTUREUSE=''
45
46   if [ $# -eq 0 ]; then
47     read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
48             USER_EXPIRE USER_DEVICES USER_FUTUREUSE
49   elif [ "$user" -a -f "$user_db" -a -r "$user_db" ]; then
50     read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
51             USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF
52         $(grep "^${user}        " "${user_db}")
53         EOF
54   fi
55   if [ "$USER_ID" -a "${USER_EXPIRE:-0}" -gt "$_DATE" ]; then
56        USER_NAME="$(UNSTRING "$USER_NAME")"
57       USER_EMAIL="$(UNSTRING "$USER_EMAIL")"
58     USER_DEVICES="$(UNSTRING "$USER_DEVICES")"
59     unset USER_PWSALT USER_PWHASH
60   else
61     eval "$UNSET_USER"
62     return 1
63   fi
64 }
65
66 update_user() {
67   # internal function for user update
68   local uid="$1" uname status email pwsalt pwhash expire devices futureuse
69   local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
70   local arg
71
72   for arg in "$@"; do case $arg in
73     uname=*) uname="${arg#*=}";;
74     status=*) status="${arg#*=}";;
75     email=*) email="${arg#*=}";;
76     password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";;
77     expire=*) expire="${arg#*=}";;
78     devices=*) devices="${arg#*=}";;
79   esac; done
80
81   if LOCK "$user_db"; then
82     while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
83                   FUTUREUSE; do
84     if [ "$UID_" = "$uid" ]; then
85       printf '%s        %s      %s      %s      %s      %s      %i      %s      %s\n' \
86              "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \
87              "${status:-${status-${STATUS}}${status+\\}}" \
88              "${email:-${email-${EMAIL}}${email+\\}}" \
89              "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \
90              "${expire:-$((_DATE + 86400 * 730))}" \
91              "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \
92              "${FUTUREUSE:-\\}"
93     elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then
94       # omit expired invitations from output
95       :
96     else
97       printf '%s        %s      %s      %s      %s      %s      %i      %s      %s\n' \
98              "$UID_" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
99              "$EXPIRE" "$DEVICES" "$FUTUREUSE"
100     fi
101     done <"$user_db" >"${user_db}.$$"
102     mv -- "${user_db}.$$" "$user_db"
103     RELEASE "$user_db"
104   else
105     return 1
106   fi
107 }
108
109 new_user(){
110   local user="${1:-$(timeid)}"
111   shift 1
112
113   if LOCK "$user_db"; then
114     if grep -q "^${user}        " "$user_db"; then
115       RELEASE "$user_db"
116       return 1
117     fi
118     printf '%s  \\      %s      \\      \\      \\      %i      \\      \\\n' \
119            "$user" "pending" "$(( $_DATE + 86400 ))" >>"$user_db"
120   else
121     return 1
122   fi
123
124   if [ $# -eq 0 ]; then
125     RELEASE "$user_db"
126     return 0
127   elif update_user "$user" "$@"; then
128     return 0
129   else
130     RELEASE "$user_db"
131     return 1
132   fi
133 }
134
135 user_idmap(){
136   local uid="$1" ret
137   eval "$LOCAL_USER"
138
139   if [ ! "$USER_IDMAP" ]; then
140     while read_user; do
141       USER_IDMAP="${USER_IDMAP}${USER_ID}       ${USER_NAME}${BR}"
142     done <"$user_db"
143   fi
144   if [ "$uid" -a "$USER_IDMAP" != "${USER_IDMAP##*${uid}        }" ]; then
145     ret="${USER_IDMAP##*${uid}  }"; ret="${ret%%${BR}*}";
146     printf '%s\n' "$ret"
147     return 0
148   elif [ "$uid" ]; then
149     return 1
150   else
151     printf '%s' "$USER_IDMAP"
152     return 0
153   fi
154 }
155
156 user_idof(){
157   local name="$(STRING "$1")" ret
158   [ "$USER_IDMAP" ] || user_idmap >/dev/null
159
160   if [ "${name%\\}" -a "$USER_IDMAP" != "${USER_IDMAP%  ${name}${BR}*}" ]; then
161     ret="${USER_IDMAP%  ${name}${BR}*}"; ret="${ret##*${BR}}"
162     printf '%s\n' "$ret"
163     return 0
164   else
165     return 1
166   fi
167 }
168
169 user_checkname(){
170   { [ $# -gt 0 ] && printf %s "$*" || cat; } \
171   | sed -nE '
172     :X; $!{N;bX;}
173     s;[ \t\r\n]+; ;g;
174     s;^ ;;; s; $;;;
175     /@/d;
176     /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d;
177     p;
178     '
179 }
180
181 user_checkemail(){
182   { [ $# -gt 0 ] && printf %s "$*" || cat; } \
183   | sed -nE '
184     # W3C recommended email regex
185     # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)
186     /^[a-zA-Z0-9.!#$%&'\''*+\/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/p;
187     '
188 }
189
190 user_nameexist(){
191   local uname="$(STRING "$1")"
192   local UID_    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
193   [ -f "$user_db" -a -r "$user_db" ] \
194   && while read -r UID_ UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
195     [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
196   done <"$user_db"
197   return 1
198 }
199
200 user_emailexist(){
201   local email="$(STRING "$1")"
202   local UID_    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
203   [ -f "$user_db" -a -r "$user_db" ] \
204   && while read -r UID_ UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
205     [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
206   done <"$user_db"
207   return 1
208 }
209
210 user_pwhash(){
211   local salt="$1" secret="$2" hash
212   hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)"
213   printf '%s\n' "${hash%% *}"
214 }
215
216 user_register(){
217   # reserve account, send registration mail
218   # preliminary uid, expiration, signature
219   local uid="$(timeid)"
220   local uname="$(POST uname |user_checkname)"
221   local email="$(POST email |user_checkemail)"
222   local pwsalt="$(randomid)"
223   local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
224
225   if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
226     REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
227   fi
228
229   if   [ "$USER_REQUIREEMAIL" = true ]; then
230     if [ ! "email" ]; then
231       REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
232     elif user_emailexist "$email"; then
233       REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
234     elif new_user "$uid" status=pending email="$email" expire="$((_DATE + 86400))"; then
235       debug "Sending Activation Link:" \
236             "https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
237       "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
238         From: ${MAILFROM}
239         To: ${email}
240         Subject: Your account registration at ${HTTP_HOST%:*}
241
242         Someone tried to sign up for a user account using this email address.
243
244         You can activate your account using this link:
245
246             https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
247
248         This registration link will expire after 24 hours.
249
250         If you did not request an account at ${HTTP_HOST%:*}, then someone else
251         probably entered your email address by accident. In this case you shoud
252         simply ignore this message and we will remove your email address from
253         our database within the next day.
254
255         This is an automatic email. Any direct reply will not be received.
256         Your Account Registration Robot.
257         EOF
258       REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
259     else
260       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
261     fi
262
263   elif [ "$USER_REQUIREEMAIL" != true ]; then
264     if [ ! "$uname" ]; then
265       REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID"
266     elif user_nameexist "$uname"; then
267       REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS"
268     elif [ ! "$pw" ]; then
269       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
270     elif [ "$pw" != "$pwconfirm" ]; then
271       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
272     elif new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + 86400 * 730))"; then
273       SESSION_COOKIE new
274       SESSION_BIND user_id "$uid"
275
276       if [ "$USER_ACCOUNTPAGE" ]; then
277         REDIRECT "${USER_ACCOUNTPAGE}"
278       else
279         REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
280       fi
281     else
282       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
283     fi
284   fi
285 }
286
287 user_invite(){
288   local uid="$(timeid)"
289   local email="$(POST email |user_checkemail)"
290   local message="$(POST message)"
291
292   if [ ! "email" ]; then
293     REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
294   elif user_emailexist "$email"; then
295     REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
296   elif new_user "$uid" status=pending email="$email" expire="$((_DATE + 86400))"; then
297     debug "Sending Invitation Link:" \
298           "https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
299     "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
300         From: ${MAILFROM}
301         To: ${email}
302         Subject: You have been invited to ${HTTP_HOST%:*}
303
304         ${USER_NAME:-Someone} has offered an invitation to this email address.
305
306         ${message}
307
308         You can create your account using this link:
309
310             https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
311
312         This registration link will expire after 24 hours.
313
314         If you do not know what this is about, then someone else probably
315         entered your email address by accident. In this case you shoud
316         simply ignore this message and we will remove your email address from
317         our database within the next day.
318
319         This is an automatic email. Any direct reply will not be received.
320         Your Account Registration Robot.
321         EOF
322     REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
323   else
324     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
325   fi
326 }
327
328 user_confirm(){
329   # enable account
330   eval "$LOCAL_USER"
331   local uid="$(POST uid |checkid || printf invalid)"
332   local signature="$(POST signature)"
333   local uname="$(POST uname |user_checkname)"
334   local pwsalt="$(randomid)"
335   local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
336
337   read_user "${uid}"
338
339   if [ "$signature" != "$(session_mac "$uid")" ]; then
340     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
341   elif [ ! "$uname" ]; then
342     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID"
343   elif user_nameexist "$uname"; then
344     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS"
345   elif [ ! "$pw" ]; then
346     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
347   elif [ "$pw" != "$pwconfirm" ]; then
348     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
349   elif [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then
350     REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
351   elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then
352     SESSION_COOKIE new
353     SESSION_BIND user_id "$USER_ID"
354     if [ "$USER_ACCOUNTPAGE" ]; then
355       REDIRECT "${USER_ACCOUNTPAGE}"
356     else
357       REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
358     fi
359   else
360     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
361   fi
362 }
363
364 user_login(){
365   # set cookie
366   # keep logged in - device cookie?
367   # initialize new session!
368   local UID_    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
369   local uname="$(POST uname |STRING)" pw="$(POST pw)"
370
371   [ -f "$user_db" -a -r "$user_db" ] \
372   && while read -r UID_ UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
373     if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
374       if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
375         SESSION_COOKIE new
376         SESSION_BIND user_id "$UID_"
377         REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
378       fi
379     fi
380   done <"$user_db"
381   REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN"
382 }
383
384 user_logout(){
385   # destroy cookie, destroy session
386   # keep device cookie
387   new_session
388   SESSION_COOKIE new
389   SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly
390   REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT"
391 }
392
393 user_update(){
394   # todo: username update, email update / email confirm
395   local UID_    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
396   # local uname="$(POST uname |STRING)"
397   local uid oldpw pw pwconfirm
398
399         uid="$(POST uid)"
400       oldpw="$(POST oldpw)"
401          pw="$(POST pw |grep -xE '.{6}')"
402   pwconfirm="$(POST pwconfirm)"
403
404
405   read -r UID_  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
406         $(grep "^${uid} " "$user_db")
407         EOF
408
409   if [ "$UID_" = "$USER_ID" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$oldpw")" ]; then
410     if [ "$pw" -a "$pw" = "$pwconfirm" ]; then
411       update_user "${uid}" password="$pw"
412       REDIRECT "${_BASE}${PATH_INFO}#UPDATE_SUCCESS"
413     else
414       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PWMISMATCH"
415     fi
416   elif [ "$UID_" = "$USER_ID" ]; then
417     REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_AUTH_PASSWORD"
418   else
419     REDIRECT "${_BASE}${PATH_INFO}#ERROR_NOTLOGGEDIN"
420   fi
421 }
422
423 user_recover(){
424   # send recover link
425   :
426 }
427 user_disable(){
428   :
429 }
430
431 read_user "$(SESSION_VAR user_id)"
432 [ "$USER_STATUS" -a "$USER_STATUS" != active ] && eval $UNSET_USER
433
434 [ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
435   user_register) user_register ;;
436   user_confirm)  user_confirm ;;
437   user_invite)   user_invite ;;
438   user_login)    user_login ;;
439   user_logout)   user_logout ;;
440   user_update)   user_update ;;
441   user_recover)
442     :;;
443   user_disable)
444     :;;
445 esac
446
447 w_user_update(){
448   if [ ! "$USER_ID" ]; then
449     cat <<-EOF
450         [div #user_update .nouser
451         This page can only be used by registered users
452         ]
453         EOF
454   else
455     cat <<-EOF
456         [form #user_update method=POST
457           [hidden "uid" "$USER_ID"]
458           [p .username Logged in as $USER_NAME]
459           [input type=password name=oldpw placeholder="Current Passphrase"]
460           [input type=password name=pw placeholder="New Passphrase" pattern=".{6,}"]
461           [input type=password name=pwconfirm placeholder="Confirm New Passphrase" pattern=".{6,}"]
462           [submit "action" "user_update" Update Passphrase]
463         ]
464         EOF
465   fi
466 }
467
468 w_user_register(){
469   if [ "$(GET user_confirm)" ]; then
470     w_user_confirm
471   elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
472     cat <<-EOF
473         [div #user_register .disabled
474         User Registration is disabled.
475         ]
476         EOF
477   elif [ "$USER_REQUIREEMAIL" = true ]; then
478     cat <<-EOF
479         [form #user_register .registeremail method=POST
480           [p We will send an activation mail to your email address.
481             You can continue the signup process when you click on the
482             activation link in this email.]
483           [input type=email name=email placeholder="Email"]
484           [submit "action" "user_register" Sign Up]
485         ]
486         EOF
487   elif [ "$USER_REQUIREEMAIL" != true ]; then
488     cat <<-EOF
489         [form #user_register .registername method=POST
490           [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]
491           [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
492           [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
493           [submit "action" "user_register" Sign Up]
494         ]
495         EOF
496   fi
497 }
498
499 w_user_confirm(){
500   local UID_    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
501   local user_confirm="$(GET user_confirm)"
502   local uid="${user_confirm% *}" signature="${user_confirm#* }"
503
504   if [ "$signature" = "$(session_mac "$uid")" ]; then
505     read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
506         $(grep "^${uid} " "$user_db")
507         EOF
508     if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
509       cat <<-EOF
510         [form #user_confirm method=POST
511           [input type=hidden name=uid value="${uid}"]
512           [input type=hidden name=signature value="${signature}"]
513           $([ "$EMAIL" != '\' ] && printf \
514             '[input disabled=disabled value="%s" placeholder="Email"]' "$(UNSTRING "$EMAIL" |HTML)"
515           )
516           [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]
517           [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
518           [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
519           [submit "action" "user_confirm" Finish Registration]
520         ]
521         EOF
522     else
523       cat <<-EOF
524         [div #user_confirm .expired
525           [p This activation link is not valid anymore.]
526         ]
527         EOF
528     fi
529   else
530     cat <<-EOF
531         [div #user_confirm .invalid
532           [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.]
533         ]
534         EOF
535   fi
536 }
537
538 w_user_invite(){
539   if [ "$(GET user_confirm)" ]; then
540     w_user_confirm
541   elif [ "$USER_ID" -a "$SENDMAIL" ]; then
542     cat <<-EOF
543         [form #user_invite method=POST
544           [input placeholder="Email Recipient" name=email autocomplete=off]
545           [textarea name="message" placeholder="Message to recipient" . ]
546           [submit "action" "user_invite" Send Invitation]
547         ]
548         EOF
549   elif [ "$USER_ID" ]; then
550     uid="$(timeid)"
551     new_user "$uid" status=pending email="$email" expire="$((_DATE + 86400))"
552     cat <<-EOF
553         [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 24 hours.]
554         [p . $(HTML "https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")" |debug)]
555
556         [p [a href="#" . Set up another account]]
557         EOF
558   else
559     cat <<-EOF
560         [div #user_invite .notallowed
561           Only registered users may send an invitation to another user.
562         ]
563         EOF
564   fi
565 }
566
567 w_user_login(){
568   if [ ! "$USER_ID" ]; then
569     cat <<-EOF
570         [form #user_login .login method=POST
571           [input name=uname placeholder="Username or Email" autocomplete=off]
572           [input type=password name=pw placeholder="Passphrase"]
573           [submit "action" "user_login" Login]
574         ]
575         EOF
576   elif [ "$USER_ID" ]; then
577     cat <<-EOF
578         [form #user_login .logout method=POST
579           [p Logged in as [span . $(HTML ${USER_NAME})]]
580           [submit "action" "user_logout" Logout]
581         ]
582         EOF
583   fi
584 }