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