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