From: Paul Hänsch Date: Wed, 27 Jul 2022 10:58:14 +0000 (+0200) Subject: Squashed 'cgilite/' changes from a76f6a5..5038774 X-Git-Url: https://git.plutz.net/?p=webpoll;a=commitdiff_plain;h=2a7e36f6052d9a0a3d7bc83758f35437222343d0 Squashed 'cgilite/' changes from a76f6a5..5038774 5038774 escape CR and BR in HTML output (as previously specified) e02243e table style 74f16aa bugfix: allow trailing white space in indented code 175ea96 bugfix anchor links starting with # character dfadf30 bugfix: prevent white space lines from becoming code blocks e619859 anchor links for headlines, bugfix: continue block processing right after tables aa80431 Implemented pandoc grid tables 9bb2256 Implemented Pipe Tables d1bb79c bugfix in recognition of fenced code block attributes d09c1c1 ordered list of mime types, additional pdf and text types cc4a446 styling classes for task list, additional task list status 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 1d27862 bugfix URL escaping for ? and % 6147b0e faster HTML and URL functions b191eb8 export application globals dba2d39 idmap functions f477dc5 better data-layer / UI-layer abstraction in user functions 5a44f82 allow server site message page to confirm registration 24df501 perform _BASE striping outside of internal web server 343a22a cleaner display of activation link, include port number in activation link f6fa7fb strip _BASE path from PATH_INFO variable 6c1784b user invite function, handle invite/registration expire, always allow registration of first user af27357 bugfix tooltips 7459611 improved markup for styling 9451cdd min-height for textarea 84a16dd unambiguous cookie path when destroying user session 6bfa64b automatically swap in confirmation dialog for registration 5d5fc0f fix in email syntax and confirm path d468e35 ignore automatic files from modules 5a714a2 syntax fixes, minor sanity checks 142f5b0 user account functions d6e0c1a function new_session to force session update, limit session cookies to _BASE path git-subtree-dir: cgilite git-subtree-split: 5038774f89b33ecd03e407898e9d57d38245b5bb --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c9950a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +cgilite +serverkey +users.db diff --git a/cgilite.sh b/cgilite.sh index a2aa329..2193e29 100755 --- a/cgilite.sh +++ b/cgilite.sh @@ -48,7 +48,9 @@ _EXEC="${_EXEC:-${0%/*}}" _DATA="${_DATA:-.}" _EXEC="${_EXEC%/}" _DATA="${_DATA%/}" _BASE="${_BASE%/}" -# Carriare Return and Line Break characters for convenience +export _EXEC _DATA _BASE + +# Carriage Return and Line Break characters for convenience CR=" " BR=' ' @@ -87,10 +89,11 @@ 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 escaes for printf (e.g. \x41) are not portable + # Hex escapes for printf (e.g. \x41) are not portable # The portable way for Hex output is transforming Hex to Octal # (e.g. \x41 = \101) case $in in @@ -141,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 @@ -149,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#*: }";; @@ -159,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. @@ -195,6 +200,8 @@ if [ "${REQUEST_METHOD}" = POST -a "${CONTENT_LENGTH:-0}" -gt 0 -a \ cgilite_post="$(head -c "$CONTENT_LENGTH")" fi +PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")" + debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } [ "${DEBUG+x}" ] && env >&2 @@ -277,21 +284,18 @@ HTML(){ # Also escape [, ], and \n for use in html-sh local str out [ $# -eq 0 ] && str="$(cat)" || str="$*" - while [ "$str" ]; do - case $str in - \&*) out="${out}&";; - \<*) out="${out}<";; - \>*) out="${out}>";; - \"*) out="${out}"";; - \'*) out="${out}'";; - \[*) out="${out}[";; - \]*) out="${out}]";; - "${CR}"*) out="${out} ";; - "${BR}"*) out="${out} ";; - *) out="${out}${str%"${str#?}"}";; - esac - str="${str#?}" - done + while [ "$str" ]; do case $str in + \&*) out="${out}&"; str="${str#?}";; + \<*) out="${out}<"; str="${str#?}";; + \>*) out="${out}>"; str="${str#?}";; + \"*) out="${out}""; str="${str#?}";; + \'*) out="${out}'"; str="${str#?}";; + \[*) out="${out}["; str="${str#?}";; + \]*) out="${out}]"; str="${str#?}";; + "${CR}"*) out="${out} "; str="${str#?}";; + "${BR}"*) out="${out} "; str="${str#?}";; + *) out="${out}${str%%[]&<>\"\'${CR}${BR}[]*}"; str="${str#"${str%%[]&<>\"\'${CR}${BR}[]*}"}";; + esac; done printf %s "$out" } @@ -299,24 +303,21 @@ URL(){ # Escape pathes, so they can be used in link tags and HTTP Headers local str out [ $# -eq 0 ] && str="$(cat)" || str="$*" - while [ "$str" ]; do - case $str in - \&*) out="${out}%26";; - \"*) out="${out}%22";; - \'*) out="${out}%27";; - \?*) out="${out}%3F";; - \#*) out="${out}%23";; - \[*) out="${out}%5B";; - \]*) out="${out}%5D";; - \ *) out="${out}%20";; - " "*) out="${out}%09";; - "${CR}"*) out="${out}%0D";; - "${BR}"*) out="${out}%0A";; - %*) out="${out}%25";; - *) out="${out}${str%"${str#?}"}";; - esac - str="${str#?}" - done + while [ "$str" ]; do case $str in + \&*) out="${out}%26"; str="${str#?}";; + \"*) out="${out}%22"; str="${str#?}";; + \'*) out="${out}%27"; str="${str#?}";; + \?*) out="${out}%3F"; str="${str#?}";; + \#*) out="${out}%23"; str="${str#?}";; + \[*) out="${out}%5B"; str="${str#?}";; + \]*) out="${out}%5D"; str="${str#?}";; + \ *) out="${out}%20"; str="${str#?}";; + " "*) out="${out}%09"; str="${str#?}";; + "${CR}"*) out="${out}%0D"; str="${str#?}";; + "${BR}"*) out="${out}%0A"; str="${str#?}";; + %*) out="${out}%25"; str="${str#?}";; + *) out="${out}${str%%[]&\"\'\?# ${CR}${BR}%[]*}"; str="${str#"${str%%[]&\"\'\?# ${CR}${BR}%[]*}"}";; + esac; done printf %s "$out" } diff --git a/common.css b/common.css index f9b17ad..65c28f3 100644 --- a/common.css +++ b/common.css @@ -16,7 +16,14 @@ body { color: #000; background: #FFF; } -ul, ol, dl, table, p { margin-bottom: .5em; } +ul, ol, dl, table, pre, p { margin-bottom: .5em; } +p:only-child { margin-bottom: 0; } + +table { + max-width: 100%; + overflow-x: auto; +} +th, td { padding: .25em .75em; } a { font-style: italic; @@ -44,6 +51,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; @@ -68,6 +81,7 @@ select, input, button, textarea, a.button { border-radius: 2pt; } select { padding: .375em 0; } +textarea { min-height: 7em; } input[type=radio], input[type=checkbox] { vertical-align: baseline; @@ -87,6 +101,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; } @@ -108,7 +137,8 @@ input + label { *[tooltip]:hover:after { display: block; position: absolute; - bottom: -100%; left: 50%; transform: translate(-50%, 0); + min-width: 12em; + bottom: 100%; left: 50%; transform: translate(-50%, 0); content: attr(tooltip); padding: .5em; color: #000; background-color: #FFC; diff --git a/file.sh b/file.sh index 6f956df..0d1f4ea 100755 --- a/file.sh +++ b/file.sh @@ -22,24 +22,27 @@ include_fileserve="$0" file_type(){ case ${1##*.} in - html|html) printf 'text/html';; css) printf 'text/css';; - js) printf 'text/javascript';; - txt) printf 'text/plain';; - sh) printf 'text/shellscript';; + gif) printf 'image/gif';; + html|html) printf 'text/html';; jpg|jpeg) printf 'image/jpeg';; + js) printf 'text/javascript';; + m3u8) printf 'application/x-mpegURL';; + m4a) printf 'audio/mp4';; + m4s) printf 'video/iso.segment';; + m4v|mp4) printf 'video/mp4';; + mpd) printf 'application/dash+xml';; + ogg) printf 'audio/ogg';; + pdf) printf 'application/pdf';; png) printf 'image/png';; + sh) printf 'text/x-shellscript';; svg) printf 'image/svg+xml';; - gif) printf 'image/gif';; + tex) printf 'text/x-tex';; + txt) printf 'text/plain';; + short) printf 'text/prs.shorthand';; + ts) printf 'video/MP2T';; webm) printf 'video/webm';; - mp4|m4v) printf 'video/mp4';; - m4a) printf 'audio/mp4';; - ogg) printf 'audio/ogg';; xml) printf 'application/xml';; - m3u8) printf 'application/x-mpegURL';; - ts) printf 'video/MP2T';; - mpd) printf 'application/dash+xml';; - m4s) printf 'video/iso.segment';; *) printf 'application/octet-stream';; esac } diff --git a/markdown.awk b/markdown.awk index 361e600..ccd4cde 100755 --- a/markdown.awk +++ b/markdown.awk @@ -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,18 +41,19 @@ # - ? Heading identifiers (php md, pandoc) # - [x] Automatic heading identifiers (custom) # - [x] Fenced code blocks (php md, pandoc) -# - [-] Fenced code attributes -# - [ ] Tables +# - [x] Fenced code attributes +# - [/] Tables # - ? Simple table (pandoc) # - ? Multiline table (pandoc) -# - ? Grid table (pandoc) -# - ? Pipe table (php md pandoc) +# - [x] Grid table (pandoc) +# - [x] Pipe table (php md pandoc) # - [x] Line blocks (pandoc) -# - [x] Task lists (pandoc) +# - [x] Task lists (pandoc, custom) # - [ ] 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 "" href "" 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 "" href "" 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 "" inline( text ) "" 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 "" inline(text) "" inline( substr( line, len + 1) ); + return "" inline(text) "" inline( substr( line, len + 1) ); } else if ( rl_href[id] ) { - return "" inline(text) "" inline( substr( line, len + 1) ); + return "" inline(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) ); } # 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 "\""" 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 "\""" inline( substr( line, len + 1) ); + return "\""" inline( substr( line, len + 1) ); } else if ( rl_href[id] ) { - return "\""" inline( substr( line, len + 1) ); + return "\""" 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 "" inline( substr( line, 2, len - 2 ) ) "" 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-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, /^> /) ) { @@ -282,6 +296,115 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) { return "
\n" _block( gensub( /(^|\n)> /, "\n", "g", substr(block, 1, st - 1) ) ) "
\n\n" \ _block( substr(block, st + len) ); + # Pipe Tables (pandoc / php md / gfm ) + } else if ( match(block, "^((\\|)?([^\n]+\\|)+[^\n]+(\\|)?)\n" \ + "((\\|)?:?(-+:?[\\|+])+:?-+:?(\\|)?)\n" \ + "((\\|)?([^\n]+\\|)+[^\n]+(\\|)?(\n|$))+" ) ) { + len = RLENGTH; st = RSTART; + #initialize empty arrays + split("", talign); split("", tarray); + cols = 0; cnt=0; ttext = ""; + + # table header and alignment + split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tarray, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + cols = split( \ + gensub( /(^\||\|$)/, "", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + ), talign, /[+\|]/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + + for( cnt = 1; cnt < cols; cnt++ ) { + if (match(talign[cnt], /:-+:/)) talign[cnt]="center"; + else if (match(talign[cnt], /-+:/)) talign[cnt]="right"; + else if (match(talign[cnt], /:-+/)) talign[cnt]="left"; + else talign[cnt]=""; + } + + ttext = "\n" + for (cnt = 1; cnt < cols; cnt++) + ttext = ttext "" inline(tarray[cnt]) "" + ttext = ttext "\n\n" + + while ( match(block, "^((\\|)?([^\n]+\\|)+[^\n]+(\\|)?(\n|$))+" ) ){ + split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tarray, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + + ttext = ttext "" + for (cnt = 1; cnt < cols; cnt++) + ttext = ttext "" inline(tarray[cnt]) "" + ttext = ttext "\n" + } + return "" ttext "
\n" _block(block); + + # Grid Tables (pandoc) + } else if ( match(block, "^\\+(-+\\+)+\n" \ + "(\\|([^\n]+\\|)+\n)+" \ + "\\+(:?=+:?\\+)+\n" \ + "((\\|([^\n]+\\|)+\n)+" \ + "\\+(-+\\+)+(\n|$))+" \ + ) ) { + len = RLENGTH; st = RSTART; + #initialize empty arrays + split("", talign); split("", tarray); split("", tread); + cols = 0; cnt=0; ttext = ""; + + # table header and alignment + block = substr(block, match(block, /(\n|$)/) + 1 ); + while ( match(block, "^\\|([^\n]+\\|)+\n") ) { + cols = split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tread, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + for (cnt = 1; cnt < cols; cnt++) + tarray[cnt] = tarray[cnt] "\n" tread[cnt]; + } + + cols = split( \ + gensub( /(^\+|\+$)/, "", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + ), talign, /\+/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + + for (cnt = 1; cnt < cols; cnt++) { + if (match(talign[cnt], /:=+:/)) talign[cnt]="center"; + else if (match(talign[cnt], /=+:/)) talign[cnt]="right"; + else if (match(talign[cnt], /:=+/ )) talign[cnt]="left"; + else talign[cnt]=""; + } + + ttext = "\n" + for (cnt = 1; cnt < cols; cnt++) + ttext = ttext "" _block(tarray[cnt]) "" + ttext = ttext "\n\n" + + while ( match(block, /^((\|([^\n]+\|)+\n)+\+(-+\+)+(\n|$))+/ ) ){ + split("", tarray); + while ( match(block, /^\|([^\n]+\|)+\n/) ) { + split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tread, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + for (cnt = 1; cnt < cols; cnt++) + tarray[cnt] = tarray[cnt] "\n" tread[cnt]; + } + block = substr(block, match(block, /(\n|$)/) + 1 ); + + ttext = ttext "" + for (cnt = 1; cnt < cols; cnt++) + ttext = ttext "" _block(tarray[cnt]) "" + ttext = ttext "\n" + } + return "" ttext "
\n" _block(block); + # Line Blocks (pandoc) } else if ( match(block, /^\| [^\n]*(\n|$)(\| [^\n]*(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) { len = RLENGTH; st = RSTART; @@ -293,7 +416,7 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) { _block( substr( block, len + 1) ); # Indented Code Block - } else if ( match(block, /^( |\t)[^\n]+(\n|$)(( |\t)[^\n]+(\n|$)|[ \t]*(\n|$))*/) ) { + } else if ( match(block, /^( |\t)( *\t*[^ \t\n]+ *\t*)+(\n|$)(( |\t)[^\n]+(\n|$)|[ \t]*(\n|$))*/) ) { len = RLENGTH; st = RSTART; code = substr(block, 1, len); gsub(/(^|\n)( |\t)/, "\n", code); @@ -301,13 +424,34 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) { return "
" HTML( code ) "
\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 "
" _block( substr(code, 1, st - 1) ) "
\n" \ + _block( substr( code, st + len ) ); + } else { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ ); + len = RLENGTH; st = RSTART; + return "

