]> git.plutz.net Git - serve0/commitdiff
Squashed 'cgilite/' changes from 1d27862..6bdb2db
authorPaul Hänsch <paul@plutz.net>
Sun, 22 May 2022 18:13:13 +0000 (20:13 +0200)
committerPaul Hänsch <paul@plutz.net>
Sun, 22 May 2022 18:13:13 +0000 (20:13 +0200)
6bdb2db style for search button
7680549 variable expiration times, clickable invitation links
38314fd detect https/http schema for invite links
98d46bf export user variables
b3075fd allow email quicklinks, bugfix pattern extractor in all inline links
d4b1cb4 variable $UID is reserved in bash and cannot be used
49a67fe metadata blocks
b406efc avoid odd margins in list items
2092bc6 user passphrase update, improved username form
2f3c712 allow invitation without email, allow setting user page url
e5e180a "cgilite_headers" among export variables
6cc62de reset header variables when processing multiple requests
b2b268b corrected paragraph splitting and hr/h2 distinction
33cd660 faster hexdecode for mixed data (e.g. post-data)
6fe824f API CHANGE: do not set session cookie automatically
a8f5776 enable pandoc fenced divs, and fenced code attributes
fabbc00 make hr tag visible again
47295e6 bugfix: prevent HTML injection in reference style link titles
882f37d markdown support for external macro plugin

git-subtree-dir: cgilite
git-subtree-split: 6bdb2dbf2cc57ab1938def5b678cbfe669f2d3b2

cgilite.sh
common.css
markdown.awk
session.sh
users.sh

index 90d65577648d2d78efa7d349e90677984f87c436..b47a3e2e683219b773af4383059a6701f2d44d46 100755 (executable)
@@ -89,7 +89,8 @@ HEX_DECODE(){
       "$pfx"[0-9a-fA-F][0-9a-fA-F]*) in="${in#${pfx}}";;
       \\*) in="${in#?}"; out="${out}\\\\"; continue;;
        %*) in="${in#?}"; out="${out}%%";  continue;;
-        *) out="${out}${in%"${in#?}"}"; in="${in#?}"; continue;;
+        *) att="${in%%"${pfx}"*}"; att="${att%%%*}"; att="${att%%\\*}"
+           out="${out}${att}"; in="${in#"${att}"}"; continue;;
     esac;
 
     # Hex escapes for printf (e.g. \x41) are not portable 
@@ -143,6 +144,8 @@ if [ -z "$REQUEST_METHOD" ]; then
 
   (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$!
   while read REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL; do
+    unset PATH_INFO QUERY_STRING cgilite_headers CONTENT_LENGTH CONTENT_TYPE
+
     [ "${SERVER_PROTOCOL#HTTP/1.[01]${CR}}" ] && break
     kill $cgilite_watchdog
 
@@ -151,7 +154,7 @@ if [ -z "$REQUEST_METHOD" ]; then
     [ "${REQUEST_URI}" = "${REQUEST_URI#*\?}" ] \
     && QUERY_STRING='' \
     || QUERY_STRING="${REQUEST_URI#*\?}"
-    cgilite_headers=''; while read -r hl; do
+    while read -r hl; do
       hl="${hl%${CR}}"; [ "$hl" ] || break
       case $hl in
         'Content-Length: '*) CONTENT_LENGTH="${hl#*: }";;
@@ -161,7 +164,7 @@ if [ -z "$REQUEST_METHOD" ]; then
     done
 
     export REMOTE_ADDR SERVER_NAME SERVER_PORT REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL \
-           PATH_INFO QUERY_STRING CONTENT_TYPE CONTENT_LENGTH
+           PATH_INFO QUERY_STRING CONTENT_TYPE CONTENT_LENGTH cgilite_headers
 
     # Try to serve multiple requests, provided that script serves a
     # Content-Length header.
index 71bead6e26485069ff5c4cfc0ca17cd4a87bea69..c5cf6c443c355a7ef2a2219278173968e0aa6d61 100644 (file)
@@ -17,6 +17,7 @@ body {
 }
 
 ul, ol, dl, table, p { margin-bottom: .5em; }
