--- /dev/null
+cgilite
+serverkey
+users.db
_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="\r"
BR='
'
"$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
(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
[ "${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#*: }";;
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.
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
# 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"
}
# 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"
}
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;
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;
border-radius: 2pt;
}
select { padding: .375em 0; }
+textarea { min-height: 7em; }
input[type=radio], input[type=checkbox] {
vertical-align: baseline;
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; }
*[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;
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
}
# 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:
# ==========================
# - ? 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:
# ----------------------------
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 {
# 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 {
# 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)
# 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 "))__$") ) {
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;
}
}
-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 == "" ) {
} 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, /^> /) ) {
return "<blockquote>\n" _block( gensub( /(^|\n)> /, "\n", "g", substr(block, 1, st - 1) ) ) "</blockquote>\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 = "<thead>\n<tr>"
+ for (cnt = 1; cnt < cols; cnt++)
+ ttext = ttext "<th align=\"" talign[cnt] "\">" inline(tarray[cnt]) "</th>"
+ ttext = ttext "</tr>\n</thead><tbody>\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 "<tr>"
+ for (cnt = 1; cnt < cols; cnt++)
+ ttext = ttext "<td align=\"" talign[cnt] "\">" inline(tarray[cnt]) "</td>"
+ ttext = ttext "</tr>\n"
+ }
+ return "<table>" ttext "</tbody></table>\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 = "<thead>\n<tr>"
+ for (cnt = 1; cnt < cols; cnt++)
+ ttext = ttext "<th align=\"" talign[cnt] "\">" _block(tarray[cnt]) "</th>"
+ ttext = ttext "</tr>\n</thead><tbody>\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 "<tr>"
+ for (cnt = 1; cnt < cols; cnt++)
+ ttext = ttext "<td align=\"" talign[cnt] "\">" _block(tarray[cnt]) "</td>"
+ ttext = ttext "</tr>\n"
+ }
+ return "<table>" ttext "</tbody></table>\n" _block(block);
+
# Line Blocks (pandoc)
} else if ( match(block, /^\| [^\n]*(\n|$)(\| [^\n]*(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) {
len = RLENGTH; st = RSTART;
_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);
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.*$/, "\\2", 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, /$/ );
} 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 "<h1 id=\"" HL[1] " - " HTML(gensub( /\n.*$/, "", "g", block )) "\">" inline( gensub( /\n.*$/, "", "g", block ) ) "</h1>\n\n" \
+ return "<h1 id=\"" HL[1] " - " HTML(gensub( /\n.*$/, "", "g", block )) "\">" \
+ inline( gensub( /\n.*$/, "", "g", block ) ) \
+ "<a class=\"anchor\" href=\"#" HL[1] " - " \
+ HTML(gensub( /\n.*$/, "", "g", block )) "\"></a></h1>\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 "<h2 id=\"" HL[1] "." HL[2] " - " HTML(gensub( /\n.*$/, "", "g", block )) "\">" inline( gensub( /\n.*$/, "", "g", block ) ) "</h2>\n\n" \
+ return "<h2 id=\"" HL[1] "." HL[2] " - " HTML(gensub( /\n.*$/, "", "g", block )) "\">" \
+ inline( gensub( /\n.*$/, "", "g", block ) ) \
+ "<a class=\"anchor\" href=\"#" HL[1] "." HL[2] " - " \
+ HTML(gensub( /\n.*$/, "", "g", block )) "\"></a></h2>\n\n" \
_block( substr( block, len + 1) );
# Nth Order Heading
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 "<h" hlvl " id=\"" hid " - " HTML(htxt) "\">" inline( htxt ) "</h" hlvl ">\n\n" \
+ return "<h" hlvl " id=\"" hid " - " HTML(htxt) "\">" inline( htxt ) \
+ "<a class=\"anchor\" href=\"#" hid "\"></a></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;
# 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";
}
}
}
sub( /\n$/, "", p );
- # Task List (pandoc)
- if ( p ~ /^\[ \].*/ ) { p = "<input type=checkbox disabled />" substr(p, 4); }
- else if ( p ~ /^\[[xX]\].*/ ) { p = "<input type=checkbox disabled checked />" substr(p, 4); }
- else if ( p ~ /^<p>\[ \].*/ ) { p = "<p><input type=checkbox disabled />" substr(p, 7); }
- else if ( p ~ /^<p>\[[xX]\].*/ ) { p = "<p><input type=checkbox disabled checked />" substr(p, 7); }
- return "<li>" p "</li>\n" _list( block, last );
+ # Task List (pandoc, custom)
+ if ( p ~ /^\[ \].*/ ) { return "<li class=\"task pending\"><input type=checkbox disabled />" \
+ substr(p, 4) "</li>\n" _list( block, last );
+ } else if ( p ~ /^\[-\].*/ ) { return "<li class=\"task negative\"><input type=checkbox disabled />" \
+ substr(p, 4) "</li>\n" _list( block, last );
+ } else if ( p ~ /^\[\?\].*/ ) { return "<li class=\"task unsure\"><input type=checkbox disabled />" \
+ substr(p, 4) "</li>\n" _list( block, last );
+ } else if ( p ~ /^\[\/\].*/ ) { return "<li class=\"task partial\"><input type=checkbox disabled />" \
+ substr(p, 4) "</li>\n" _list( block, last );
+ } else if ( p ~ /^\[[xX]\].*/ ) { return "<li class=\"task done\"><input type=checkbox disabled checked />" \
+ substr(p, 4) "</li>\n" _list( block, last );
+ } else if ( p ~ /^<p>\[ \].*/ ) { return "<li class=\"task pending\"><p><input type=checkbox disabled />" \
+ substr(p, 7) "</li>\n" _list( block, last );
+ } else if ( p ~ /^<p>\[-\].*/ ) { return "<li class=\"task negative\"><p><input type=checkbox disabled />" \
+ substr(p, 7) "</li>\n" _list( block, last );
+ } else if ( p ~ /^<p>\[\?\].*/ ) { return "<li class=\"task unsure\"><p><input type=checkbox disabled />" \
+ substr(p, 7) "</li>\n" _list( block, last );
+ } else if ( p ~ /^<p>\[\/\].*/ ) { return "<li class=\"task partial\"><p><input type=checkbox disabled />" \
+ substr(p, 7) "</li>\n" _list( block, last );
+ } else if ( p ~ /^<p>\[[xX]\].*/ ) { return "<li class=\"task done\"><p><input type=checkbox disabled checked />" \
+ substr(p, 7) "</li>\n" _list( block, last );
+ } else { return "<li>" p "</li>\n" _list( block, last ); }
}
BEGIN {
update_session(){
local session sid time sig checksig
+ unset SESSION_KEY SESSION_ID
read -r sid time sig <<-END
$(POST session_key || COOKIE 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() {
}
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
--- /dev/null
+#!/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
+}