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