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