+p:only-child { margin-bottom: 0; }
 
 a {
   font-style: italic;
@@ -44,6 +45,12 @@ ul, ol { margin-left: 1.125em; }
 dl dt { font-weight: bolder; }
 table th { font-weight: bold; }
 
+li p + ul, li p + ol {
+  margin-top: -.25em;
+}
+
+hr { border-bottom: 1pt solid; }
+
 h1, h2, h3 {
   font-weight: bold;
   margin-top: .75em;
@@ -88,6 +95,21 @@ input + label {
   margin-left: .375em;
 }
 
+input.search + button.search {
+  width: 2.5em;
+  color: transparent;
+  background-color: #CCC;
+  margin-left: -2pt;
+  border-left: none;
+  border-radius: 0 2pt 2pt 0;
+  white-space: nowrap;
+}
+input.search + button.search:before {
+  content: '\1f50d';
+  color: #000;
+  font-weight: bold;
+}
+
 @media print {
   @page { margin: 20mm; }
 
index 361e6005c3fc42fa5e1f81859d69ec59d0c0a138..e63541bebfa78ffc70053ca23a1d94ff6b586c66 100755 (executable)
@@ -4,10 +4,6 @@
 # EXPERIMENTAL Markdown processor with minimal dependencies.
 # Meant to support all features of John Grubers basic Markdown
 # + a number of common extensions, mostly inspired by Pandoc Markdown
-#
-# ToDo:
-# - HTML processing / escaping (according to environment flag)
-# - em-dashes and arrows
 
 # Supported Features / TODO:
 # ==========================
@@ -45,7 +41,7 @@
 # -  ?  Heading identifiers (php md, pandoc)
 # - [x] Automatic heading identifiers (custom)
 # - [x] Fenced code blocks (php md, pandoc)
-#   - [-] Fenced code attributes
+#   - [x] Fenced code attributes
 # - [ ] Tables
 #   -  ?  Simple table (pandoc)
 #   -  ?  Multiline table (pandoc)
@@ -56,7 +52,8 @@
 # - [ ] Definition lists (php md, pandoc)
 # - [-] Numbered example lists (pandoc)
 # - [-] Metadata blocks (pandoc)
-# - [-] Fenced Divs (pandoc)
+# - [x] Metadata blocks (custom)
+# - [x] Fenced Divs (pandoc)
 #
 # Extensions - Inline elements:
 # ----------------------------
@@ -126,12 +123,18 @@ function inline( line, LOCAL, len, code, href, guard ) {
     href = HTML( substr( line, 2, len - 2) );
     return "<a href=\"" href "\">" href "</a>" inline( substr( line, len + 1) );
 
+  # quick link email
+  } else if ( match( line, /^<[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])?)*>/ ) ) {
+    len = RLENGTH;
+    href = HTML( substr( line, 2, len - 2) );
+    return "<a href=\"mailto:" href "\">" href "</a>" inline( substr( line, len + 1) );
+
   # inline links
-  } else if ( match(line, /^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/) ) {
+  } else if ( match(line, /^\[([^]]+)\]\(([^"\)]+)([[:space:]]+"([^"]+)")?\)/) ) {
     len = RLENGTH;
-    text  = gensub(/^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\1", "g", line);
-    href  = gensub(/^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\2", "g", line);
-    title = gensub(/^\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\4", "g", line);
+    text  = gensub(/^\[([^]]+)\]\(([^"\)]+)([[:space:]]+"([^"]+)")?\)/, "\\1", 1, substr(line, 1, len) );
+    href  = gensub(/^\[([^]]+)\]\(([^"\)]+)([[:space:]]+"([^"]+)")?\)/, "\\2", 1, substr(line, 1, len) );
+    title = gensub(/^\[([^]]+)\]\(([^"\)]+)([[:space:]]+"([^"]+)")?\)/, "\\4", 1, substr(line, 1, len) );
     if ( title ) {
       return "<a href=\"" HTML(href) "\" title=\"" HTML(title) "\">" inline( text ) "</a>" inline( substr( line, len + 1) );
     } else {
@@ -141,23 +144,23 @@ function inline( line, LOCAL, len, code, href, guard ) {
   # reference style links
   } else if ( match(line, /^\[([^]]+)\] ?\[([^]]*)\]/ ) ) {
     len = RLENGTH;
-    text = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, line);
-      id = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, line);
+    text = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, substr(line, 1, len) );
+      id = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, substr(line, 1, len) );
     if ( ! id ) id = text;
     if ( rl_href[id] && rl_title[id] ) {
-      return "<a href=\"" rl_href[id] "\" title=\"" rl_title[id] "\">" inline(text) "</a>" inline( substr( line, len + 1) );
+      return "<a href=\"" HTML(rl_href[id]) "\" title=\"" HTML(rl_title[id]) "\">" inline(text) "</a>" inline( substr( line, len + 1) );
     } else if ( rl_href[id] ) {
-      return "<a href=\"" rl_href[id] "\">" inline(text) "</a>" inline( substr( line, len + 1) );
+      return "<a href=\"" HTML(rl_href[id]) "\">" inline(text) "</a>" inline( substr( line, len + 1) );
     } else {
-      return "" substr(line, 1, len) inline( substr(line, len + 1) );
+      return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) );
     }
 
   # inline images
   } else if ( match(line, /^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/) ) {
     len = RLENGTH;
-    text  = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\1", "g", line);
-    href  = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\2", "g", line);
-    title = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\4", "g", line);
+    text  = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\1", "g", substr(line, 1, len) );
+    href  = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\2", "g", substr(line, 1, len) );
+    title = gensub(/^!\[([^]]+)\]\(([^"\)]+)([ \t]+"([^"]+)")?\)/, "\\4", "g", substr(line, 1, len) );
     if ( title ) {
       return "<img src=\"" HTML(href) "\" alt=\"" HTML(text) "\" title=\"" HTML(title) "\" />" inline( substr( line, len + 1) );
     } else {
@@ -167,15 +170,15 @@ function inline( line, LOCAL, len, code, href, guard ) {
   # reference style images
   } else if ( match(line, /^!\[([^]]+)\] ?\[([^]]*)\]/ ) ) {
     len = RLENGTH;
-    text = gensub(/^!\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, line);
-      id = gensub(/^!\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, line);
+    text = gensub(/^!\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, substr(line, 1, len) );
+      id = gensub(/^!\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, substr(line, 1, len) );
     if ( ! id ) id = text;
     if ( rl_href[id] && rl_title[id] ) {
-      return "<img src=\"" rl_href[id] "\" alt=\"" HTML(text) "\" title=\"" rl_title[id] "\" />" inline( substr( line, len + 1) );
+      return "<img src=\"" HTML(rl_href[id]) "\" alt=\"" HTML(text) "\" title=\"" HTML(rl_title[id]) "\" />" inline( substr( line, len + 1) );
     } else if ( rl_href[id] ) {
-      return "<img src=\"" rl_href[id] "\" alt=\"" HTML(text) "\" />" inline( substr( line, len + 1) );
+      return "<img src=\"" HTML(rl_href[id]) "\" alt=\"" HTML(text) "\" />" inline( substr( line, len + 1) );
     } else {
-      return "" substr(line, 1, len) inline( substr(line, len + 1) );
+      return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) );
     }
 
   #  ~~strikeout~~ (pandoc)
@@ -195,7 +198,7 @@ function inline( line, LOCAL, len, code, href, guard ) {
 
   # ignore embedded underscores (pandoc, php md)
   } else if ( match(line, "^[[:alnum:]](__|_)") ) {
-    return substr( line, 1, RLENGTH) inline( substr(line, RLENGTH + 1) );
+    return HTML(substr( line, 1, RLENGTH)) inline( substr(line, RLENGTH + 1) );
 
   #  __strong__$
   } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__$") ) {
@@ -227,6 +230,11 @@ function inline( line, LOCAL, len, code, href, guard ) {
     len = RLENGTH;
     return "<em>" inline( substr( line, 2, len - 2 ) ) "</em>" inline( substr( line, len + 1 ) );
 
+  # Macros
+  } else if ( AllowMacros && match( line, /^<<([^>]|>[^>])+>>/) ) {
+    len = RLENGTH;
+    return macro( substr( line, 3, len - 4 ) ) inline(substr(line, len + 1));
+
   # Verbatim inline HTML
   } else if ( AllowHTML && match( line, /^(<!--([^-]|-[^-]|--[^>])*-->|<\?([^\?]|\?[^>])*\?>|<![A-Z][^>]*>|<!\[CDATA\[([^\]]|\][^\]]|\]\][^>])*\]\]>|<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)/) ) {
     len = RLENGTH;
@@ -247,7 +255,7 @@ function inline( line, LOCAL, len, code, href, guard ) {
   }
 }
 
-function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) {
+function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib ) {
   gsub( /^\n+|\n+$/, "", block );
 
   if ( block == "" ) {
@@ -274,6 +282,12 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) {
   } else if ( AllowHTML && match( block, /^ ? ? ?(<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)([[:space:]]*\n)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) {
     len = RLENGTH; st = RSTART;
     return substr(block, st, len) _block(substr(block, st + len));
+
+  # Metadata (custom, block starting with %something)
+  # Metadata is ignored but can be interpreted externally
+  } else if ( match(block, /^%[a-zA-Z]+([[:space:]][^\n]*)?(\n|$)(%[a-zA-Z]+([[:space:]][^\n]*)?(\n|$)|%([[:space:]][^\n]*)?(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) {
+    len = RLENGTH; st = RSTART;
+    return  _block( substr( block, len + 1) );
  
   # Blockquote (leading >)
   } else if ( match( block, /^> /) ) {
@@ -301,13 +315,34 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) {
     return "<pre><code>" HTML( code ) "</code></pre>\n" \
            _block( substr( block, len + 1 ) );
 
+  # Fenced Divs (pandoc, custom)
+  } else if ( match( block, /^(:::+)/ ) ) {
+    guard = substr( block, 1, RLENGTH );
+    code = gensub(/^[^\n]+\n/, "", 1, block);
+    attrib = gensub(/^:::+[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\1", 1, block);
+    gsub(/[^a-zA-Z0-9_-]+/, " ", attrib);
+    gsub(/(^ | $)/, "", attrib);
+    if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) {
+      len = RLENGTH; st = RSTART;
+      return "<div class=\"" attrib "\">" _block( substr(code, 1, st - 1) ) "</div>\n" \
+             _block( substr( code, st + len ) );
+    } else {
+      match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ );
+      len = RLENGTH; st = RSTART;
+      return "<p>" inline( substr(block, 1, st - 1) ) "</p>\n" \
+             _block( substr(block, st + len) );
+    }
+
   # Fenced Code Block (pandoc)
   } else if ( match( block, /^(~~~+|```+)/ ) ) {
     guard = substr( block, 1, RLENGTH );
     code = gensub(/^[^\n]+\n/, "", 1, block);
+    attrib = gensub(/^:::+[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\1", 1, block);
+    gsub(/[^a-zA-Z0-9_-]+/, " ", attrib);
+    gsub(/(^ | $)/, "", attrib);
     if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) {
       len = RLENGTH; st = RSTART;
-      return "<pre><code>" HTML( substr(code, 1, st - 1) ) "</code></pre>\n" \
+      return "<pre><code class=\"" attrib "\">" HTML( substr(code, 1, st - 1) ) "</code></pre>\n" \
              _block( substr( code, st + len ) );
     } else {
       match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ );
@@ -364,6 +399,12 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) {
     return "<h" hlvl " id=\"" hid " - " HTML(htxt) "\">" inline( htxt ) "</h" hlvl ">\n\n" \
            _block( substr( block, len + 1) );
 
+  # Split paragraphs
+  } else if ( match( block, /(^|\n)[[:space:]]*(\n|$)/) ) {
+    len = RLENGTH; st = RSTART;
+    return _block( substr(block, 1, st - 1) ) "\n" \
+           _block( substr(block, st + len) );
+
   # Horizontal rule
   } else if ( match( block, /(^|\n) ? ? ?((\* *){3,}|(- *){3,}|(_ *){3,})($|\n)/) ) {
     len = RLENGTH; st = RSTART;
@@ -371,10 +412,7 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) {
 
   # Plain paragraph
   } else {
-    match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ );
-    len = RLENGTH; st = RSTART;
-    return "<p>" inline( substr(block, 1, st - 1) ) "</p>\n" \
-           _block( substr(block, st + len) );
+    return "<p>" inline(block) "</p>\n";
   }
 }
 
index 8fb623639c14b372c7c88b98695b8edca24b1943..1f4699e441357303b4dd4b6e4daeff3af1bb47c8 100755 (executable)
@@ -136,5 +136,3 @@ SESSION_COOKIE() {
 }
 
 update_session || new_session
-
-[ "$1" = nocookie ] || SESSION_COOKIE
index 873edf05ffbb72fa9729ff1eb7792c39ac2a86c3..6a6833e96c592043669006bbeec91b628a70667e 100755 (executable)
--- a/users.sh
+++ b/users.sh
@@ -6,11 +6,20 @@ include_users="$0"
 . "${_EXEC}/cgilite/session.sh"
 . "${_EXEC}/cgilite/storage.sh"
 
-USER_REGISTRATION="${USER_REGISTRATION:-true}"
-USER_REQUIREEMAIL="${USER_REQUIREEMAIL:-true}"
+SENDMAIL=${SENDMAIL-sendmail}
+
+USER_REGISTRATION="${USER_REGISTRATION-true}"
+USER_REQUIREEMAIL="${USER_REQUIREEMAIL-true}"
+USER_ACCOUNTPAGE="${USER_ACCOUNTPAGE}"
+
+USER_ACCOUNTEXPIRE="${USER_ACCOUNTEXPIRE:-$((86400 * 730))}"
+USER_CONFIRMEXPIRE="${USER_CONFIRMEXPIRE:-86400}"
+
+MAILFROM="${MAILDOMAIN-noreply@${HTTP_HOST%:*}}"
 
 HTTP_HOST="$(HEADER Host)"
-MAILFROM="${MAILDOMAIN:-noreply@${HTTP_HOST%:*}}"
+
+[ "$HTTPS" ] && SCHEMA=https || SCHEMA=http
 
 # == FILE FORMAT ==
 # UID  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
@@ -62,7 +71,7 @@ read_user() {
 update_user() {
   # internal function for user update
   local uid="$1" uname status email pwsalt pwhash expire devices futureuse
-  local UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
+  local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
   local arg
 
   for arg in "$@"; do case $arg in
@@ -75,15 +84,15 @@ update_user() {
   esac; done
 
   if LOCK "$user_db"; then
-    while read -r UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
+    while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
                   FUTUREUSE; do
-    if [ "$UID" = "$uid" ]; then
+    if [ "$UID_" = "$uid" ]; then
       printf '%s       %s      %s      %s      %s      %s      %i      %s      %s\n' \
              "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \
              "${status:-${status-${STATUS}}${status+\\}}" \
              "${email:-${email-${EMAIL}}${email+\\}}" \
              "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \
-             "${expire:-$((_DATE + 86400 * 730))}" \
+             "${expire:-$((_DATE + USER_ACCOUNTEXPIRE))}" \
              "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \
              "${FUTUREUSE:-\\}"
     elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then
@@ -91,7 +100,7 @@ update_user() {
       :
     else
       printf '%s       %s      %s      %s      %s      %s      %i      %s      %s\n' \
-             "$UID" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
+             "$UID_" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
              "$EXPIRE" "$DEVICES" "$FUTUREUSE"
     fi
     done <"$user_db" >"${user_db}.$$"
@@ -112,7 +121,7 @@ new_user(){
       return 1
     fi
     printf '%s \\      %s      \\      \\      \\      %i      \\      \\\n' \
-           "$user" "pending" "$(( $_DATE + 86400 ))" >>"$user_db"
+           "$user" "pending" "$(( _DATE + USER_CONFIRMEXPIRE ))" >>"$user_db"
   else
     return 1
   fi
@@ -185,9 +194,9 @@ user_checkemail(){
 
 user_nameexist(){
   local uname="$(STRING "$1")"
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
   [ -f "$user_db" -a -r "$user_db" ] \
-  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
     [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
   done <"$user_db"
   return 1
@@ -195,9 +204,9 @@ user_nameexist(){
 
 user_emailexist(){
   local email="$(STRING "$1")"
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
   [ -f "$user_db" -a -r "$user_db" ] \
-  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
     [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
   done <"$user_db"
   return 1
@@ -227,10 +236,10 @@ user_register(){
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
     elif user_emailexist "$email"; then
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
-    elif new_user "$uid" status=pending email="$email" expire="$((_DATE + 86400))"; then
+    elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
       debug "Sending Activation Link:" \
-            "https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
-      sendmail -t -f "$MAILFROM" <<-EOF
+            "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+      "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
        From: ${MAILFROM}
        To: ${email}
        Subject: Your account registration at ${HTTP_HOST%:*}
@@ -239,9 +248,9 @@ user_register(){
 
        You can activate your account using this link:
 
-           https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+           ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
 
-       This registration link will expire after 24 hours.
+       This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
 
        If you did not request an account at ${HTTP_HOST%:*}, then someone else
        probably entered your email address by accident. In this case you shoud
@@ -265,11 +274,15 @@ user_register(){
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
     elif [ "$pw" != "$pwconfirm" ]; then
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
-    elif new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + 86400 * 730))"; then
+    elif new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + USER_ACCOUNTEXPIRE))"; then
       SESSION_COOKIE new
       SESSION_BIND user_id "$uid"
 
-      REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+      if [ "$USER_ACCOUNTPAGE" ]; then
+        REDIRECT "${USER_ACCOUNTPAGE}"
+      else
+        REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+      fi
     else
       REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
     fi
@@ -285,10 +298,10 @@ user_invite(){
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
   elif user_emailexist "$email"; then
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
-  elif new_user "$uid" status=pending email="$email" expire="$((_DATE + 86400))"; then
+  elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
     debug "Sending Invitation Link:" \
-          "https://${HTTP_HOST}${BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
-    sendmail -t -f "$MAILFROM" <<-EOF
+          "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+    "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
        From: ${MAILFROM}
        To: ${email}
        Subject: You have been invited to ${HTTP_HOST%:*}
@@ -299,9 +312,9 @@ user_invite(){
 
        You can create your account using this link:
 
-           https://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+           ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
 
-       This registration link will expire after 24 hours.
+       This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
 
        If you do not know what this is about, then someone else probably
        entered your email address by accident. In this case you shoud
@@ -343,7 +356,11 @@ user_confirm(){
   elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then
     SESSION_COOKIE new
     SESSION_BIND user_id "$USER_ID"
-    REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
+    if [ "$USER_ACCOUNTPAGE" ]; then
+      REDIRECT "${USER_ACCOUNTPAGE}"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
+    fi
   else
     REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
   fi
@@ -353,15 +370,15 @@ user_login(){
   # set cookie
   # keep logged in - device cookie?
   # initialize new session!
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
   local uname="$(POST uname |STRING)" pw="$(POST pw)"
 
   [ -f "$user_db" -a -r "$user_db" ] \
-  && while read -r UID UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
     if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
       if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
         SESSION_COOKIE new
-        SESSION_BIND user_id "$UID"
+        SESSION_BIND user_id "$UID_"
         REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
       fi
     fi
@@ -379,9 +396,35 @@ user_logout(){
 }
 
 user_update(){
-  # passphrase, email
-  :
+  # todo: username update, email update / email confirm
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  # local uname="$(POST uname |STRING)"
+  local uid oldpw pw pwconfirm
+
+        uid="$(POST uid)"
+      oldpw="$(POST oldpw)"
+         pw="$(POST pw |grep -xE '.{6}')"
+  pwconfirm="$(POST pwconfirm)"
+
+
+  read -r UID_ UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${uid} " "$user_db")
+       EOF
+
+  if [ "$UID_" = "$USER_ID" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$oldpw")" ]; then
+    if [ "$pw" -a "$pw" = "$pwconfirm" ]; then
+      update_user "${uid}" password="$pw"
+      REDIRECT "${_BASE}${PATH_INFO}#UPDATE_SUCCESS"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_PWMISMATCH"
+    fi
+  elif [ "$UID_" = "$USER_ID" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_AUTH_PASSWORD"
+  else
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_NOTLOGGEDIN"
+  fi
 }
+
 user_recover(){
   # send recover link
   :
@@ -399,14 +442,38 @@ read_user "$(SESSION_VAR user_id)"
   user_invite)   user_invite ;;
   user_login)    user_login ;;
   user_logout)   user_logout ;;
-  user_update)
-    :;;
+  user_update)   user_update ;;
   user_recover)
     :;;
   user_disable)
     :;;
 esac
 
+export USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+       USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+
+
+w_user_update(){
+  if [ ! "$USER_ID" ]; then
+    cat <<-EOF
+       [div #user_update .nouser
+       This page can only be used by registered users
+       ]
+       EOF
+  else
+    cat <<-EOF
+       [form #user_update method=POST
+         [hidden "uid" "$USER_ID"]
+         [p .username Logged in as $USER_NAME]
+         [input type=password name=oldpw placeholder="Current Passphrase"]
+         [input type=password name=pw placeholder="New Passphrase" pattern=".{6,}"]
+         [input type=password name=pwconfirm placeholder="Confirm New Passphrase" pattern=".{6,}"]
+         [submit "action" "user_update" Update Passphrase]
+       ]
+       EOF
+  fi
+}
+
 w_user_register(){
   if [ "$(GET user_confirm)" ]; then
     w_user_confirm
@@ -429,7 +496,7 @@ w_user_register(){
   elif [ "$USER_REQUIREEMAIL" != true ]; then
     cat <<-EOF
        [form #user_register .registername method=POST
-          [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="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off]
+          [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]
          [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
          [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
          [submit "action" "user_register" Sign Up]
@@ -439,12 +506,12 @@ w_user_register(){
 }
 
 w_user_confirm(){
-  local UID    UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
   local user_confirm="$(GET user_confirm)"
   local uid="${user_confirm% *}" signature="${user_confirm#* }"
 
   if [ "$signature" = "$(session_mac "$uid")" ]; then
-    read -r UID        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+    read -r UID_       UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
        $(grep "^${uid} " "$user_db")
        EOF
     if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
@@ -452,8 +519,10 @@ w_user_confirm(){
        [form #user_confirm method=POST
          [input type=hidden name=uid value="${uid}"]
          [input type=hidden name=signature value="${signature}"]
-         [input disabled=disabled value="$(HTML "$EMAIL")"]
-          [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="^\[a-zA-Z\]\[a-zA-Z0-9 -~\]{2,127}$" autocomplete=off]
+         $([ "$EMAIL" != '\' ] && printf \
+           '[input disabled=disabled value="%s" placeholder="Email"]' "$(UNSTRING "$EMAIL" |HTML)"
+         )
+          [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]
          [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
          [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
          [submit "action" "user_confirm" Finish Registration]
@@ -476,9 +545,11 @@ w_user_confirm(){
 }
 
 w_user_invite(){
+  local uid invlink
+
   if [ "$(GET user_confirm)" ]; then
     w_user_confirm
-  elif [ "$USER_ID" ]; then
+  elif [ "$USER_ID" -a "$SENDMAIL" ]; then
     cat <<-EOF
        [form #user_invite method=POST
          [input placeholder="Email Recipient" name=email autocomplete=off]
@@ -486,6 +557,19 @@ w_user_invite(){
          [submit "action" "user_invite" Send Invitation]
        ]
        EOF
+  elif [ "$USER_ID" ]; then
+    uid="$(timeid)"
+    new_user "$uid" status=pending expire="$((_DATE + USER_CONFIRMEXPIRE))"
+    invlink="${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+    debug "New Invitation Link: $invlink"
+    cat <<-EOF
+       [div #user_invite .link
+          [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.]
+          [a href="$(HTML "$invlink")" . $(HTML "$invlink")]
+
+          [p [a href="#" . Set up another account]]
+       ]
+       EOF
   else
     cat <<-EOF
        [div #user_invite .notallowed