From: Paul Hänsch Date: Wed, 17 Apr 2024 14:33:29 +0000 (+0200) Subject: debug function X-Git-Url: http://git.plutz.net/?p=cgilite;a=commitdiff_plain;h=HEAD;hp=238f0f8a2932b5ba66f7139c227eaaeb5dd7a013 debug function --- 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.awk b/cgilite.awk new file mode 100644 index 0000000..ebf4411 --- /dev/null +++ b/cgilite.awk @@ -0,0 +1,161 @@ +#!/bin/env awk -f + +function debug(t) { printf "%s\n", t >>"/dev/stderr"; } + +function PATH( str, seg, out ) { + while ( str ) { + seg = str; + sub( /\/.*$/, "", seg); + sub( /^[^\/]*\//, "", str); + + if ( seg == ".." ) sub(/\/[^\/]*\/?$/, "", out); + else if ( seg ~ /^\.?$/) sub(/\/?$/, "/", out); + else sub(/\/?$/, "/" seg, out); + + if (seg == str) break; + } + if (!(str && out)) sub(/\/?$/,"/" out); + return out; +} + +function HEX_DECODE( pfx, inp, out, n, k ) { + k = length(pfx); + gsub(/[].*+?^${}()|\\[]/,"\\\\&",pfx); + while ( inp ) if ( n = match(inp, pfx "[0-9a-fA-F][0-9a-fA-F]") ) { + out = out substr(inp, 1, n - 1); + inp = substr(inp, n + k); + if (inp ~ /^[0-9]/) n = 16 * substr(inp, 1, 1); + else if (inp ~ /^[aA]/) n = 160; + else if (inp ~ /^[bB]/) n = 176; + else if (inp ~ /^[cC]/) n = 192; + else if (inp ~ /^[dD]/) n = 208; + else if (inp ~ /^[eE]/) n = 224; + else if (inp ~ /^[fF]/) n = 240; + if (inp ~ /^.[0-9]/) n += substr(inp, 2, 1); + else if (inp ~ /^.[aA]/) n += 10; + else if (inp ~ /^.[bB]/) n += 11; + else if (inp ~ /^.[cC]/) n += 12; + else if (inp ~ /^.[dD]/) n += 13; + else if (inp ~ /^.[eE]/) n += 14; + else if (inp ~ /^.[fF]/) n += 15; + out = out sprintf("%c", n); + inp = substr(inp, 3); + } else { + out = out inp; + break; + } + return out; +} + +function HTML( text ) { + gsub( /&/, "\\&", text ); + gsub( //, "\\>", text ); + gsub( /"/, "\\"", text ); + gsub( /'/, "\\'", text ); + gsub( /\[/, "\\[", text ); + gsub( /\]/, "\\]", text ); + gsub( /\r/, "\\ ", text ); + gsub( /\n/, "\\ ", text ); + gsub( /\\/, "\\\", text ); + return text; +} + +function URL( text ) { + gsub( /&/, "%26", text ); + gsub( /"/, "%22", text ); + gsub( /'/, "%27", text ); + gsub( /`/, "%60", text ); + gsub( /\?/, "%3F", text ); + gsub( /#/, "%23", text ); + gsub( /\[/, "%5B", text ); + gsub( /\]/, "%5D", text ); + gsub( / /, "%20", text ); + gsub( /\t/, "%09", text ); + gsub( /\r/, "%0D", text ); + gsub( /\n/, "%0A", text ); + gsub( /%/, "%25", text ); + gsub( /\\/, "%5C", text ); + return text; +} + +function _cgilite_urldecode( str, arr, spl, form, k, n, key) { + if (! spl) spl="&" + split(str, form, spl); + for ( k in form ) { + key = form[k]; sub(/=.*$/, "", key); + sub(/^[^=]*=/, "", form[k]); + if ( key in arr ) { + n = 1; while ( (key, n) in arr ) n++; + arr[key,n] = HEX_DECODE( "%", form[k]); + } else { + arr[key] = HEX_DECODE( "%", form[k]); + } + } +} + +function _cgilite_request( key, val) { + # Read request from client connection + + # Read Headers + getline; REQUEST_METHOD = $1; REQUEST_URI = $2; SERVER_PROTOCOL = $3; + while ( getline ) { + if ($0 ~ /^\r?$/) break; + else if ($0 ~ /^[a-zA-Z][0-9a-zA-Z_-]+: .*/) { + key = toupper($0); + sub(/:.*$/, "", key); + gsub(/-/, "_", key); + _HEADER[key] = $0; + sub(/^[^:]:[\t ]*/, "", _HEADER[key]); + sub(/[\t ]*\r?$/, "", _HEADER[key]); + } + } + CONTENT_LENGTH = _HEADER["CONTENT_LENGTH"]; + CONTENT_TYPE = _HEADER["CONTENT_TYPE"]; + + PATH_INFO = REQUEST_URI; gsub(/\?.*$/, "", PATH_INFO) + PATH_INFO = PATH( HEX_DECODE( "%", PATH_INFO ) ); + QUERY_STRING = REQUEST_URI; + if ( !gsub(/^[^?]+\?/, "", QUERY_STRING) ) QUERY_STRING = ""; + + # Set up _GET[]-Array + _cgilite_urldecode(QUERY_STRING, _GET); + + if ( _HEADER["CONTENT_TYPE"] == "application/x-www-form-urlencoded" \ + && _HEADER["CONTENT_LENGTH"] ) { + # Set up _POST[]-Array + + val = ""; key = "head -c " _HEADER["CONTENT_LENGTH"]; + while (key |getline) val = val $0; close(key); + _cgilite_urldecode(val, _POST); + } + + if ( _HEADER["COOKIE"] ) { + # Set up _COOKIE[]-Array + _cgilite_urldecode(_HEADER["COOKIE"], _COOKIE, "; ?"); + } + + if ( _HEADER["REFERER"] ) { + key = HEADER["REFERER"]; + if (! sub(/^[^\?]+?/, "", key)) key = "" + _cgilite_urldecode(key, _REF); + } + +} + +function _cgilite_headers() { + # Import request data from webserver environment variables +} + +BEGIN { + REQUEST_METHOD=""; REQUEST_URI=""; SERVER_PROTOCOL=""; + PATH_INFO=""; QUERY_STRING=""; CONTENT_LENGTH=""; CONTENT_TYPE=""; + split("", _GET); split("", _POST); split("", _REF); + split("", _HEADER); split("", _COOKIE); + + if ( ENVIRON["REQUEST_METHOD"] ) { + _cgilite_headers(); + } else { + _cgilite_request(); + } +} diff --git a/cgilite.sh b/cgilite.sh index 2e53832..b2467c3 100755 --- a/cgilite.sh +++ b/cgilite.sh @@ -1,36 +1,64 @@ #!/bin/sh -# Copyright 2017 - 2020 Paul Hänsch -# # This is CGIlite. # A collection of posix shell functions for writing CGI scripts. + +# Copyright 2017 - 2023 Paul Hänsch # -# CGIlite is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# CGIlite is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. # -# You should have received a copy of the GNU Affero General Public License -# along with CGIlite. If not, see . +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[ -n "$include_cgilite" ] && return 0 +# guard set after webserver part # ksh and zsh workaround # set -o posix # ksh, not portable setopt -o OCTAL_ZEROES 2>&- +# Integrated webserver request timeout +cgilite_timeout=2 + +# General environment variables +# $_EXEC - directory containing application itself +# $_DATA - direcotry where application data may be stored +# $_BASE - optional prefix for http path, e.g. "/myapp" +# +# Programmers should take care to use those variables throughout the +# application. +# Variables may be set via CLI argument, in environment, or left as default. + +for cgilite_arg in "$@"; do case $cgilite_arg in + --exec=*) _EXEC="${cgilite_arg#*=}";; + --data=*) _DATA="${cgilite_arg#*=}";; + --base=*) _BASE="${cgilite_arg#*=}";; +esac; done +unset cgilite_arg + +_EXEC="${_EXEC:-${0%/*}}" +_DATA="${_DATA:-.}" +_EXEC="${_EXEC%/}" _DATA="${_DATA%/}" _BASE="${_BASE%/}" + +export _EXEC _DATA _BASE + +# Carriage Return and Line Break characters for convenience CR=" " BR=' ' -cgilite_timeout=2 - -debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } PATH(){ local str seg out + # normalize path + # read from stdin if no arguments are provided + [ $# -eq 0 ] && str="$(cat)" || str="$*" while [ "$str" ]; do seg=${str%%/*}; str="${str#*/}" @@ -44,18 +72,65 @@ PATH(){ [ "${str}" -a "${out}" ] && printf %s "$out" || printf %s/ "${out%/}" } -HEX_DECODE=' - s;\\;\\\\;g; :HEXDECODE_X; s;%([^0-9A-F]);\\045\1;g; tHEXDECODE_X; - # Hexadecimal { %00 - %FF } will be transformed to octal { \000 - \377 } for posix printf - s;%[0123].;&\\0;g; s;%[4567].;&\\1;g; s;%[89AB].;&\\2;g; s;%[CDEF].;&\\3;g; - s;%[048C][0-7]\\.;&0;g; s;%[048C][89A-F]\\.;&1;g; s;%[159D][0-7]\\.;&2;g; s;%[159D][89A-F]\\.;&3;g; - s;%[26AE][0-7]\\.;&4;g; s;%[26AE][89A-F]\\.;&5;g; s;%[37BF][0-7]\\.;&6;g; s;%[37BF][89A-F]\\.;&7;g; - s;%.[08](\\..);\10;g; s;%.[19](\\..);\11;g; s;%.[2A](\\..);\12;g; s;%.[3B](\\..);\13;g; - s;%.[4C](\\..);\14;g; s;%.[5D](\\..);\15;g; s;%.[6E](\\..);\16;g; s;%.[7F](\\..);\17;g; -' - HEX_DECODE(){ - printf -- "$(printf %s "$1" |sed -E "$HEX_DECODE")" + local pfx="$1" in="$2" out + # Print out Data encoded as Hex + # + # Arguments: + # pfx - required, prefix for a hex tupel, e.g. "\x", "%" "\", may be empty + # in - required, string to be decoded + # + # anything that does not constitute a tupel of valid Hex numerals + # will be copied to the output literally + + while [ "$in" ]; do + [ "$pfx" ] || case $in in + [0-9a-fA-F][0-9a-fA-F]*):;; + ?*) out="${out}${in%%"${in#?}"}" + in="${in#?}"; continue;; + esac + + case $in in + "$pfx"[0-9a-fA-F][0-9a-fA-F]*) in="${in#"${pfx}"}";; + \\*) in="${in#?}"; out="${out}\\\\"; continue;; + %*) in="${in#?}"; out="${out}%%"; continue;; + *) att="${in%%"${pfx}"*}"; att="${att%%%*}"; att="${att%%\\*}" + out="${out}${att}"; in="${in#"${att}"}"; continue;; + esac; + + # Hex escapes for printf (e.g. \x41) are not portable + # The portable way for Hex output is transforming Hex to Octal + # (e.g. \x41 = \101) + case $in in + [0123]?*) out="${out}\\0";; + [4567]?*) out="${out}\\1";; + [89aAbB]?*) out="${out}\\2";; + [c-fC-F]?*) out="${out}\\3";; + esac + case $in in + [048cC][0-7]*) out="${out}0";; + [048cC][89a-fA-F]*) out="${out}1";; + [159dD][0-7]*) out="${out}2";; + [159dD][89a-fA-F]*) out="${out}3";; + [26aAeE][0-7]*) out="${out}4";; + [26aAeE][89a-fA-F]*) out="${out}5";; + [37bBfF][0-7]*) out="${out}6";; + [37bBfF][89a-fA-F]*) out="${out}7";; + esac + case $in in + ?[08]*) out="${out}0";; + ?[19]*) out="${out}1";; + ?[2aA]*) out="${out}2";; + ?[3bB]*) out="${out}3";; + ?[4cC]*) out="${out}4";; + ?[5dD]*) out="${out}5";; + ?[6eE]*) out="${out}6";; + ?[7fF]*) out="${out}7";; + esac + in="${in#?}" + in="${in#?}" + done + printf -- "$out" } if [ -z "$REQUEST_METHOD" ]; then @@ -74,15 +149,17 @@ 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 SERVER_PROTOCOL="${SERVER_PROTOCOL%${CR}}" - PATH_INFO="$(HEX_DECODE "${REQUEST_URI%\?*}" |PATH)" + PATH_INFO="$(HEX_DECODE % "${REQUEST_URI%\?*}" |PATH)" [ "${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#*: }";; @@ -92,7 +169,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. @@ -121,13 +198,20 @@ if [ -z "$REQUEST_METHOD" ]; then exit 0 fi +include_cgilite="$0" + if [ "${REQUEST_METHOD}" = POST -a "${CONTENT_LENGTH:-0}" -gt 0 -a \ "${CONTENT_TYPE}" = "application/x-www-form-urlencoded" ]; then 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 +# general helper functions, see GET, POST, and REF below + cgilite_count(){ printf %s "&$1" \ | grep -oE '&'"$2"'=[^&]*' \ @@ -141,7 +225,7 @@ cgilite_value(){ str="${str#*&${name}=}" cnt=$((cnt - 1)) done - printf -- "$(printf %s "${str%%&*}" |sed -E 's;\+; ;g;'"$HEX_DECODE")" + HEX_DECODE % "$(printf %s "${str%%&*}" |tr + \ )" } cgilite_keys(){ @@ -153,15 +237,26 @@ cgilite_keys(){ | sort -u } -GET(){ cgilite_value "${QUERY_STRING}" $@; } +# Read arguments from GET, POST, or the query string of the referrer (REF). +# Example: +# GET varname n +# +# where n is number for the Nth occurence of a variable and defaults to 1 +# +# *_COUNT varname +# -> returns number of ocurences +# *_KEYS +# -> returns list of available varnames + +GET(){ cgilite_value "${QUERY_STRING}" "$@"; } GET_COUNT(){ cgilite_count "${QUERY_STRING}" $1; } GET_KEYS(){ cgilite_keys "${QUERY_STRING}"; } -POST(){ cgilite_value "${cgilite_post}" $@; } +POST(){ cgilite_value "${cgilite_post}" "$@"; } POST_COUNT(){ cgilite_count "${cgilite_post}" $1; } POST_KEYS(){ cgilite_keys "${cgilite_post}"; } -REF(){ cgilite_value "${HTTP_REFERER#*\?}" $@; } +REF(){ cgilite_value "${HTTP_REFERER#*\?}" "$@"; } REF_COUNT(){ cgilite_count "${HTTP_REFERER#*\?}" $1; } REF_KEYS(){ cgilite_keys "${HTTP_REFERER#*\?}"; } @@ -174,14 +269,15 @@ HEADER(){ str="${str#*${BR}${1}: }" printf %s "${str%%${BR}*}" else - local var="HTTP_$(printf %s "$1" |tr a-z- A-Z-)" + local var="HTTP_$(printf %s "$1" |tr a-z- A-Z_)" eval "[ \"\$$var\" ] && printf %s \"\$$var\" || return 1" # eval "printf %s \"\$HTTP_$(printf %s "${1}" |tr a-z A-Z |tr -c A-Z _)\"" fi } COOKIE(){ - HEX_DECODE "$( + # Read value of cookie + HEX_DECODE % "$( HEADER Cookie \ | grep -oE '(^|; ?)'"$1"'=[^;]*' \ | sed -En "${2:-1}"'{s;^[^=]+=;;; s;\+; ;g; p;}' @@ -193,20 +289,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}]";; - "${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" } @@ -214,22 +308,22 @@ 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";; - "${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}%60"; 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" } @@ -252,6 +346,7 @@ SET_COOKIE(){ } REDIRECT(){ + # Trigger redirct and terminate script printf '%s: %s\r\n' \ Status "303 See Other" \ Content-Length 0 \ diff --git a/common.css b/common.css new file mode 100644 index 0000000..30c3942 --- /dev/null +++ b/common.css @@ -0,0 +1,191 @@ +/* ======= GENERIC HTML STYLES ======= */ + +* { + box-sizing: border-box; + position: relative; + font: inherit; + text-decoration: inherit; + color: inherit; background: transparent; + max-width: 100%; + margin: 0; padding: 0; + border: none; +} + +body { + font: normal normal normal medium/1.5em sans-serif; + color: #000; background: #FFF; +} + +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; + text-decoration: underline; + color: #068; + word-break: break-word; +} +a.button { + font-style: inherit; + text-decoration: inherit; + color: inherit; +} + +sup { vertical-align: super; } +sub { vertical-align: sub; } +small { font-size: .75em; } +big { font-size: 1.25em; } +strike, del, s { text-decoration: line-through; } +u {text-decoration: underline; } +i, em { font-style: italic; } +b, strong { font-weight: bolder; } +tt, code, var, samp, kbd { font-family: monospace; } +kbd { font-style: italic; } + +blockquote { + background-color: #EEE; + margin: .5em 0; + padding: 1em 2em; + white-space: pre-line; +} + +ul, ol { padding-left: 1.5em; } +dl dt { font-weight: bolder; } +dl dd { + margin: 0 2em; + background-color: #EEE; +} +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; + margin-bottom: .5em; +} + +h4, h5, h6, form legend { + font-weight: bolder; + margin-bottom: .25em; +} + +h1 { + text-align: center; + font-size: 1.5em; +} +h2 { font-size: 1.125em; } + +select, input, button, textarea, a.button { + display: inline-block; + color: #000; background-color: #FFF; + border: .5pt solid; + padding: .25em .75em; + vertical-align: text-bottom; + border: .5pt solid #000; + border-radius: 2pt; +} +select { padding: .375em 0; } +textarea { min-height: 7em; } + +input[type=radio], input[type=checkbox] { + vertical-align: baseline; +} +input[type=number] { text-align: right; padding-right: 0; } + +button, input[type=button], a.button { + box-shadow: .125em .125em .25em; + cursor: pointer; +} +input[type=radio], input[type=checkbox], label[for] { + cursor: pointer; +} + +label { margin-right: .75em; } +input + label { + margin-left: .375em; +} + +input[type="search"] + button.search, +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; + overflow: hidden; +} +input[type="search"] + button.search:before, +input.search + button.search:before { + content: '\1f50d'; + color: #000; + font-weight: bold; +} + +@media print { + @page { margin: 20mm; } + + h1, h2, h3, h4, h5, h6, form legend { + page-break-inside: avoid; + page-break-after: avoid; + page-break-before: auto; + } + li { page-break-inside: avoid; } + th, dt { page-break-after: avoid; } +} + +/* ======= End Generic Styles ======= */ + +/* ======= Common Styles ======= */ + +*[tooltip]:hover:after { + display: block; + position: absolute; + min-width: 12em; + bottom: 100%; left: 50%; transform: translate(-50%, 0); + content: attr(tooltip); + padding: .5em; + color: #000; background-color: #FFC; + border: .5pt solid; + z-index: 1; +} + +input[type=radio].tab { display: none; } +input[type=radio].tab + label { + display: table-cell; + padding: .5em 1em; + color: #000; background-color: #EEE; + border: .5pt solid; +} +input[type=radio].tab:checked + label { + background-color: #FFF; + border-bottom: none; + box-shadow: .125em -.125em .125em #888; + z-index: 1; +} +input[type=radio].tab ~ *.tab { + display: none; + width: 100%; + margin-top: -.5pt; padding: .25em .75em; + border: .5pt solid; + border-radius: 0; + box-shadow: .125em .125em .125em #888; +} + +/* Markdown line-block */ +.line-block { white-space: pre-wrap; } +.line-block br { display: none; } + +/* ======= End Common Styles ======= */ diff --git a/db23.sh b/db23.sh new file mode 100755 index 0000000..e8a0d64 --- /dev/null +++ b/db23.sh @@ -0,0 +1,114 @@ +#!/bin/sh + +# Copyright 2023, 2024 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[ -n "$include_db23" ] && return 0 +include_db23="$0" + +. "${_EXEC:-.}/cgilite/storage.sh" + +DB2() { + local call data file key val seq + data="${BR}${1}${BR}" call="$2" + shift 2 + + case $call in + new|discard) + printf '' + ;; + open|load) file="$1" + cat "$file" || return 1 + ;; + check|contains) key="$(STRING "$1")" val='' + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] && return 1 + ;; + count) key="$(STRING "$1")" val='' seq=0 + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] || val="${val} " + while [ "$val" != '' ]; do + seq=$((seq + 1)) val="${val#* }" + done + printf "%i\n" "$seq" + [ $seq = 0 ] && return 1 + ;; + get) key="$(STRING "$1")" seq="${2:-1}" + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] && return 1 || val="${val} " + while [ $seq -gt 1 ]; do + seq=$((seq - 1)) val="${val#* }" + done + [ "$val" = '' ] && return 1 + UNSTRING "${val%% *}" + ;; + iterate|raw) key="$(STRING "$1")" + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] && return 1 + printf '%s\n' $val + ;; + delete|remove) key="$(STRING "$1")" + val="${data#*"${BR}${key}" *"${BR}"}" + key="${data%"${BR}${key}" *"${BR}"*}" + [ "${key}${BR}${val}" = "${data}" ] && return 1 + printf '%s' "${key#"${BR}"}${BR}${val%"${BR}"}" + ;; + set|store) key="$(STRING "$1")" val="" + shift 1 + val="$(for v in "$@"; do STRING "$v"; printf \\t; done)" + if [ "${data#*"${BR}${key}" *}" != "$data" ]; then + data="${data%"${BR}${key}" *"${BR}"*}${BR}${key} ${val% }${BR}${data#*"${BR}${key}" *"${BR}"}" + data="${data#"${BR}"}" data="${data%"${BR}"}" + else + data="${data#"${BR}"}${key} ${val% }${BR}" + data="${data#"${BR}"}" + fi + printf %s\\n "${data}" + ;; + append) key="$(STRING "$1")" val="" + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + if [ "$val" = '' ]; then + printf %s\\n "${data}" + return 1 + else + shift 1 + val="${val}$(for v in "$@"; do printf \\t; STRING "$v"; done)" + data="${data%"${BR}${key}" *"${BR}"*}${BR}${key} ${val% }${BR}${data#*"${BR}${key}" *"${BR}"}" + data="${data#"${BR}"}" data="${data%"${BR}"}" + printf %s\\n "${data}" + fi + ;; + flush|save|write) file="$1" + data="${data#"${BR}"}" data="${data%"${BR}"}" + printf '%s\n' "$data" >"$file" || return 1 + ;; + esac + return 0 +} + +DB3() { + # wrapper function that allows easyer use of DB2 + # by always keeping file data in $db3_data + + case "$1" in + new|discard|open|load|delete|remove|set|store|append) + db3_data="$(DB2 "$db3_data" "$@")" + return "$?" + ;; + get|count|check|contains|iterate|raw|flush|save|write) + DB2 "$db3_data" "$@" + return "$?" + ;; + esac +} diff --git a/file.sh b/file.sh index 51ec245..c66b17d 100755 --- a/file.sh +++ b/file.sh @@ -1,64 +1,76 @@ #!/bin/sh -# Copyright 2016 - 2019 Paul Hänsch -# -# This file is part of cgilite. +# Copyright 2016 - 2024 Paul Hänsch # -# cgilite is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. # -# cgilite is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with cgilite. If not, see . +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. [ -n "$include_fileserve" ] && return 0 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) printf 'video/mp4';; - ogg) printf 'audio/ogg';; xml) printf 'application/xml';; *) printf 'application/octet-stream';; esac } FILE(){ - local file file_size file_date http_date cachedate range mime - file="$1" mime="$2" + local file="$1" mime="$2" + local file_size file_date http_date cachedate range if ! [ -f "$file" ]; then printf 'Content-Length: 0\r\nStatus: 404 Not Found\r\n\r\n' - exit 0 + return 0 elif ! [ -r "$file" ]; then printf 'Content-Length: 0\r\nStatus: 403 Forbidden\r\n\r\n' - exit 0 + return 0 fi - file_size="$(stat -Lc %s "$file")" - file_date="$(stat -Lc %Y "$file")" - http_date="$(date -uRd @$file_date)" - http_date="${http_date%+0000}GMT" + read file_size file_date <<-EOF + $(stat -Lc "%s %Y" "$file") + EOF + http_date="$(date -ud "@$file_date" +"%a, %d %b %Y %T GMT")" + + [ ! "$HTTP_IF_MODIFIED_SINCE" -a "$cgilite_headers" ] \ + && HTTP_IF_MODIFIED_SINCE="$(HEADER If-Modified-Since)" + [ ! "$HTTP_RANGE" -a "$cgilite_headers" ] \ + && HTTP_RANGE="$(HEADER Range)" + cachedate="$( # Parse the allowable date formats from Section 3.3.1 of # https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html - HEADER If-Modified-Since \ + # HEADER If-Modified-Since \ + printf %s "$HTTP_IF_MODIFIED_SINCE" \ | sed -E 's;^[^ ]+, ([0-9]{2}) (...) ([0-9]{4}) (..:..:..) GMT$;\3-\2-\1 \4;; s;^[^ ]+, ([0-9]{2})-(...)-([789][0-9]) (..:..:..) GMT$;19\3-\2-\1 \4;; s;^[^ ]+, ([0-9]{2})-(...)-([0-6][0-9]) (..:..:..) GMT$;20\3-\2-\1 \4;; @@ -69,14 +81,25 @@ FILE(){ | xargs -r0 date +%s -ud 2>&- )" - range="$(HEADER Range |sed -nE 's;^bytes=([0-9]+-[0-9]*|-[0-9]+)$;\1;p;q;')" + range="${HTTP_RANGE#bytes=}" case "$range" in - *-) range="${range}$((file_size - 1))";; - -*) [ ${range#-} -le $file_size ] \ - && range="$((file_size - ${range#-}))-$((file_size - 1))" \ - || range="0-$((file_size - 1))";; - *-*) [ ${range#*-} -ge $file_size ] \ - && range="${range%-*}-$((file_size - 1))";; + *[!0-9]*-*|*-*[!0-9]*) + range="" + ;; + *-) + range="${range}$((file_size - 1))" + ;; + -*) + [ ${range#-} -le $file_size ] \ + && range="$((file_size - ${range#-}))-$((file_size - 1))" \ + || range="0-$((file_size - 1))" + ;; + *-*) + [ ${range#*-} -ge $file_size ] \ + && range="${range%-*}-$((file_size - 1))" + ;; + *) range="" + ;; esac if [ "$file_date" -lt "$cachedate" ] 2>&-; then diff --git a/html-sh.sed b/html-sh.sed index 8d7b61c..1a0f2b4 100755 --- a/html-sh.sed +++ b/html-sh.sed @@ -1,5 +1,19 @@ #!/bin/sed -nEf +# Copyright 2018 - 2019 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + :Escapes s,\\\\,\\,g; s,\\&,\&,g; s,\\<,\<,g; s,\\>,\>,g; diff --git a/json.sh b/json.sh new file mode 100755 index 0000000..12afdc4 --- /dev/null +++ b/json.sh @@ -0,0 +1,360 @@ +#!/bin/sh + +[ -n "$include_json" ] && return 0 +include_json="$0" + +. "${_EXEC:-.}/cgilite/db23.sh" + +# debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } + +json_except() { + printf '%s\n' "$@" >&2; + printf 'Exc: %s\n' "$json_document" >&2 +} + +json_space() { + while true; do case "$json_document" in + [" ${BR}${CR} "]*) json_document="${json_document#?}";; + *) break ;; + esac; done +} + +json_string() { + local string json_document="$json_document" end=0 + + json_space + case $json_document in + \"*) json_document="${json_document#?}" + ;; + *) json_except "Expected string specifyer starting with (\")" + return 1 + ;; + esac + while [ "$json_document" ]; do case $json_document in + \\?*) + string="${string}${json_document%"${json_document#??}"}" + json_document="${json_document#??}" + ;; + \"*) + json_document="${json_document#?}" + end=1 + break + ;; + *) + string="${string}${json_document%"${json_document#?}"}" + json_document="${json_document#?}" + ;; + esac; done + + if [ $end -eq 0 ]; then + json_except "Document ended mid-string" + return 1 + fi + + printf "%s %s\n" "$(STRING "$string")" "$json_document" +} + +json_key() { + local key json_document="$json_document" + + json_space + case $json_document in + \"*) + key="$(json_string)" || return 1 + json_document="${key#* }" + key="${key%% *}" + ;; + *) json_except "Expected key specifyer starting with '\"'" + return 1 + ;; + esac + json_space + case $json_document in + :*) json_document="${json_document#?}" + ;; + *) json_except "Expected value separator \":\"" + return 1 + ;; + esac + + printf '%s %s\n' "$key" "$json_document" +} + +json_number() { + local number json_document="$json_document" + + json_space + number="${json_document%%[" ${BR}${CR} ,}]"]*}" + json_document="${json_document#"$number"}" + if ! number="$(printf %f "$number")"; then + json_except "Invalid number format" + return 1 + fi + + printf '%s %s\n' "${number%.000000}" "$json_document" +} + +json_array() { + local struct="$(DB2 "" new)" value json_document="$json_document" + + json_space + case $json_document in + "["*) json_document="${json_document#?}" + ;; + *) json_except "Expected array starting with \"[\"" + return 1 + ;; + esac + + json_space + case $json_document in + "]"*) + printf "%s %s\n" "" "${json_document#?}" + return 0 + ;; + esac + + while :; do + json_space + + value="$(json_value)" || return 1 + json_document="${value#* }" + value="$(UNSTRING "${value%% *}")" + + struct="$(DB2 "$struct" append "@" "$value")" \ + || struct="$(DB2 "$struct" set "@" "$value")" + + json_space + case $json_document in + ,*) json_document="${json_document#?}" + ;; + "]"*) json_document="${json_document#?}" + break + ;; + *) json_except "Unexpected character mid-array" + return 1 + ;; + esac + done + + printf "%s %s\n" "$(STRING "$struct")" "$json_document" +} + +json_object() { + local struct="$(DB2 "" new)" key value json_document="$json_document" + + json_space + case $json_document in + "{"*) json_document="${json_document#?}" + ;; + *) json_except "Expected object starting with \"{\"" + return 1 + ;; + esac + + json_space + case $json_document in + "}"*) + printf "%s %s\n" "" "${json_document#?}" + return 0 + ;; + esac + + while :; do + json_space + + key="$(json_key)" || return 1 + json_document="${key#* }" + key="$(UNSTRING "${key%% *}")" + + value="$(json_value)" || return 1 + json_document="${value#* }" + value="$(UNSTRING "${value%% *}")" + + struct="$(DB2 "$struct" set "$key" "$value")" + + json_space + case $json_document in + ,*) json_document="${json_document#?}" + ;; + "}"*) json_document="${json_document#?}" + break + ;; + *) json_except "Unexpected character mid-object" + return 1 + ;; + esac + done + + printf "%s %s\n" "$(STRING "$struct")" "$json_document" +} + +json_value() { + local value json_document="$json_document" + json_type="" + + json_space + case $json_document in + \"*) + value="$(json_string)" || return 1 + json_document="${value#* }" + value="str:${value%% *}" + json_type=string + ;; + [+-.0-9]*) + value="$(json_number)" || return 1 + json_document="${value#* }" + value="num:${value%% *}" + json_type=number + ;; + "{"*) + value="$(json_object)" || return 1 + json_document="${value#* }" + value="obj:${value%% *}" + json_type=object + ;; + "["*) + value="$(json_array)" || return 1 + json_document="${value#* }" + value="arr:${value%% *}" + json_type=array + ;; + null*) + json_document="${json_document#null}" + value="null" + json_type=null + ;; + true*) + json_document="${json_document#true}" + value="true" + json_type=boolean + ;; + false*) + json_document="${json_document#false}" + value="false" + json_type=boolean + ;; + esac + + printf "%s %s\n" "$value" "$json_document" +} + +json_load() { + local json_document="$1" json + + json_value |UNSTRING +} + +json_get() { + local json="$1" jpath="${2#.}" key idx + json_type='' + + case $json in + str:*) json_type="string";; + arr:*) json_type="array";; + obj:*) json_type="object";; + num:*) json_type="number";; + true|false) + json_type="boolean";; + null) json_type="null";; + esac + + case $jpath in + "") + printf %s\\n "${json#???:}" + return 0 + ;; + "["[0-9]*"]"*) + idx="${jpath%%"]"*}" idx="${idx#"["}" + jpath="${jpath#"["*"]"}" + ;; + "['"*"']"*) + key="${jpath%%"']"*}" key="${key#"['"}" + jpath="${jpath#"['"*"']"}" + ;; + "$"*) + jpath="${jpath#?}" + ;; + *) key="${jpath%%[".["]*}" + jpath="${jpath#"$key"}" + ;; + esac + + if [ "$key" -a "$json_type" = object ]; then + if ! json="$(DB2 "${json#obj:}" get "$key")"; then + debug "Key not found: \"$key\"" + return 1 + fi + elif [ "$idx" -a "$json_type" = array ]; then + if ! json="$(DB2 "${json#arr:}" get @ "$(( idx + 1 ))")"; then + debug "Array index not found: \"$idx\"" + return 1 + fi + elif [ "$key" ]; then + debug "Cannot select key (\"$key\") from value of type \"$json_type\"" + return 1 + elif [ "$idx" ]; then + debug "Cannot select index ($idx) from value of type \"$json_type\"" + return 1 + fi + json_get "$json" "$jpath" + return $? +} + +json_dump_string() { + local in="$1" out='' + while [ "$in" ]; do case $in in + \\*) out="${out}\\\\"; in="${in#\\}" ;; + "$BR"*) out="${out}\\n"; in="${in#${BR}}" ;; + "$CR"*) out="${out}\\r"; in="${in#${CR}}" ;; + " "*) out="${out}\\t"; in="${in# }" ;; + \"*) out="${out}\\\""; in="${in#\"}" ;; + *) out="${out}${in%%[\\${CR}${BR} \"]*}"; in="${in#"${in%%[\\${BR}${CR} \"]*}"}" ;; + esac; done + printf '"%s"' "${out}" +} + +json_dump_array() { + local json="$1" value out='' + + for value in $(DB2 "$json" iterate @); do + out="${out},$(json_dump "$(UNSTRING "$value")")" + done + printf '[%s]' "${out#,}" +} + +json_dump_object() { + local json="$1" key value out='' + + while read -r key value; do + out="${out},$(json_dump_string "$(UNSTRING "$key")"):$(json_dump "$(UNSTRING "$value")")" + done <<-EOF + ${json} + EOF + printf '{%s}' "${out#,}" +} + +json_dump() { + local json="$1" + + case $json in + str:*) + json_dump_string "${json#str:}" + ;; + arr:*) + json_dump_array "${json#arr:}" + ;; + obj:*) + json_dump_object "${json#obj:}" + ;; + num:*) + printf "${json#num:}" + ;; + true|false|null) + printf %s\\n "$json" + ;; + *) + json_dump_string "${json}" + ;; + esac +} diff --git a/markdown.awk b/markdown.awk new file mode 100755 index 0000000..34879d2 --- /dev/null +++ b/markdown.awk @@ -0,0 +1,866 @@ +#!/bin/awk -f +#!/opt/busybox/awk -f + +# 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 + +# Copyright 2021 - 2023 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# Supported Features / TODO: +# ========================== +# [x] done [ ] todo [-] not planned ? unsure +# +# Basic Markdown - Block elements: +# ------------------------------- +# - [x] Paragraphs +# - [x] Double space line breaks +# - [x] Proper block element nesting +# - [x] Headings +# - [x] ATX-Style Headings +# - [x] Blockquotes +# - [x] Lists (ordered, unordered) +# - [x] Code blocks (using indention) +# - [x] Horizontal rules +# - [x] Verbatim HTML block (disabled by default) +# +# Basic Markdown - Inline elements: +# --------------------------------- +# - [x] Links +# - [x] Reference style links +# - [x] Emphasis *em*/**strong** (*Asterisk*, _Underscore_) +# - [x] `code`, also ``code containing `backticks` `` +# - [x] Images / reference style images +# - [x] +# - [x] backslash escapes +# - [x] Verbatim HTML inline (disabled by default) +# - [x] HTML escaping +# +# NOTE: Set the environment variable MD_HTML=true to enable verbatim HTML +# +# Extensions - Block elements: +# ---------------------------- +# - [x] Automatic
-wrapping (custom) +# - ? Heading identifiers (php md, pandoc) +# - [x] Heading attributes (custom) +# - [ ]
terminates section +# - [x] Automatic heading identifiers (custom) +# - [x] Fenced code blocks (php md, pandoc) +# - [x] Fenced code attributes +# - [x] Images (as block elements,
-wrapped) (custom) +# - [x] reference style block images +# - [/] Tables +# - ? Simple table (pandoc) +# - ? Multiline table (pandoc) +# - [x] Grid table (pandoc) +# - [x] Headerless +# - [x] Pipe table (php md, pandoc) +# - [x] Line blocks (pandoc) +# - [x] Task lists (pandoc, custom) +# - [x] Definition lists (php md, pandoc) +# - [-] Numbered example lists (pandoc) +# - [-] Metadata blocks (pandoc) +# - [x] Metadata blocks (custom) +# - [x] Fenced Divs (pandoc) +# +# Extensions - Inline elements: +# ---------------------------- +# - [x] Ignore embedded_underscores (php md, pandoc) +# - [x] ~~strikeout~~ (pandoc) +# - [x] ^Superscript^ ~Subscript~ (pandoc) +# - [-] Bracketed spans (pandoc) +# - [-] Inline attributes (pandoc) +# - [x] Image attributes (custom, pandoc inspired, not for reference style) +# - [x] Wiki style links [[PageName]] / [[PageName|Link Text]] +# - [-] TEX-Math (pandoc) +# - ? Footnotes (php md) +# - ? Abbreviations (php md) +# - ? "Curly quotes" (smartypants) +# - [ ] em-dashes (--) (smartypants old) +# - ? ... three-dot ellipsis (smartypants) +# - [-] en-dash (smartypants) +# - [ ] Automatic em-dash / en-dash +# - [x] Automatic -> Arrows <- (custom) + +function debug(text) { printf "\n---\n%s\n---\n", text > "/dev/stderr"; } + +function HTML ( text ) { + gsub( /&/, "\\&", text ); + gsub( //, "\\>", text ); + gsub( /"/, "\\"", text ); + gsub( /'/, "\\'", text ); + gsub( /\\/, "\\\", text ); + return text; +} + +function URL ( text, sharp ) { + gsub( /&/, "%26", text ); + gsub( /"/, "%22", text ); + gsub( /'/, "%27", text ); + gsub( /`/, "%60", text ); + gsub( /\?/, "%3F", text ); + if (sharp) gsub( /#/, "%23", text ); + gsub( /\[/, "%5B", text ); + gsub( /\]/, "%5D", text ); + gsub( / /, "%20", text ); + gsub( / /, "%09", text ); + gsub( /\\/, "%5C", text ); + return text; +} + +function inline( line, LOCAL, len, text, code, href, guard ) { + if ( line ~ /^$/ ) { # Recursion End + return ""; + + # omit processing of escaped characters + } else if ( line ~ /^\\./) { + return HTML(substr(line, 2, 1)) inline( substr(line, 3) ); + + # hard brakes + } else if ( match(line, /^ \n/) ) { + return "
\n" inline( substr(line, RLENGTH + 1) ); + + # ``code spans`` + } else if ( match( line, /^`+/) ) { + len = RLENGTH + guard = substr( line, 1, len ) + if ( match(line, guard ".*" guard) ) { + code = substr( line, len + 1, match( substr(line, len + 1), guard ) - 1) + len = 2 * length(guard) + length(code) + # strip single surrounding white spaces + gsub( /^ | $/, "", code) + # escape HTML within code span + gsub( /&/, "\\&", code ); gsub( //, "\\>", code ); + return "" code "" inline( substr( line, len + 1 ) ) + } + + # Macros + } else if ( match( line, /^<<([^>]|>[^>])+>>/ ) ) { + len = RLENGTH; + return "" HTML( substr( line, 3, len - 4 ) ) "" inline(substr(line, len + 1)); + + # Wiki style links + } else if ( match( line, /^\[\[([^]|]+)(\|[^]]+)?\]\]/) ) { + len = RLENGTH; + href = gensub(/^\[\[([^]|]+)(\|([^]]+))?\]\]/, "\\1", 1, substr(line, 1, len) ); + text = gensub(/^\[\[([^]|]+)(\|([^]]+))?\]\]/, "\\3", 1, substr(line, 1, len) ); + if ( ! text ) text = href; + return "" HTML(text) "" inline( substr( line, len + 1) ); + + # quick links ("automatic links" in md doc) + } else if ( match( line, /^<[a-zA-Z]+:\/\/([-\.[:alnum:]]+)(:[0-9]*)?(\/[^>]*)?>/ ) ) { + len = RLENGTH; + 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) ); + + # 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; + return substr( line, 1, len) inline(substr(line, len + 1)); + + # inline links + } else if ( match(line, "^" lii "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)") ) { + len = RLENGTH; + text = href = title = substr( line, 1, len); + sub("^\\[", "", text); sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)$", "", text); + sub("^" lii "\\([\n\t ]*", "", href); sub("([\n\t ]+" lit ")?[\n\t ]*\\)$", "", href); + sub("^" lii "\\([\n\t ]*" lid, "", title); sub("[\n\t ]*\\)$", "", title); sub("^[\n\t ]+", "", title); + + if ( match(href, /^<.*>$/) ) { sub(/^$/, "", href); } + if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); } + else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); } + else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); } + + gsub(/\\/, "", href); gsub(/\\/, "", title); gsub(/[\n\t]+/, " ", title); + + return "" \ + inline( text ) "" inline( substr( line, len + 1) ); + + # reference style links + } else if ( match(line, /^\[([^]]+)\] ?\[([^]]*)\]/ ) ) { + len = RLENGTH; + 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) ); + } else if ( rl_href[id] ) { + return "" inline(text) "" inline( substr( line, len + 1) ); + } else { + return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) ); + } + + # inline images + } else if ( match(line, "^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?") ) { + len = RLENGTH; text = href = title = attrib = substr( line, 1, len); + + sub("^!\\[", "", text); + sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", text); + + sub("^!" lix "\\([\n\t ]*", "", href); + sub("([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", href); + + sub("^!" lix "\\([\n\t ]*" lid, "", title); + sub("[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", title); + sub("^[\n\t ]+", "", title); + + sub("^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)", "", attrib); + sub(/^\{[ \t]*/, "", attrib); sub(/[ \t]*\}$/, "", attrib); gsub(/[ \t]+/, " ", attrib); + + if ( match(href, /^<.*>$/) ) { sub(/^$/, "", href); } + if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); } + else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); } + else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); } + + gsub(/^[\t ]+$/, "", text); gsub(/\\/, "", href); + gsub(/\\/, "", title); gsub(/[\n\t]+/, " ", title); + + return "\""" inline( substr( line, len + 1) ); + + # reference style images + } else if ( match(line, /^!\[([^]]*)\] ?\[([^]]*)\]/ ) ) { + len = RLENGTH; + 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) ); + } else if ( rl_href[id] ) { + return "\""" \ + inline( substr( line, len + 1) ); + } else { + return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) ); + } + + # ~~strikeout~~ (pandoc) + } else if ( match(line, /^~~([[:graph:]]|[[:graph:]]([^~]|~[^~])*[[:graph:]])~~/) ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # ^superscript^ (pandoc) + } else if ( match(line, /^\^([^[:space:]^]|\\[ ^])+\^/) ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # ~subscript~ (pandoc) + } else if ( match(line, /^~([^[:space:]~]|\\[ ~])+~/) ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # ignore embedded underscores (pandoc, php md) + } else if ( match(line, "^[[:alnum:]](__|_)") ) { + 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 "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # __strong__ + } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__[[:space:][:punct:]]") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 5 ) ) "" inline( substr( line, len) ); + + # **strong** + } else if ( match(line, "^\\*\\*(([^\\*[:space:]]|" iea ")|([^\\*[:space:]]|" iea ")(" na "|" iea ")*([^\\*[:space:]]|" iea "))\\*\\*") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # _em_$ + } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_$") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # _em_ + } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_[[:space:][:punct:]]") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 3 ) ) "" inline( substr( line, len ) ); + + # *em* + } else if ( match(line, "^\\*(([^\\*[:space:]]|" isa ")|([^\\*[:space:]]|" isa ")(" na "|" isa ")*([^\\*[:space:]]|" isa "))\\*") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # Literal HTML entities + } else if ( match( line, /^&([a-zA-Z]{2,32}|#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6});/) ) { + len = RLENGTH; + return substr( line, 1, len ) inline(substr(line, len + 1)); + + # Arrows + } else if ( line ~ /^-->( |$)/) { # ignore multidash-arrow + return "-->" inline( substr(line, 4) ); + } else if ( line ~ /^<-( |$)/) { + return "←" inline( substr(line, 3) ); + } else if ( line ~ /^->( |$)/) { + return "→" inline( substr(line, 3) ); + + # Escape lone HTML character + } else if ( match( line, /^[&<>"']/) ) { + return HTML(substr(line, 1, 1)) inline(substr(line, 2)); + + # continue walk over string + } else { + return substr(line, 1, 1) inline( substr(line, 2) ); + } +} + +function headline( hlvl, htxt, attrib, LOCAL, sec, n, HL) { + match(hstack, /([0-9]+( [0-9]+){5})$/); split( substr(hstack, RSTART), HL); + + for ( n = hlvl; n <= 6; n++ ) { sec = sec (HL[n]?"
":""); } + HL[hlvl]++; for ( n = hlvl + 1; n <= 6; n++) { HL[n] = 0;} + + hid = ""; for ( n = 2; n <= blvl; n++) { hid = hid BL[n] "/"; } + hid = hid HL[1]; for ( n = 2; n <= hlvl; n++) { hid = hid "." HL[n] ; } + hid = hid ":" URL(htxt, 1); + + sub(/([0-9]+( [0-9]+){5})$/, "", hstack); + hstack = hstack HL[1] " " HL[2] " " HL[3] " " HL[4] " " HL[5] " " HL[6]; + + return sec "
" \ + "" inline( htxt ) \ + "" \ + "\n"; +} + +# Nested Block, resets heading counters +function _nblock( block, LOCAL, sec, n ) { + hstack = hstack " 0 0 0 0 0 0"; + + # Block Level + blvl++; BL[blvl]++; + for ( n = blvl + 1; n in BL; n++) { delete BL[n]; } + + block = _block( block ); + match(hstack, /([0-9]+( [0-9]+){5})$/); split( substr(hstack, RSTART), HL); + sec = ""; for ( n = 1; n <= 6; n++ ) { sec = sec (HL[n]?"
":""); } + + sub("( +[0-9]+){6} *$", "", hstack); blvl--; + return block sec; +} + +function _block( block, LOCAL, st, len, text, title, attrib, href, guard, code, indent, list ) { + gsub( "(^\n+|\n+$)", "", block ); + + if ( block == "" ) { + return ""; + + # HTML #2 #3 #4 $5 + } else if ( AllowHTML && match( block, /(^|\n) ? ? ?(|$)|<\?([^\?]|\?[^>])*(\?>|$)|]*(>|$)|])*(\]\]>|$))/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #6 + } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<\/?(address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)([[:space:]\n>]|\/>)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #1 + } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<(script|pre|style)([[:space:]\n>]).*(<\/script>|<\/pre>|<\/style>|$)/) ) { + len = RLENGTH; st = RSTART; + match( tolower(substr(block, st, len)), /(<\/script>|<\/pre>|<\/style>)/); + len = RSTART + RLENGTH; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #7 + } 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, /^> /) ) { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match(block, /$/); + len = RLENGTH; st = RSTART; + text = substr(block, 1, st - 1); gsub( /(^|\n)> /, "\n", text ); + text = _nblock( text ); gsub( /^\n|\n$/, "", text ) + return "
" text "
\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) + # (with, and without header) + } else if ( match( block, "^\\+(-+\\+)+\n" \ + "(\\|([^\n]+\\|)+\n)+" \ + "(\\+(:?=+:?\\+)+)\n" \ + "((\\|([^\n]+\\|)+\n)+" \ + "\\+(-+\\+)+(\n|$))+" \ + ) || \ + match( block, "^()()()" \ + "(\\+(:?-+:?\\+)+)\n" \ + "((\\|([^\n]+\\|)+\n)+" \ + "\\+(-+\\+)+(\n|$))+" \ + ) ) { + len = RLENGTH; st = RSTART; + #initialize empty arrays + split("", talign); split("", tarray); split("", tread); + cols = 0; cnt=0; ttext = ""; + + # Column Count + cols = split( gensub( "^(\\+(:?-+:?\\+)+)(\n.*)*$", "\\1", 1, block), tread, /\+/) - 2; + # debug(" Cols: " gensub( "^(\\+(:?-+:?\\+)+)(\n.*)*$", "\\1", 1, block )); + + # table alignment + split( gensub( "^(.*\n)?\\+((:?=+:?\\+|(:-+|-+:|:-+:)\\+)+)(\n.*)$", "\\2", "g", block ), talign, /\+/ ); + # debug("Align: " gensub( "^(.*\n)?\\+((:?=+:?\\+|(:-+|-+:|:-+:)\\+)+)(\n.*)$", "\\2", "g", block )); + + 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]=""; + } + + if ( match(block, "^\\+(-+\\+)+\n" \ + "(\\|([^\n]+\\|)+\n)+" \ + "\\+(:?=+:?\\+)+\n" \ + "((\\|([^\n]+\\|)+\n)+" \ + "\\+(-+\\+)+(\n|$))+" \ + ) ) { + # table header + block = substr(block, match(block, /(\n|$)/) + 1 ); + 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]; + } + + ttext = "\n" + for (cnt = 1; cnt <= cols; cnt++) + ttext = ttext "" _nblock(tarray[cnt]) "" + ttext = ttext "\n" + } + + # table body + block = substr(block, match(block, /(\n|$)/) + 1 ); + ttext = ttext "\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 "" _nblock(tarray[cnt]) "" + ttext = ttext "\n" + } + return "" ttext "
\n" _nblock(block); + + # Line Blocks (pandoc) + } else if ( match(block, /^\| [^\n]*(\n|$)(\| [^\n]*(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) { + len = RLENGTH; st = RSTART; + + text = substr(block, 1, len); gsub(/\n[[:space:]]+/, " ", text); + gsub(/\n\| /, "\n", text); gsub(/^\| |\n$/, "", text); + text = inline(text); gsub(/\n/, "
\n", text); + + return "
" text "
\n" _block( substr( block, len + 1) ); + + # Indented Code Block + } 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); + gsub(/^\n|\n+$/, "", code); + return "
" HTML( code ) "
\n" \ + _block( substr( block, len + 1 ) ); + + # Fenced Divs (pandoc, custom) + } else if ( match( block, /^(:::+)/ ) ) { + guard = substr( block, 1, RLENGTH ); + code = block; sub(/^[^\n]+\n/, "", code); + 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 "
" _nblock( 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" \ + _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) ); + } + + # First Order Heading H1 + Attrib + } else if ( match( block, /^([^\n]+)([ \t]*\{([^\}\n]+)\})\n===+(\n|$)/ ) ) { + len = RLENGTH; text = attrib = block; + sub(/([ \t]*\{([^\}\n]+)\})\n===+(\n.*)?$/, "", text); + sub(/\}\n===+(\n.*)?$/, "", attrib); sub(/^([^\n]+)[ \t]*\{/, "", attrib); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib); + + return headline(1, text, attrib) _block( substr( block, len + 1 ) ); + + # First Order Heading H1 + } else if ( match( block, /^([^\n]+)\n===+(\n|$)/ ) ) { + len = RLENGTH; text = substr(block, 1, len); + sub(/\n===+(\n.*)?$/, "", text); + + return headline(1, text, 0) _block( substr( block, len + 1 ) ); + + # Second Order Heading H2 + Attrib + } else if ( match( block, /^([^\n]+)([ \t]*\{([^\}\n]+)\})\n---+(\n|$)/ ) ) { + len = RLENGTH; text = attrib = block; + sub(/([ \t]*\{([^\}\n]+)\})\n---+(\n.*)?$/, "", text); + sub(/\}\n---+(\n.*)?$/, "", attrib); sub(/^([^\n]+)[ \t]*\{/, "", attrib); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib); + + return headline(2, text, attrib) _block( substr( block, len + 1) ); + + # Second Order Heading H2 + } else if ( match( block, /^([^\n]+)\n---+(\n|$)/ ) ) { + len = RLENGTH; text = substr(block, 1, len); + sub(/\n---+(\n.*)?$/, "", text); + + return headline(2, text, 0) _block( substr( block, len + 1) ); + + # Nth Order Heading H1 H2 H3 H4 H5 H6 + Attrib + } else if ( match( block, /^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*([ \t]*\{([a-zA-Z \t-]*)\})(\n|$)/ ) ) { + len = RLENGTH; text = attrib = substr(block, 1, len); + match(block, /^#{1,6}/); n = RLENGTH; + + sub(/^(#{1,6})[ \t]*/, "", text); sub(/[ \t]*#*([ \t]*\{([a-zA-Z \t-]*)\})(\n.*)?$/, "", text); + sub(/^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*[ \t]*\{/, "", attrib); + sub(/\})(\n.*)?$/, "", attrib); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib); + + return headline( n, text, attrib ) _block( substr( block, len + 1) ); + + # Nth Order Heading H1 H2 H3 H4 H5 H6 + } else if ( match( block, /^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*(\n|$)/ ) ) { + len = RLENGTH; text = substr(block, 1, len); + match(block, /^#{1,6}/); n = RLENGTH; + sub(/^(#{1,6})[ \t]*/, "", text); sub(/[ \t]*#*(\n.*)?$/, "", text); + + return headline( n, text, 0 ) _block( substr( block, len + 1) ); + + # block images (wrapped in
) + } else if ( match(block, "^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n|$)") ) { + len = RLENGTH; text = href = title = attrib = substr( block, 1, len); + + sub("^!\\[", "", text); + sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", text); + + sub("^!" lix "\\([\n\t ]*", "", href); + sub("([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", href); + + sub("^!" lix "\\([\n\t ]*" lid, "", title); + sub("[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", title); + sub("^[\n\t ]+", "", title); + + sub("^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)", "", attrib); + sub("(\n.*)?$", "", attrib); + sub(/^\{[ \t]*/, "", attrib); sub(/[ \t]*\}$/, "", attrib); gsub(/[ \t]+/, " ", attrib); + + if ( match(href, /^<.*>$/) ) { sub(/^$/, "", href); } + if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); } + else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); } + else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); } + + gsub(/^[\t ]+$/, "", text); gsub(/\\/, "", href); + + return "
" \ + "\""" \ + (title?"
" inline(title) "
":"") \ + "
\n\n" \ + _block( substr( block, len + 1) ); + + # reference style images (block) + } else if ( match(line, /^!\[([^]]*)\] ?\[([^]]*)\](\n|$)/ ) ) { + len = RLENGTH; + text = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\](\n.*)?$/, "\\1", 1, block); + id = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\](\n.*)?$/, "\\2", 1, block); + if ( ! id ) id = text; + if ( rl_href[id] && rl_title[id] ) { + return "
" \ + "\""" \ + "
" inline(rl_title[id]) "
" \ + "
\n\n" \ + _block( substr( block, len + 1) ); + } else if ( rl_href[id] ) { + return "
" \ + "\""" \ + "
\n\n" \ + _block( substr( block, len + 1) ); + } else { + return "

" HTML(substr(block, 1, len)) "

\n" _block( substr(block, len + 1) ); + } + + # Macros (standalone <> calls handled as block, so they are not wrapped in paragraph) + } else if ( match( block, /^<<(([^>]|>[^>])+)>>(\n|$)/ ) ) { + len = RLENGTH; + text = gensub(/^<<(([^>]|>[^>])+)>>(\n.*)?$/, "\\1", 1, block); + return "" HTML(text) "" _block(substr(block, len + 1) ); + + # Definition list + } else if (match( block, "^(([ \t]*\n)*[^:\n \t][^\n]+\n" \ + "([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \ + "(([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \ + "|[^:\n \t][^\n]+(\n|$)" \ + "|( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|([ \t]*\n)+( ? ? ?\t| +)[^\n]+(\n|$))*)+" \ + )) { + list = substr( block, 1, RLENGTH); block = substr( block, RLENGTH + 1); + return "\n
\n" _dlist( list ) "
\n" _block( block ); + + # Unordered list types + } else if ( text = _startlist( block, "ul", "-", "([+*•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ul", "\\+", "([-*•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ul", "\\*", "([-+•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ul", "•", "([-+*]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + + # Ordered list types + } else if ( text = _startlist( block, "ol", "[0-9]+\\.", "([-+*•]|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ol", "[0-9]+\\)", "([-+*•]|[0-9]+\\.|#\\.|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ol", "#\\.", "([-+*•]|[0-9]+\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ol", "#\\)", "([-+*•]|[0-9]+\\.|#\\.|[0-9]+\\))") ) { + return text; + + # 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; + return _block(substr(block, 1, st - 1)) "
\n" _block(substr(block, st + len)); + + # Plain paragraph + } else { + return "

" inline(block) "

\n"; + } +} + +function _startlist(block, type, mark, exclude, LOCAL, st, len, list, indent, text) { + if (match( block, "(^|\n) ? ? ?" mark "[ \t][^\n]+(\n|$)" \ + "(([ \t]*\n)* ? ? ?" mark "[ \t][^\n]+(\n|$)" \ + "|([ \t]*\n)*( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|[^\n \t][^\n]+(\n|$))*" ) ) { + st = RSTART; len = RLENGTH; list = substr( block, st, len); + + sub("^\n", "", list); match(list, "^ ? ? ?"); indent = RLENGTH; + gsub( "(^|\n) {0," indent "}", "\n", list); sub("^\n", "", list); + + text = substr(block, 1, st - 1); block = substr(block, st + len); + if (match(text, /\n[[:space:]]*\n/)) return 0; + if (match(text, "(^|\n) ? ? ?" exclude "[ \t][^\n]+")) return 0; + if (match( list, "\n" exclude "[ \t]" )) { + block = substr(list, RSTART + 1) block; + list = substr(list, 1, RSTART); + } + + return _block( text ) "<" type ">\n" _list( list, mark ) "\n" _block( block ); + } else return 0; +} + +function _list (block, mark, p, LOCAL, len, st, text, indent, task) { + if ( match(block, "^([ \t]*\n)*$")) return; + + match(block, "^" mark "[ \t]"); indent = RLENGTH; + sub("^" mark "[ \t]", "", block); + + if (match(block, /\n[ \t]*\n/)) p = 1; + + match( block, "\n" mark "[ \t][^\n]+(\n|$)" ); + st = (RLENGTH == -1) ? length(block) + 1 : RSTART; + text = substr(block, 1, st); block = substr(block, st + 1); + + gsub("\n {0," indent "}", "\n", text); + + task = match( text, /^\[ \]/ ) ? "
  • " : \ + match( text, /^\[-\]/ ) ? "
  • " : \ + match( text, /^\[\/\]/ ) ? "
  • " : \ + match( text, /^\[\?\]/ ) ? "
  • " : \ + match( text, /^\[[xX]\]/) ? "
  • " : "
  • "; + sub(/^\[[-? \/xX]\]/, "", text); + + text = _nblock( text ); + if ( ! p && match( text, "^

    (]|\n$" )) + gsub( "(^

    |

    \n$)", "", text); + + return task text "
  • \n" _list(block, mark, p); +} + +function _dlist (block, LOCAL, len, st, text, indent, p) { + if (match( block, "^([ \t]*\n)*[^:\n \t][^\n]+\n" )) { + len = RLENGTH; text = substr(block, 1, len); + gsub( "(^\n*|\n*$)", "", text ); + return "
    " inline( text ) "
    \n" _dlist( substr(block, len + 1) ); + } else if (match( block, "^([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \ + "([^:\n \t][^\n]+(\n|$)" \ + "|( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|([ \t]*\n)+( ? ? ?\t| +)[^\n]+(\n|$))*" \ + )) { + len = RLENGTH; text = substr(block, 1, len); + sub( "^([ \t]*\n)*", "", text); + match(text, "^ ? ? ?:(\t| +)"); indent = RLENGTH; + sub( "^ ? ? ?:(\t| +)", "", text); + gsub( "(^|\n) {0," indent "}", "\n", text ); + + text = _nblock(text); + if (match( text, "^

    (]|\n$" )) + gsub( "(^

    |

    \n$)", "", text); + + return "
    " text "
    \n" _dlist( substr(block, len + 1) ); + } +} + +BEGIN { + # Global Vars + file = ""; rl_href[""] = ""; rl_title[""] = ""; + if (ENVIRON["MD_HTML"] == "true") { AllowHTML = "true"; } + HL[1] = 0; HL[2] = 0; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0; + # hls = "0 0 0 0 0 0"; + + # Universal Patterns + nu = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\_]|_[[:alnum:]])*" # not underline (except when escaped) + na = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\\\*])*" # not asterisk (except when escaped) + ieu = "_([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])_" # inner (underline) + isu = "__([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])__" # inner (underline) + iea = "\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*" # inner (asterisk) + isa = "\\*\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*\\*" # inner (asterisk) + + lix="\\[(\\\\[^\n]|[^]\n\\\\[])*\\]" # link text + lid="(<(\\\\[^\n]|[^\n<>\\\\])*>|(\\\\.|[^()\"'\\\\])+|([^<\n\t ()\\\\]|\\\\[^\n])(\\\\[\n]|[^\n\t \\(\\)\\\\])*)" # link dest + lit="(\"(\\\\.|[^\"\\\\])*\"|'(\\\\.|[^'\\\\])*'|\\((\\\\.|[^\\(\\)\\\\])*\\))" # link text + # link text with image def + lii="\\[(\\\\[^\n]|[^]\n\\\\[])*(!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\))?(\\\\[^\n]|[^]\n\\\\[])*\\]" + + # Buffering of full file ist necessary, e.g. to find reference links + while (getline) { file = file $0 "\n"; } + # Clean up MS-DOS line breaks + gsub(/\r\n/, "\n", file); + + # Fill array of reference links + f = file; rl_id; + re_reflink = "(^|\n) ? ? ?\\[([^]\n]+)\\]: ([^ \t\n]+)(\n?[ \t]+(\"([^\"]+)\"|'([^']+)'|\\(([^)]+)\\)))?(\n|$)"; + # /(^|\n) ? ? ?\[([^]\n]+)\]: ([^ \t\n]+)(\n?[ \t]+("([^"]+)"|'([^']+)'|\(([^)]+)\)))?(\n|$)/ + while ( match(f, re_reflink ) ) { + rl_id = gensub( re_reflink, "\\2", 1, substr(f, RSTART, RLENGTH) ); + rl_href[rl_id] = gensub( re_reflink, "\\3", 1, substr(f, RSTART, RLENGTH) ); + rl_title[rl_id] = gensub( re_reflink, "\\5", 1, substr(f, RSTART, RLENGTH) ); + f = substr(f, RSTART + RLENGTH); + rl_title[rl_id] = substr( rl_title[rl_id], 2, length(rl_title[rl_id]) - 2 ); + if ( rl_href[rl_id] ~ /<.*>/ ) rl_href[rl_id] = substr( rl_href[rl_id], 2, length(rl_href[rl_id]) - 2 ); + } + # Clear reflinks from File + while( gsub(re_reflink, "\n", file ) ); + # for (n in rl_href) { debug(n " | " rl_href[n] " | " rl_title[n] ); } + + # Run Block Processing -> The Actual Markdown! + printf "%s", _nblock( file ); +} diff --git a/session.sh b/session.sh index ee5c499..c3a44e8 100755 --- a/session.sh +++ b/session.sh @@ -1,11 +1,47 @@ #!/bin/sh +# Copyright 2018 - 2022 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + [ -n "$include_session" ] && return 0 include_session="$0" -_DATE="$(date +%s)" +export _DATE="$(date +%s)" SESSION_TIMEOUT="${SESSION_TIMEOUT:-7200}" +if ! which uuencode >/dev/null; then + uuencode() { busybox uuencode "$@"; } +fi +if ! which sha256sum >/dev/null; then + sha256sum() { busybox sha256sum "$@"; } +fi + +if which openssl >/dev/null; then + session_mac(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | openssl dgst -sha1 -hmac "$(server_key)" -binary |slopecode; } +else + # Gonzo MAC if openssl is unavailable + session_mac(){ + { server_key | dd status=none bs=256 count=1 skip=1 + { server_key | dd status=none bs=256 count=1 + [ $# -gt 0 ] && printf %s "$*" || cat + } \ + | sha256sum -; + } \ + | sha256sum | cut -d\ -f1 + } +fi + server_key(){ IDFILE="${IDFILE:-${_DATA:-.}/serverkey}" if [ "$(stat -c %s "$IDFILE")" -ne 512 ] || ! cat "$IDFILE"; then @@ -18,7 +54,8 @@ slopecode(){ # 6-Bit Code that retains sort order of input data, while beeing safe to use # in ascii transmissions, unix file names, HTTP URLs, and HTML attributes - uuencode -m - | sed ' + { [ $# -gt 0 ] && printf %s "$*" || cat; } \ + | uuencode -m - | sed ' 1d;$d; y;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/;0123456789:=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; ' @@ -42,42 +79,74 @@ timeid(){ } | slopecode } -checkid(){ grep -m 1 -xE '[0-9a-zA-Z:=]{16}'; } - transid(){ # transaction ID to modify a given file local file="$1" - { stat -c %F%i%n%N%s%Y "$file" 2>&- - printf %s "$SESSION_ID" - server_key - } | sha256sum | cut -d\ -f1 + session_mac "$(stat -c %F%i%n%N%s%Y "$file" 2>&-)" "$SESSION_ID" } +checkid(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | grep -m 1 -xE '[0-9a-zA-Z:=]{16}'; } + update_session(){ - local session sid time sig serverkey checksig + local session sid time sig checksig + unset SESSION_KEY SESSION_ID - IFS=- read -r sid time sig <<-END - $(COOKIE session) + read -r sid time sig <<-END + $(POST session_key || COOKIE session) END - serverkey="$(server_key)" - checksig="$(printf %s "$sid" "$time" "$serverkey" | sha256sum)" - checksig="${checksig%% *}" + 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="$(printf %s "$sid" "$time" "$serverkey" |sha256sum)" - sig="${sig%% *}" - printf %s\\n "${sid}-${time}-${sig}" + sig="$(session_mac "$sid" "$time")" + + 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")" Path="/${_BASE#/}" SameSite=Strict HttpOnly +} + +SESSION_VAR() { + # read authenticated cookie + # fail if value has been tampered with + local key="$1" value sig + value="$(COOKIE "$key")" + sig="${value##* }" value="${value% *}" + if [ "$sig" = "$(session_mac "$value" "$SESSION_ID")" ]; then + printf %s\\n "$value" + else + return 1 + fi +} + +SESSION_COOKIE() { + [ "$1" = new ] && new_session + SET_COOKIE 0 session="$SESSION_KEY" Path="/${_BASE#/}" SameSite=Strict HttpOnly } -SESSION_ID="$(update_session)" -SET_COOKIE 0 session="$SESSION_ID" Path=/ SameSite=Strict HttpOnly -SESSION_ID="${SESSION_ID%%-*}" +update_session || new_session diff --git a/storage.sh b/storage.sh index 7f70e64..17ea0d0 100755 --- a/storage.sh +++ b/storage.sh @@ -1,21 +1,18 @@ #!/bin/sh -# Copyright 2018, 2019 Paul Hänsch -# -# This is a file format helper, part of CGIlite. +# Copyright 2018 - 2021 Paul Hänsch # -# CGIlite is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. # -# CGIlite is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with CGIlite. If not, see . +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. [ -n "$include_storage" ] && return 0 include_storage="$0" @@ -25,62 +22,47 @@ BR=' ' LOCK(){ - local lock timeout block - lock="${1}.lock" - timeout="${2-20}" - if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -d "$lock" ]; then - printf 'Impossible to get lock: %s\n' "$lock" >&2 + local lock="${1}.lock" timeout="${2-20}" block + + if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -f "$lock" ]; then + debug "Impossible to get lock: $lock" return 1 fi - while ! mkdir "$lock" 2>&-; do - block="$(cat "$lock/pid" || printf 1)" - if ! { ps -eo pid |grep -qwF "$block"; }; then - printf 'Overriding stale lock: %s\n' "$lock" >&2 - break + while [ $timeout -gt 0 ]; do + printf '%i\n' $$ >>"${lock}" + read block <"$lock" + if [ "$block" = $$ ]; then + return 0 + elif ! { ps -eo pid |grep -qwF "$block"; }; then + debug "Trying to override stale lock: $lock" + if LOCK "$lock" 1; then + rm -- "$lock" + RELEASE "$lock" + fi + else + timeout=$((timeout - 1)) + [ $timeout -gt 0 ] && sleep 1 fi - if [ $timeout -le 0 ]; then - printf 'Timeout while trying to get lock: %s\n' "$lock" >&2 - return 1 - fi - timeout=$((timeout - 1)) - sleep 1 done - printf '%i\n' $$ >"${lock}/pid" - return 0 + + debug "Timeout while trying to get lock: $lock" + return 1 } RELEASE(){ - local lock - lock="${1}.lock" - if [ "$(cat "$lock/pid")" = "$$" ]; then - rm "$lock/pid" - if ! rmdir "$lock"; then - printf 'Cannot remove tainted lock: %s\n' "$lock" >&2 - printf '%i\n' $$ >"${lock}/pid" - return 1 - fi + local lock="${1}.lock" block + + read block <"$lock" + if [ "$block" = $$ ]; then + rm -- "$lock" return 0 else - printf 'Refusing to release foreign lock: %s\n' "$lock" >&2 + debug "Refusing to release foreign lock: $lock" return 1 fi } -STRING=' - s;\\;\\\\;g; - s;\n;\\n;g; - s;\t;\\t;g; - s;\r;\\r;g; - s;\+;\\+;g; - s; ;+;g; -' - -STRING_OLD(){ - { [ $# -eq 0 ] && cat || printf %s "$*"; } \ - | sed -E ':X; $!{N;bX;}'"$STRING" -} - STRING(){ local in out='' [ $# -gt 0 ] && in="$*" || in="$(cat)" @@ -93,24 +75,9 @@ STRING(){ " "*) out="${out}+"; in="${in# }" ;; *) out="${out}${in%%[\\${CR}${BR} + ]*}"; in="${in#"${in%%[\\${BR}${CR} + ]*}"}" ;; esac; done - printf '%s' "$out" + printf '%s' "${out:-\\}" } - -UNSTRING=' - :UNSTRING_X - s;((^|[^\\])(\\\\)*)\\n;\1\n;g; - s;((^|[^\\])(\\\\)*)\\t;\1\t;g; - s;((^|[^\\])(\\\\)*)\\r;\1\r;g; - s;((^|[^\\])(\\\\)*)\+;\1 ;g; - tUNSTRING_X; - s;((^|[^\\])(\\\\)*)\\\+;\1+;g; - s;\\\\;\\;g; -' -UNSTRING_OLD(){ - { [ $# -eq 0 ] && cat || printf %s "$*"; } \ - | sed -E "$UNSTRING" -} UNSTRING(){ local in out='' [ $# -gt 0 ] && in="$*" || in="$(cat)" @@ -119,11 +86,103 @@ UNSTRING(){ \\n*) out="${out}${BR}"; in="${in#\\n}" ;; \\r*) out="${out}${CR}"; in="${in#\\r}" ;; \\t*) out="${out} "; in="${in#\\t}" ;; - \\+) out="${out}+"; in="${in#\\+}" ;; + \\+*) out="${out}+"; in="${in#\\+}" ;; +*) out="${out} "; in="${in#+}" ;; \\*) in="${in#\\}" ;; *) out="${out}${in%%[\\+]*}"; in="${in#"${in%%[\\+]*}"}" ;; esac; done - printf '%s' "$out" + printf '%s\n' "$out" } +DBM() { + local file="$1" cmd="$2" + local k v key value + shift 2; + + case "$cmd" in + check|contains) + key="$(STRING "$1")" + while read -r k v; do if [ "$k" = "$key" ]; then + return 0 + fi; done <"$file" 2>&- + return 1 + ;; + get) + key="$(STRING "$1")" + while read -r k v; do if [ "$k" = "$key" ]; then + UNSTRING "$v" + return 0 + fi; done <"$file" 2>&- + return 1 + ;; + set|store) + key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + { while read -r k v; do + [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + printf '%s\t%s\n' "$key" "$value" + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + add|insert) + k="$1" key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + if DBM "$file" check "$k"; then + RELEASE "$file" + return 1 + else + printf '%s\t%s\n' "$key" "$value" >>"${file}" + RELEASE "$file" + return 0 + fi + ;; + update|replace) + k="$1" key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + if ! DBM "$file" check "$k"; then + RELEASE "$file" + return 1 + fi + { while read -r k v; do + [ "$k" = "$key" ] \ + && printf '%s\t%s\n' "$key" "$value" \ + || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + append) + key="$(STRING "$1")" value="$(STRING "$2")" + LOCK "$file" || return 1 + if ! DBM "$file" check "$1"; then + RELEASE "$file" + return 1 + fi + { while read -r k v; do + [ "$k" = "$key" ] \ + && printf '%s\t%s\n' "$key" "$v$value" \ + || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + delete|remove) + key="$(STRING "$1")" + LOCK "$file" || return 1 + { while read -r k v; do + [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v" + done <"$file" 2>&- + } >"${file}.$$.tmp" + mv "${file}.$$.tmp" "${file}" + RELEASE "$file" + return 0 + ;; + esac +} diff --git a/users.sh b/users.sh new file mode 100755 index 0000000..32299ff --- /dev/null +++ b/users.sh @@ -0,0 +1,661 @@ +#!/bin/sh + +# Copyright 2021 - 2024 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[ -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}" + +HTTP_HOST="$(HEADER Host)" +MAILFROM="noreply@${HTTP_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 +' + +# == TRANSLATIONS == +# override all functions marked with "TRANSLATION" +# sed -n '/TRANSLATION$/,/^}/p;' "${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_email() { # TRANSLATION + "$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 +} + +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")" + user_register_email + 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_email(){ # TRANSLATION + "$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 +} + +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")" + user_invite_email + 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 -m1 -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_PW_MISMATCH" + 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_disabled(){ # TRANSLATION + cat <<-EOF + [div #user_register .disabled + User Registration is disabled. + ] + EOF +} +w_user_register_sendmail(){ # TRANSLATION + 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 +} +w_user_register_direct(){ # TRANSLATION + 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 +} + +w_user_register(){ + if [ "$(GET user_confirm)" ]; then + w_user_confirm + elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then + w_user_register_disabled + elif [ "$USER_REQUIREEMAIL" = true ]; then + w_user_register_sendmail + elif [ "$USER_REQUIREEMAIL" != true ]; then + w_user_register_direct + fi +} + +w_user_confirm_proceed(){ # TRANSLATION + 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 +} +w_user_confirm_expired(){ # TRANSLATION + cat <<-EOF + [div #user_confirm .expired + [p This activation link is not valid anymore.] + ] + EOF +} +w_user_confirm_invalid(){ # TRANSLATION + 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 +} + +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 + w_user_confirm_proceed + else + w_user_confirm_expired + fi + else + w_user_confirm_invalid + fi +} + +w_user_invite_email(){ # TRANSLATION + 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 +} +w_user_invite_link(){ # TRANSLATION + 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 +} +w_user_invite_deny(){ # TRANSLATION + cat <<-EOF + [div #user_invite .notallowed + Only registered users may send an invitation to another user. + ] + EOF +} + +w_user_invite(){ + local uid invlink + + if [ "$(GET user_confirm)" ]; then + w_user_confirm + elif [ "$USER_ID" -a "$USER_REQUIREEMAIL" = true ]; then + w_user_invite_email + 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" + w_user_invite_link + else + w_user_invite_deny + fi +} + +w_user_login_logon(){ # TRANSLATION + cat <<-EOF + [form #user_login .login method=POST + [input name=uname placeholder="Username or Email"] + [input type=password name=pw placeholder="Passphrase"] + [submit "action" "user_login" Login] + ] + EOF +} +w_user_login_logoff(){ # TRANSLATION + cat <<-EOF + [form #user_login .logout method=POST + [p Logged in as [span . $(HTML ${USER_NAME})]] + [submit "action" "user_logout" Logout] + ] + EOF +} + +w_user_login(){ + if [ ! "$USER_ID" ]; then + w_user_login_logon + elif [ "$USER_ID" ]; then + w_user_login_logoff + fi +}