" inline( substr(block, 1, st - 1) ) "

\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.*$/, "\\2", 1, block); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); + gsub(/(^ | $)/, "", attrib); if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) { len = RLENGTH; st = RSTART; - return "
" HTML( substr(code, 1, st - 1) ) "
\n" \ + return "
" HTML( substr(code, 1, st - 1) ) "
\n" \ _block( substr( code, st + len ) ); } else { match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ ); @@ -344,14 +488,20 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) { } else if ( match( block, /^[^\n]+\n===+(\n|$)/ ) ) { len = RLENGTH; HL[1]++; HL[2] = 0; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0; - return "

" inline( gensub( /\n.*$/, "", "g", block ) ) "

\n\n" \ + return "

" \ + inline( gensub( /\n.*$/, "", "g", block ) ) \ + "

\n\n" \ _block( substr( block, len + 1 ) ); # Second Order Heading } else if ( match( block, /^[^\n]+\n---+(\n|$)/ ) ) { len = RLENGTH; HL[2]++; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0; - return "

" inline( gensub( /\n.*$/, "", "g", block ) ) "

\n\n" \ + return "

" \ + inline( gensub( /\n.*$/, "", "g", block ) ) \ + "

\n\n" \ _block( substr( block, len + 1) ); # Nth Order Heading @@ -361,9 +511,16 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent ) { htxt = gensub(/^#{1,6}[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[^\n#])+)([ \t]*#*)(\n.*)?$/, "\\1", 1, block); HL[hlvl]++; for ( n = hlvl + 1; n < 7; n++) { HL[n] = 0;} hid = HL[1]; for ( n = 2; n <= hlvl; n++) { hid = hid "." HL[n] ; } - return "" inline( htxt ) "\n\n" \ + return "" inline( htxt ) \ + "\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 +528,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 "

" inline( substr(block, 1, st - 1) ) "

\n" \ - _block( substr(block, st + len) ); + return "

" inline(block) "

\n"; } } @@ -402,12 +556,28 @@ function _list( block, last, LOCAL, p) { } sub( /\n$/, "", p ); - # Task List (pandoc) - if ( p ~ /^\[ \].*/ ) { p = "" substr(p, 4); } - else if ( p ~ /^\[[xX]\].*/ ) { p = "" substr(p, 4); } - else if ( p ~ /^

\[ \].*/ ) { p = "

" substr(p, 7); } - else if ( p ~ /^

\[[xX]\].*/ ) { p = "

" substr(p, 7); } - return "

  • " p "
  • \n" _list( block, last ); + # Task List (pandoc, custom) + if ( p ~ /^\[ \].*/ ) { return "
  • " \ + substr(p, 4) "
  • \n" _list( block, last ); + } else if ( p ~ /^\[-\].*/ ) { return "
  • " \ + substr(p, 4) "
  • \n" _list( block, last ); + } else if ( p ~ /^\[\?\].*/ ) { return "
  • " \ + substr(p, 4) "
  • \n" _list( block, last ); + } else if ( p ~ /^\[\/\].*/ ) { return "
  • " \ + substr(p, 4) "
  • \n" _list( block, last ); + } else if ( p ~ /^\[[xX]\].*/ ) { return "
  • " \ + substr(p, 4) "
  • \n" _list( block, last ); + } else if ( p ~ /^

    \[ \].*/ ) { return "

  • " \ + substr(p, 7) "

  • \n" _list( block, last ); + } else if ( p ~ /^

    \[-\].*/ ) { return "

  • " \ + substr(p, 7) "

  • \n" _list( block, last ); + } else if ( p ~ /^

    \[\?\].*/ ) { return "

  • " \ + substr(p, 7) "

  • \n" _list( block, last ); + } else if ( p ~ /^

    \[\/\].*/ ) { return "

  • " \ + substr(p, 7) "

  • \n" _list( block, last ); + } else if ( p ~ /^

    \[[xX]\].*/ ) { return "

  • " \ + substr(p, 7) "

  • \n" _list( block, last ); + } else { return "
  • " p "
  • \n" _list( block, last ); } } BEGIN { diff --git a/session.sh b/session.sh index 5b36ae0..1f4699e 100755 --- a/session.sh +++ b/session.sh @@ -75,6 +75,7 @@ checkid(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | grep -m 1 -xE '[0-9a-zA- update_session(){ local session sid time sig checksig + unset SESSION_KEY SESSION_ID read -r sid time sig <<-END $(POST session_key || COOKIE session) @@ -82,23 +83,38 @@ update_session(){ checksig="$(session_mac "$sid" "$time")" - if ! [ "$checksig" = "$sig" \ - -a "$time" -ge "$_DATE" \ - -a "$(printf %s "$sid" |checkid)" ] 2>&- + if [ "$checksig" = "$sig" \ + -a "$time" -ge "$_DATE" \ + -a "$(checkid "$sid")" ] 2>&- then - debug "Setting up new session" - sid="$(randomid)" + time=$(( $_DATE + $SESSION_TIMEOUT )) + sig="$(session_mac "$sid" "$time")" + + SESSION_KEY="${sid} ${time} ${sig}" + SESSION_ID="${sid}" + return 0 + else + return 1 fi +} + +new_session(){ + local sid time sig + + debug "Setting up new session" + sid="$(randomid)" time=$(( $_DATE + $SESSION_TIMEOUT )) sig="$(session_mac "$sid" "$time")" - printf %s\\n "${sid} ${time} ${sig}" + + SESSION_KEY="${sid} ${time} ${sig}" + SESSION_ID="${sid}" } SESSION_BIND() { # Set tamper-proof authenticated cookie local key="$1" value="$2" - SET_COOKIE session "$key"="${value} $(session_mac "$value" "$SESSION_ID")" + SET_COOKIE session "$key"="${value} $(session_mac "$value" "$SESSION_ID")" Path="/${_BASE#/}" SameSite=Strict HttpOnly } SESSION_VAR() { @@ -115,10 +131,8 @@ SESSION_VAR() { } SESSION_COOKIE() { - SET_COOKIE 0 session="$SESSION_KEY" Path=/ SameSite=Strict HttpOnly + [ "$1" = new ] && new_session + SET_COOKIE 0 session="$SESSION_KEY" Path="/${_BASE#/}" SameSite=Strict HttpOnly } -SESSION_KEY="$(update_session)" -SESSION_ID="${SESSION_KEY%% *}" - -[ "$1" = nocookie ] || SESSION_COOKIE +update_session || new_session diff --git a/users.sh b/users.sh new file mode 100755 index 0000000..6a6833e --- /dev/null +++ b/users.sh @@ -0,0 +1,599 @@ +#!/bin/sh + +[ -n "$include_users" ] && return 0 +include_users="$0" + +. "${_EXEC}/cgilite/session.sh" +. "${_EXEC}/cgilite/storage.sh" + +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)" + +[ "$HTTPS" ] && SCHEMA=https || SCHEMA=http + +# == FILE FORMAT == +# UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE +# (pending|active|deleted) + +# == GLOBALS == +UNSET_USER='unset \ + USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE +' + +LOCAL_USER='local \ + USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE +' + +unset USER_IDMAP +eval "$UNSET_USER" + +user_db="${user_db:-${_DATA}/users.db}" + +read_user() { + local user="$1" + + # Global exports + USER_ID='' USER_NAME='' USER_STATUS='' USER_EMAIL='' USER_PWSALT='' + USER_PWHASH='' USER_EXPIRE='' USER_DEVICES='' USER_FUTUREUSE='' + + if [ $# -eq 0 ]; then + read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE + elif [ "$user" -a -f "$user_db" -a -r "$user_db" ]; then + read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF + $(grep "^${user} " "${user_db}") + EOF + fi + if [ "$USER_ID" -a "${USER_EXPIRE:-0}" -gt "$_DATE" ]; then + USER_NAME="$(UNSTRING "$USER_NAME")" + USER_EMAIL="$(UNSTRING "$USER_EMAIL")" + USER_DEVICES="$(UNSTRING "$USER_DEVICES")" + unset USER_PWSALT USER_PWHASH + else + eval "$UNSET_USER" + return 1 + fi +} + +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 arg + + for arg in "$@"; do case $arg in + uname=*) uname="${arg#*=}";; + status=*) status="${arg#*=}";; + email=*) email="${arg#*=}";; + password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";; + expire=*) expire="${arg#*=}";; + devices=*) devices="${arg#*=}";; + esac; done + + if LOCK "$user_db"; then + while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \ + FUTUREUSE; do + 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 + USER_ACCOUNTEXPIRE))}" \ + "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \ + "${FUTUREUSE:-\\}" + elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then + # omit expired invitations from output + : + else + printf '%s %s %s %s %s %s %i %s %s\n' \ + "$UID_" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \ + "$EXPIRE" "$DEVICES" "$FUTUREUSE" + fi + done <"$user_db" >"${user_db}.$$" + mv -- "${user_db}.$$" "$user_db" + RELEASE "$user_db" + else + return 1 + fi +} + +new_user(){ + local user="${1:-$(timeid)}" + shift 1 + + if LOCK "$user_db"; then + if grep -q "^${user} " "$user_db"; then + RELEASE "$user_db" + return 1 + fi + printf '%s \\ %s \\ \\ \\ %i \\ \\\n' \ + "$user" "pending" "$(( _DATE + USER_CONFIRMEXPIRE ))" >>"$user_db" + else + return 1 + fi + + if [ $# -eq 0 ]; then + RELEASE "$user_db" + return 0 + elif update_user "$user" "$@"; then + return 0 + else + RELEASE "$user_db" + return 1 + fi +} + +user_idmap(){ + local uid="$1" ret + eval "$LOCAL_USER" + + if [ ! "$USER_IDMAP" ]; then + while read_user; do + USER_IDMAP="${USER_IDMAP}${USER_ID} ${USER_NAME}${BR}" + done <"$user_db" + fi + if [ "$uid" -a "$USER_IDMAP" != "${USER_IDMAP##*${uid} }" ]; then + ret="${USER_IDMAP##*${uid} }"; ret="${ret%%${BR}*}"; + printf '%s\n' "$ret" + return 0 + elif [ "$uid" ]; then + return 1 + else + printf '%s' "$USER_IDMAP" + return 0 + fi +} + +user_idof(){ + local name="$(STRING "$1")" ret + [ "$USER_IDMAP" ] || user_idmap >/dev/null + + if [ "${name%\\}" -a "$USER_IDMAP" != "${USER_IDMAP% ${name}${BR}*}" ]; then + ret="${USER_IDMAP% ${name}${BR}*}"; ret="${ret##*${BR}}" + printf '%s\n' "$ret" + return 0 + else + return 1 + fi +} + +user_checkname(){ + { [ $# -gt 0 ] && printf %s "$*" || cat; } \ + | sed -nE ' + :X; $!{N;bX;} + s;[ \t\r\n]+; ;g; + s;^ ;;; s; $;;; + /@/d; + /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d; + p; + ' +} + +user_checkemail(){ + { [ $# -gt 0 ] && printf %s "$*" || cat; } \ + | sed -nE ' + # W3C recommended email regex + # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email) + /^[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; + ' +} + +user_nameexist(){ + local uname="$(STRING "$1")" + 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 + [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0 + done <"$user_db" + return 1 +} + +user_emailexist(){ + local email="$(STRING "$1")" + 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 + [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0 + done <"$user_db" + return 1 +} + +user_pwhash(){ + local salt="$1" secret="$2" hash + hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)" + printf '%s\n' "${hash%% *}" +} + +user_register(){ + # reserve account, send registration mail + # preliminary uid, expiration, signature + local uid="$(timeid)" + local uname="$(POST uname |user_checkname)" + local email="$(POST email |user_checkemail)" + local pwsalt="$(randomid)" + local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)" + + if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED" + fi + + if [ "$USER_REQUIREEMAIL" = true ]; then + if [ ! "email" ]; then + 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 + USER_CONFIRMEXPIRE))"; then + debug "Sending Activation Link:" \ + "${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%:*} + + Someone tried to sign up for a user account using this email address. + + You can activate your account using this link: + + ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid") + + 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 + simply ignore this message and we will remove your email address from + our database within the next day. + + This is an automatic email. Any direct reply will not be received. + Your Account Registration Robot. + EOF + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi + + elif [ "$USER_REQUIREEMAIL" != true ]; then + if [ ! "$uname" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID" + elif user_nameexist "$uname"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS" + elif [ ! "$pw" ]; then + 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 + USER_ACCOUNTEXPIRE))"; then + SESSION_COOKIE new + SESSION_BIND user_id "$uid" + + 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 + fi +} + +user_invite(){ + local uid="$(timeid)" + local email="$(POST email |user_checkemail)" + local message="$(POST message)" + + if [ ! "email" ]; then + 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 + USER_CONFIRMEXPIRE))"; then + debug "Sending Invitation Link:" \ + "${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%:*} + + ${USER_NAME:-Someone} has offered an invitation to this email address. + + ${message} + + You can create your account using this link: + + ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid") + + 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 + simply ignore this message and we will remove your email address from + our database within the next day. + + This is an automatic email. Any direct reply will not be received. + Your Account Registration Robot. + EOF + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi +} + +user_confirm(){ + # enable account + eval "$LOCAL_USER" + local uid="$(POST uid |checkid || printf invalid)" + local signature="$(POST signature)" + local uname="$(POST uname |user_checkname)" + local pwsalt="$(randomid)" + local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)" + + read_user "${uid}" + + if [ "$signature" != "$(session_mac "$uid")" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID" + elif [ ! "$uname" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID" + elif user_nameexist "$uname"; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS" + elif [ ! "$pw" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT" + elif [ "$pw" != "$pwconfirm" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH" + elif [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID" + elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then + SESSION_COOKIE new + SESSION_BIND user_id "$USER_ID" + 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 +} + +user_login(){ + # set cookie + # keep logged in - device cookie? + # initialize new session! + 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 + 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_" + REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN" + fi + fi + done <"$user_db" + REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN" +} + +user_logout(){ + # destroy cookie, destroy session + # keep device cookie + new_session + SESSION_COOKIE new + SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly + REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT" +} + +user_update(){ + # 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 + : +} +user_disable(){ + : +} + +read_user "$(SESSION_VAR user_id)" +[ "$USER_STATUS" -a "$USER_STATUS" != active ] && eval $UNSET_USER + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + user_register) user_register ;; + user_confirm) user_confirm ;; + user_invite) user_invite ;; + user_login) user_login ;; + user_logout) user_logout ;; + 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 + elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then + cat <<-EOF + [div #user_register .disabled + User Registration is disabled. + ] + EOF + elif [ "$USER_REQUIREEMAIL" = true ]; then + cat <<-EOF + [form #user_register .registeremail method=POST + [p We will send an activation mail to your email address. + You can continue the signup process when you click on the + activation link in this email.] + [input type=email name=email placeholder="Email"] + [submit "action" "user_register" Sign Up] + ] + EOF + 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="^\[\\\\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] + ] + EOF + fi +} + +w_user_confirm(){ + 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 + $(grep "^${uid} " "$user_db") + EOF + if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then + cat <<-EOF + [form #user_confirm method=POST + [input type=hidden name=uid value="${uid}"] + [input type=hidden name=signature value="${signature}"] + $([ "$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] + ] + EOF + else + cat <<-EOF + [div #user_confirm .expired + [p This activation link is not valid anymore.] + ] + EOF + fi + else + cat <<-EOF + [div #user_confirm .invalid + [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.] + ] + EOF + fi +} + +w_user_invite(){ + local uid invlink + + if [ "$(GET user_confirm)" ]; then + w_user_confirm + elif [ "$USER_ID" -a "$SENDMAIL" ]; then + cat <<-EOF + [form #user_invite method=POST + [input placeholder="Email Recipient" name=email autocomplete=off] + [textarea name="message" placeholder="Message to recipient" . ] + [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 + Only registered users may send an invitation to another user. + ] + EOF + fi +} + +w_user_login(){ + if [ ! "$USER_ID" ]; then + cat <<-EOF + [form #user_login .login method=POST + [input name=uname placeholder="Username or Email" autocomplete=off] + [input type=password name=pw placeholder="Passphrase"] + [submit "action" "user_login" Login] + ] + EOF + elif [ "$USER_ID" ]; then + cat <<-EOF + [form #user_login .logout method=POST + [p Logged in as [span . $(HTML ${USER_NAME})]] + [submit "action" "user_logout" Logout] + ] + EOF + fi +}