From 2af4a30f8cf19d4e205888305e30bfc44525ebc5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Paul=20H=C3=A4nsch?= Date: Mon, 25 Jul 2022 14:39:58 +0200 Subject: [PATCH] Squashed 'cgilite/' changes from b2b268b..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 git-subtree-dir: cgilite git-subtree-split: 5038774f89b33ecd03e407898e9d57d38245b5bb --- cgilite.sh | 8 +- common.css | 28 ++++++- file.sh | 27 ++++--- markdown.awk | 201 +++++++++++++++++++++++++++++++++++++++++++-------- users.sh | 162 +++++++++++++++++++++++++++++++---------- 5 files changed, 341 insertions(+), 85 deletions(-) diff --git a/cgilite.sh b/cgilite.sh index 6cbd7ec..2193e29 100755 --- a/cgilite.sh +++ b/cgilite.sh @@ -144,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 @@ -152,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#*: }";; @@ -162,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. @@ -292,7 +294,7 @@ HTML(){ \]*) out="${out}]"; str="${str#?}";; "${CR}"*) out="${out} "; str="${str#?}";; "${BR}"*) out="${out} "; str="${str#?}";; - *) out="${out}${str%%[]&<>\"\'[]*}"; str="${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 359f07d..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,10 @@ 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 { @@ -90,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; } 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 d28c7cf..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: # ========================== @@ -46,16 +42,17 @@ # - [x] Automatic heading identifiers (custom) # - [x] Fenced code blocks (php md, pandoc) # - [x] Fenced code attributes -# - [ ] Tables +# - [/] 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) +# - [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,8 +144,8 @@ 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) ); @@ -155,9 +158,9 @@ function inline( line, LOCAL, len, code, href, guard ) { # 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,8 +170,8 @@ 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) ); @@ -279,6 +282,12 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib } 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, /^> /) ) { @@ -287,6 +296,115 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib 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; @@ -298,7 +416,7 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib _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); @@ -328,7 +446,7 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib } 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); + attrib = gensub(/^(~~~+|```+)[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\2", 1, block); gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib); if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) { @@ -370,14 +488,20 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib } 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 @@ -387,7 +511,8 @@ function _block( block, LOCAL, st, len, hlvl, htxt, guard, code, indent, attrib 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 @@ -431,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/users.sh b/users.sh index 873edf0..6a6833e 100755 --- 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 -- 2.39.2