From: Paul Hänsch Date: Wed, 18 May 2022 00:37:42 +0000 (+0200) Subject: Merge commit '5fa200f3143db4360818d82e9ed68e3bc297872a' X-Git-Url: https://git.plutz.net/?a=commitdiff_plain;h=e80c289e4e82f70c9e7426c6c1d3c71c8a42046e;hp=5fa200f3143db4360818d82e9ed68e3bc297872a;p=shellwiki Merge commit '5fa200f3143db4360818d82e9ed68e3bc297872a' --- diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24781a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: _subtrees + +_subtrees: _cgilite + +cgilite: + git subtree add --squash -P $@ https://git.plutz.net/git/$@ master + +_cgilite: cgilite + git subtree pull --squash -P $< https://git.plutz.net/git/$< master diff --git a/acl.sh b/acl.sh new file mode 100755 index 0000000..d232ea0 --- /dev/null +++ b/acl.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +# ACL_OVERRIDE="${ACL_OVERRIDE:-Admin:read,write}" +ACL_DEFAULT="${ACL_DEFAULT:-Known:read,write${BR}All:read}" + +acl_cachepath='' +acl_collection='' + +acl_collect(){ + local path="${1:-${PATH_INFO}}" + # Get directory part of PATH_INFO + local path="${path%/*}/./" + local pagefile head acl + + if [ "$acl_cachepath" = "$path" ]; then + printf '%s\n' "$ACL_OVERRIDE" "$acl_collection" "$ACL_DEFAULT" + return 0 + else + acl_cachepath="$path" + acl_collection='' + fi + + printf '%s\n' "$ACL_OVERRIDE" + + while :; do + [ "$path" = / ] && break + path="${path%/*/}/" + + if [ -f "$_DATA/pages/$path/#page.md" ]; then + pagefile="$_DATA/pages/$path/#page.md" + elif [ -f "$_EXEC/pages/$path/#page.md" ]; then + pagefile="$_EXEC/pages/$path/#page.md" + else + continue + fi + + acl="$(sed -En ' + s;\r$;;; + /^%acl([\t ]+.*)?$/bACL; + 20q; + b; + + :ACL + s;(%(acl)?)?[\t ]*;; + p; n; s;\r$;;; + /^(%[ \t]+|%acl[ \t]+|[ \t]+)[^ \t\r]+$/bACL; + /^(%[ \t]*|%acl[ \t]*)$/bACL; + ' <"$pagefile")" + + printf %s\\n "${acl}" + acl_collection="${acl_collection}${acl}${BR}" + done + + printf '%s\n' "$ACL_DEFAULT" +} + +acl_read(){ + local page="${1:-${PATH_INFO}}" + local acl + + while read -r acl; do + case ${acl##*:} in + read|*,read,*|read,*|*,read) + acl="${acl%%:*}:read";; + *) acl="${acl%%:*}:";; + esac + [ "$USER_NAME" ] && case $acl in + "Known:read") return 0;; + "Known:") return 1;; + "+Known:read") return 0;; + "-Known:read") return 1;; + "@${USER_NAME}:read") return 0;; + "@${USER_NAME}:") return 1;; + "+@{$USER_NAME}:read") return 0;; + "-@{$USER_NAME}:read") return 1;; + esac + case $acl in + "All:read") return 0;; + "All:") return 1;; + "+All:read") return 0;; + "-All:read") return 1;; + esac + done <<-EOF + $(acl_collect "$page") + EOF + return 1 +} + +acl_write(){ + local page="${1:-${PATH_INFO}}" + local acl + + while read -r acl; do + case ${acl##*:} in + write|*,write,*|write,*|*,write) + acl="${acl%%:*}:write";; + *) acl="${acl%%:*}:";; + esac + [ "$USER_NAME" ] && case ${acl} in + "Known:write") return 0;; + "Known:") return 1;; + "+Known:write") return 0;; + "-Known:write") return 1;; + "@${USER_NAME}:write") return 0;; + "@${USER_NAME}:") return 1;; + "+@{$USER_NAME}:write") return 0;; + "-@{$USER_NAME}:write") return 1;; + esac + case $acl in + "All:write") return 0;; + "All:") return 1;; + "+All:write") return 0;; + "-All:write") return 1;; + esac + done <<-EOF + $(acl_collect "$page") + EOF + return 1 +} diff --git a/.gitignore b/cgilite/.gitignore similarity index 100% rename from .gitignore rename to cgilite/.gitignore diff --git a/cgilite.sh b/cgilite/cgilite.sh similarity index 100% rename from cgilite.sh rename to cgilite/cgilite.sh diff --git a/common.css b/cgilite/common.css similarity index 100% rename from common.css rename to cgilite/common.css diff --git a/file.sh b/cgilite/file.sh similarity index 100% rename from file.sh rename to cgilite/file.sh diff --git a/html-sh.sed b/cgilite/html-sh.sed similarity index 100% rename from html-sh.sed rename to cgilite/html-sh.sed diff --git a/logging.sh b/cgilite/logging.sh similarity index 100% rename from logging.sh rename to cgilite/logging.sh diff --git a/markdown.awk b/cgilite/markdown.awk similarity index 100% rename from markdown.awk rename to cgilite/markdown.awk diff --git a/session.sh b/cgilite/session.sh similarity index 100% rename from session.sh rename to cgilite/session.sh diff --git a/storage.sh b/cgilite/storage.sh similarity index 100% rename from storage.sh rename to cgilite/storage.sh diff --git a/users.sh b/cgilite/users.sh similarity index 100% rename from users.sh rename to cgilite/users.sh diff --git a/handlers/10_page.sh b/handlers/10_page.sh new file mode 100755 index 0000000..004dd03 --- /dev/null +++ b/handlers/10_page.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +CACHE_AGE=${CACHE_AGE:-1800} +export MD_MACROS="$_EXEC/macros" +export MD_HTML="${MD_HTML:-false}" +export WIKI_THEME=${WIKI_THEME:-default} + +. "$_EXEC/themes/${WIKI_THEME}.sh" + +wiki() { + # Print content of a wiki page + # Get page from data or underlay dir, handle caching + local page="$(PATH "$1")" mdfile cache cachetime + + cache="$_DATA/pages/$page/#page.${USER_ID}.cache" + + mdfile="$(mdfile "$page")" || return 4 + acl_read "$page" || return 3 + + cachetime="$(stat -c %Y -- "$mdfile" "$cache" 2>/dev/null)" + + if [ "${cachetime#*${BR}}" -gt "${cachetime%${BR}*}" \ + -a "${cachetime#*${BR}}" -gt "$((_DATE - CACHE_AGE))" ]; then + cat "${cache}" + else + mkdir -p -- "$_DATA/pages/$page/" + # Macros expect to find page directory as working dir + ( cd -- "$_DATA/pages/$page/"; + md <"$mdfile" \ + | tee -- "${cache}.$$" + ) + grep -q '^%nocache' "$mdfile" \ + && rm -- "${cache}.$$" \ + || mv -- "${cache}.$$" "${cache}" + fi +} + +case "${PATH_INFO}" in + /"[.]"/*) + FILE "${_EXEC}/${PATH_INFO#/\[.\]}" + return 0 + ;; + *${BR}*) + export ERROR_MSG='Page names containing newline character are not allowed' + theme_error 400 + return 0 + ;; + */\#*) + export ERROR_MSG='Page names starting with "#" are not allowed' + theme_error 400 + return 0 + ;; + */\[*\]/*) + return 1 + ;; + */) + theme_page "${PATH_INFO}" + return 0 + ;; +esac + +return 1 diff --git a/handlers/20_account.sh b/handlers/20_account.sh new file mode 100755 index 0000000..73edb0f --- /dev/null +++ b/handlers/20_account.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +case "${PATH_INFO}" in + */"[login]") + theme_page "/[wiki]/login/" + return 0 + ;; + */"[register]") + theme_page "/[wiki]/register/" + return 0 + ;; + */"[invite]") + theme_page "/[wiki]/invite/" + return 0 + ;; + */"[settings]") + theme_page "/[wiki]/settings/" + return 0 + ;; +esac + +return 1 diff --git a/handlers/20_attachment.sh b/handlers/20_attachment.sh new file mode 100755 index 0000000..1cf4c53 --- /dev/null +++ b/handlers/20_attachment.sh @@ -0,0 +1,155 @@ +#!/bin/sh + +attachment_convert(){ + local attpath="$1" + local cachepath="${attpath%/#attachments/*}/#cache/${attpath#*/#attachments/}" + local res junk + + case $attpath in + *.webm|*.mp4|*.mkv|*.avi) + cachepath="${cachepath}.webm" + ;; + esac + + if [ -s "$cachepath" ]; then + printf %s "$cachepath" + return 0 + elif [ -f "$cachepath" ]; then + printf %s "$attpath" + return 0 + elif ! mkdir -p -- "${cachepath%/*}" && touch "$cachepath"; then + printf %s "$attpath" + return 0 + fi + + case $attpath in + *.jpg|*.jpeg|*.png) + read junk junk res junk <<-EOF + $(identify "$attpath") + EOF + if [ "${res%x*}" -gt 2048 ]; then + convert "$attpath" -resize 1920x-2 -quality 85 "$cachepath" + else + convert "$attpath" -quality 85 "$cachepath" + fi + printf %s "$cachepath" + return 0 + ;; + *.webm|*.mp4|*.mkv|*.avi) + res=$(ffprobe -show_entries stream=width "$attpath" 2>&-) + res="${res#*width=}" res="${res%%${BR}*}" + if [ "$res" -gt 1280 ]; then + ( exec >&- 2>&1; + ffmpeg -y -nostdin -i "$attpath" \ + -c:v libvpx -vf scale=1280:-2 -crf 28 -b:v 0 \ + -c:a libvorbis -q:a 6 \ + "${cachepath%.*}.tmp.webm" \ + && mv -- "${cachepatch%.*}.tmp.webm" "${cachepath}" \ + & ) & + + else + ( exec >&- 2>&1; + ffmpeg -y -nostdin -i "$attpath" \ + -c:v libvpx -crf 28 -b:v 0 \ + -c:a libvorbis -q:a 6 \ + "${cachepath%.*}.tmp.webm" \ + && mv -- "${cachepatch%.*}.tmp.webm" "${cachepath}" \ + & ) & + fi + printf %s "$attpath" + return 0 + ;; + esac +} + +case ${PATH_INFO} in + */\[attachment\]/) + tsid="$(POST session_key)"; tsid="${tsid%% *}" + attachment_delete="$(POST delete)" + + if [ "${CONTENT_TYPE%%;*}" = "multipart/form-data" ] && acl_write "${PATH_INFO%\[attachment\]/}"; then + . "$_EXEC/multipart.sh" + multipart_cache + + # Validate session id from form to prevent CSRF + # Only validate if username is present, because no username means + # anonymous uploads are allowed via acl and cgilite/session.sh does not + # validate anonymous sessions from a multipart/formdata + if [ "$USER_NAME" -a "$(multipart session_id)" != "$SESSION_ID" ]; then + rm -- "$multipart_cachefile" + printf 'Refresh: %i\r\n' 4 + theme_error 403 + return 0 + fi + + mkdir -p "$_DATA/pages/${PATH_INFO%/\[attachment\]/}/#attachments/" + n=1; while filename=$(multipart_filename "file" "$n"); do + filename="$(printf %s "$filename" |tr /\\0 __)" + multipart "file" "$n" >"$_DATA/pages/${PATH_INFO%/\[attachment\]/}/#attachments/$filename" + n=$((n + 1)) + done + rm -- "$multipart_cachefile" + REDIRECT "${_BASE}${PATH_INFO}" + elif [ "${CONTENT_TYPE%%;*}" = "multipart/form-data" ]; then + printf 'Refresh: %i\r\n' 4 + theme_error 403 + head -c $((CONTENT_LENGTH)) >/dev/null + return 0 + elif [ "$attachment_delete" -a "$SESSION_ID" = "$tsid" ]; then + rm -- "$_DATA/pages/${PATH_INFO%/\[attachment\]/}/#attachments/$attachment_delete" + REDIRECT "${_BASE}${PATH_INFO}" + elif [ "$attachment_delete" ]; then + printf 'Refresh: %i\r\n' 4 + theme_error 403 + return 0 + elif acl_read "${PATH_INFO%\[attachment\]/}"; then + theme_attachments "${PATH_INFO%\[attachment\]/}" + return 0 + else + theme_error 404 + return 0 + fi + ;; + + */\[attachment\]/*) + attpath="${PATH_INFO%/\[attachment\]/*}/#attachments/${PATH_INFO##*/}" + + if ! acl_read "${PATH_INFO%/\[attachment\]/*}"; then + theme_error 403 + return 0 + elif [ -f "$_DATA/pages/$attpath" ]; then + FILE "$_DATA/pages/$attpath" + return 0 + elif [ -f "$_EXEC/pages/$attpath" ]; then + FILE "$_EXEC/pages/$attpath" + return 0 + else + theme_error 404 + return 0 + fi + ;; + */*/) + return 1 + ;; + */*) + attpath="${PATH_INFO%/*}/#attachments/${PATH_INFO##*/}" + + if ! acl_read "${PATH_INFO%/*}/"; then + theme_error 403 + return 0 + elif [ -f "$_DATA/pages/$attpath" ]; then + FILE "$(attachment_convert "$_DATA/pages/$attpath")" + return 0 + elif [ -f "$_EXEC/pages/$attpath" ]; then + FILE "$(attachment_convert "$_EXEC/pages/$attpath")" + return 0 + elif [ -d "$_DATA/pages/${PATH_INFO}" -o -d "$_EXEC/pages/${PATH_INFO}" ]; then + REDIRECT "${_BASE}${PATH_INFO}/" + elif [ "${PATH_INFO%\[*\]}" = "${PATH_INFO}" ]; then + theme_error 404 + return 0 + fi + ;; +esac + +return 1 diff --git a/handlers/30_edit.sh b/handlers/30_edit.sh new file mode 100755 index 0000000..60b1b25 --- /dev/null +++ b/handlers/30_edit.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +. "${_EXEC}/themes/default.sh" +. "${_EXEC}/session_lock.sh" + +wiki_text() { + # Print source text of a wiki page + # Get page from data or underlay dir + local page="$(PATH "$1")" + + if [ -f "$_DATA/pages/$page/#page.md" ]; then + cat -- "$_DATA/pages/$page/#page.md" + elif [ -f "$_EXEC/pages/$page/#page.md" ]; then + cat -- "$_EXEC/pages/$page/#page.md" + else + return 1 + fi +} + +edit_page="${PATH_INFO%\[edit\]}" +edit_file="$_DATA/pages/$edit_page/#page.md" +[ "$REQUEST_METHOD" = POST ] && edit_action="$(POST action)" + +if [ "$edit_page" = "$PATH_INFO" ]; then + unset edit_page edit_action edit_file + # END EDIT SCRIPT, continue in index.cgi + +elif [ "$edit_action" = update ]; then + if mkdir -p -- "${edit_file%/#page.md}" \ + && S_LOCK "$edit_file"; then + POST pagetext >"$edit_file" + S_RELEASE "$edit_file" + REDIRECT "${_BASE}${PATH_INFO%\[edit\]}" + else + export ERRMSG="ERR_NOLOCK" + REDIRECT "${_BASE}${PATH_INFO%\[edit\]}/[edit]" + fi + +elif [ "$edit_action" = cancel ]; then + S_RELEASE "$edit_file" + REDIRECT "${_BASE}${PATH_INFO%\[edit\]}" + +elif ! acl_write "$edit_page"; then + theme_error 403 + return 0 + +elif mkdir -p -- "${edit_file%/#page.md}" \ + && S_LOCK "$edit_file"; then + theme_editor "$edit_page" + return 0 + +else + printf 'Refresh: %i; url=%s\r\n' 4 ../ + export ERROR_MSG="Unable to lock page for editing" + theme_error 409 + return 0 + +fi + +return 1 diff --git a/handlers/90_brackets.sh b/handlers/90_brackets.sh new file mode 100755 index 0000000..b6b2538 --- /dev/null +++ b/handlers/90_brackets.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# spacial case for bracket pages that are not handled otherwise +# attachment and edit (and really all) handlers should take precedence + +case "${PATH_INFO}" in + */\[*\]/*) + theme_page "${PATH_INFO}" + return 0 + ;; +esac + +return 1 diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..8975cca --- /dev/null +++ b/index.cgi @@ -0,0 +1,27 @@ +#!/bin/sh + +. "${_EXEC:-${0%/*}}/cgilite/cgilite.sh" +. "${_EXEC}/cgilite/session.sh" +. "${_EXEC}/cgilite/file.sh" +. "${_EXEC}/cgilite/users.sh" +. "${_EXEC}/tools.sh" +. "${_EXEC}/acl.sh" + +wiki_text() { + # Print source text of a wiki page + # Get page from data or underlay dir + local page="$(PATH "$1")" mdfile + + mdfile="$(mdfile "$page")" || return 4 + acl_read "$page" || return 3 + cat -- "$mdfile" +} + +for handler in "$_EXEC"/handlers/*; do + . "$handler" && break +done + +if [ $? != 0 ]; then + export ERROR_MSG="The presented URL schema cannot be handled" + theme_error 400 +fi diff --git a/macros/attachments b/macros/attachments new file mode 100755 index 0000000..34a7beb --- /dev/null +++ b/macros/attachments @@ -0,0 +1,29 @@ +#!/bin/sh + +. "$_EXEC/cgilite/cgilite.sh" +. "$_EXEC/cgilite/users.sh" +. "$_EXEC/acl.sh" +. "$_EXEC/tools.sh" + +page="$1" + +if [ "${page#/}" = "$page" ]; then + page="$(PATH "${PATH_INFO}/$page")" +fi + +acl_read "$page" || exit 0 + +printf %s\\n '' diff --git a/macros/errormessage b/macros/errormessage new file mode 100755 index 0000000..c95688a --- /dev/null +++ b/macros/errormessage @@ -0,0 +1,7 @@ +#!/bin/sh + +. "$_EXEC/cgilite/cgilite.sh" + +if [ "$1" -o "$ERROR_MSG" ]; then + printf '

%s

' "$(HTML "${1:-${ERROR_MSG}}")" +fi diff --git a/macros/include b/macros/include new file mode 100755 index 0000000..b1d1e77 --- /dev/null +++ b/macros/include @@ -0,0 +1,63 @@ +#!/bin/sh + +. "$_EXEC/cgilite/cgilite.sh" +. "$_EXEC/cgilite/users.sh" +. "$_EXEC/acl.sh" +. "$_EXEC/tools.sh" + +from=1 +to=\$ +rev='' +items=\$ +path_info="$PATH_INFO" + +while [ $# -gt 0 ]; do case $1 in + --from) from="$2"; shift 2;; + from=*) from="${1#*=}"; shift 1;; + --to) to="$2"; shift 2;; + to=*) to="${1#*=}"; shift 1;; + --items) items="$2"; shift 2;; + items=*) items="${1#*=}"; shift 1;; + --rev|--reverse) rev="-r"; shift 1;; + *) page="$1"; shift 1;; +esac; done + +if ! printf %s\\n "$from" |grep -qEx '[0-9]+|/([^/\\]|\\/|\\.)*/'; then + debug 'Include macro invalid argument: "from"' + exit 1 +fi +if ! printf %s\\n "$to" |grep -qEx '[0-9]+|/([^/\\]|\\/|\\.)*/'; then + debug 'Include macro Invalid argument: "to"' + exit 1 +fi +if ! printf %s\\n "$items" |grep -qEx '\$|[0-9]+'; then + debug 'Include macro Invalid argument: "items"' + exit 1 +fi + +page_glob "$page" \ +| sort $rev \ +| sed "${items}q" \ +| while read glob; do + case $glob in + /*) export PATH_INFO="$(PATH "$glob")";; + *) export PATH_INFO="$(PATH "$path_info/$glob")";; + esac + acl_read || continue + if [ -f "$_DATA/pages/$PATH_INFO/#page.md" ]; then + mdfile="$_DATA/pages/$PATH_INFO/#page.md" + elif [ -f "$_EXEC/pages/$PATH_INFO/#page.md" ]; then + mdfile="$_EXEC/pages/$PATH_INFO/#page.md" + else + continue + fi + printf '
' "$(HTML "$glob")" + ( cd -- "${mdfile%/*}" + sed -n "${from},${to}p" <"$mdfile" \ + | md \ + | grep -vx '' + ) | sed -E ' + s;(<[^>]+ )(href|src)="([^#/"][^"]*)"([^>]*>);\1\2="'"$(HTML "$glob")"'\3"\4;g + ' + printf '
' +done diff --git a/macros/pagelist b/macros/pagelist new file mode 100755 index 0000000..7c80041 --- /dev/null +++ b/macros/pagelist @@ -0,0 +1,27 @@ +#!/bin/sh + +. "$_EXEC/cgilite/cgilite.sh" +. "$_EXEC/cgilite/users.sh" +. "$_EXEC/acl.sh" +. "$_EXEC/tools.sh" + +dir="${1:-./}" +depth="${2:-1}" + +printf '\n' diff --git a/macros/reflink b/macros/reflink new file mode 100755 index 0000000..1b38c15 --- /dev/null +++ b/macros/reflink @@ -0,0 +1,8 @@ +#!/bin/sh + +. "$_EXEC/cgilite/cgilite.sh" + +title="$(HTML "$1")" +ref="$(HEADER Referer)" + +printf '%s' "${ref:-./}" "${title:-Return}" diff --git a/macros/toc b/macros/toc new file mode 100755 index 0000000..308fced --- /dev/null +++ b/macros/toc @@ -0,0 +1,19 @@ +#!/bin/sh + +. "$_EXEC/cgilite/cgilite.sh" + +unset MD_MACROS + +if [ "$(which awk)" ]; then + md() { awk -f "$_EXEC/cgilite/markdown.awk"; } +elif [ "$(which busybox)" ]; then + md() { busybox awk -f "$_EXEC/cgilite/markdown.awk"; } +else + md() { cat; } +fi + +md |sed -nE ' + 1i +' diff --git a/macros/wikiform b/macros/wikiform new file mode 100755 index 0000000..ecb003b --- /dev/null +++ b/macros/wikiform @@ -0,0 +1,21 @@ +#!/bin/sh + +action="$1" + +. "$_EXEC/cgilite/cgilite.sh" +. "$_EXEC/cgilite/users.sh" + +case $action in + login) + w_user_login |"$_EXEC/cgilite/html-sh.sed" + ;; + register) + w_user_register |"$_EXEC/cgilite/html-sh.sed" + ;; + invite) + w_user_invite |"$_EXEC/cgilite/html-sh.sed" + ;; + settings) + w_user_update |"$_EXEC/cgilite/html-sh.sed" + ;; +esac diff --git a/md_macros.awk b/md_macros.awk new file mode 100755 index 0000000..4b85a0e --- /dev/null +++ b/md_macros.awk @@ -0,0 +1,67 @@ +#!/bin/awk -f +#!/opt/busybox/awk -f + +function sh_escape(arg){ + return "'" gensub(/'/, "'\"'\"'", "g", arg) "'"; +} + +function argsplit(line, args, LOCAL, c, n, ctx) { + ctx="space"; n=0; + + while ( length(line) > 0 ) { + c = substr(line, 1, 1); + line = substr(line, 2); + if (ctx == "space" ) + if (c ~ /[ \t]/) ctx = "space"; + else if (c ~ /\\/) { n++; ctx = "escbare"; } + else if (c ~ /"/) { n++; ctx = "dquot"; } + else if (c ~ /'/) { n++; ctx = "squot"; } + else { n++; args[n] = c; ctx = "bare"; } + else if (ctx == "bare") + if (c ~ /[ \t]/) ctx = "space"; + else if (c ~ /\\/) ctx = "escbare"; + else if (c ~ /"/) ctx = "dquot"; + else if (c ~ /'/) ctx = "squot"; + else args[n] = args[n] c; + else if (ctx == "dquot") + if (c ~ /"/) ctx = "bare"; + else if (c ~ /\\/) ctx = "escdquot"; + else args[n] = args[n] c; + else if (ctx == "squot") + if (c ~ /'/) ctx = "bare"; + else args[n] = args[n] c; + else if (ctx == "escbare") { + args[n] = args[n] c; + ctx = "bare"; + } + else if (ctx == "escdquot") { + args[n] = args[n] c; + ctx = "dquot"; + } + } +} + +function macro(call, LOCAL, line, args) { + argsplit(call, args); + call=""; + + for (n = 1; n in args; n++) call = call sh_escape(args[n]) " "; + + if (args[1] in MACROS) { + RS=""; ORS=""; line=""; + "printf '%s' " sh_escape(file) " | " sh_escape(ENVIRON["MD_MACROS"]) "/" call | getline line; + return line; + } else { + return HTML("<<" call ">>"); + } +} + +BEGIN { + if (ENVIRON["MD_MACROS"]) { + AllowMacros = "true"; + "cd " sh_escape(ENVIRON["MD_MACROS"]) "; printf '%s/' *" |getline macro_list; + split(macro_list, MACROS, "/"); + for (n in MACROS) { MACROS[MACROS[n]] = ""; delete MACROS[n]; } + delete MACROS[""]; + } +} diff --git a/multipart.sh b/multipart.sh new file mode 100644 index 0000000..09e3ca8 --- /dev/null +++ b/multipart.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +[ "$include_multipart" ] && return 0 +inlude_multipart="$0" + +if [ "${CONTENT_TYPE}" -a ! "${CONTENT_TYPE##multipart/form-data;*}" ]; then + multipart_boundary="${CONTENT_TYPE#*; boundary=}" + multipart_boundary="${multipart_boundary%%;*}" + multipart_boundary="${multipart_boundary%${CR}}" +fi +multipart_cachefile="/tmp/multipart.$$" + +readbytes(){ + # read n bytes, like `head -c` but do not consume input + local size="$1" block + + for block in 65536 32768 16384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1; do + if [ $size -ge $block ]; then + dd status=none bs="$block" count="$((size / block))" + size="$((size % block))" + fi + done +} + +multipart_cache() { + multipart_cachefile="${1:-${multipart_cachefile}}" # global + + if [ "${multipart_boundary}" ]; then + # readbytes "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}" + head -c "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}" + else + return 1 + fi +} + +multipart(){ + local name="$1" count="${2:-1}" + local formdata state=begin + + while IFS='' read -r formdata; do case "$formdata" in + "--${multipart_boundary}--${CR}") + [ $state = data ] && return 0 \ + || return 1 + ;; + "--${multipart_boundary}${CR}") + [ $state = data ] && return 0 \ + || state=header + ;; + "Content-Disposition: form-data; name=\"${name}\""*"${CR}") + [ $state = header -a $count -eq 1 ] && state=dheader + [ $state = header -a $count -gt 1 ] && count=$((count - 1)) + [ $state = data ] && printf "%s\n" "$formdata" + ;; + "${CR}") + if [ $state = dheader ]; then + sed -n "/--${multipart_boundary}\(--\)\?${CR}/q; p;" \ + | head -c-2 + return 0; + fi + [ $state = header ] && state=junk + ;; + esac; done <"${multipart_cachefile}" +} + +multipart_filename(){ + local name="$1" count="${2:-1}" + local formdata state=begin + + while read -r formdata; do case "$formdata" in + "--${multipart_boundary}--${CR}") + return 1 + ;; + "--${multipart_boundary}${CR}") + state=header + ;; + "Content-Disposition: form-data; name=\"${name}\"; filename=\""*"\""*"${CR}") + [ $state = header -a $count -eq 1 ] && break + [ $state = header -a $count -gt 1 ] && count=$((count - 1)) + ;; + "${CR}") + [ $state = header ] && state=junk + ;; + esac; done <"${multipart_cachefile}" + + filename="${formdata#*; filename=\"}" + filename="${filename%%\"${CR}}" + filename="${filename%%\";*}" + + HEX_DECODE % "$filename" +} diff --git a/pages/#attachments/favicon.ico b/pages/#attachments/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/pages/#page.md b/pages/#page.md new file mode 100644 index 0000000..4dd6aeb --- /dev/null +++ b/pages/#page.md @@ -0,0 +1,2 @@ +It Works! +========= diff --git a/pages/[wiki]/400/#page.md b/pages/[wiki]/400/#page.md new file mode 100644 index 0000000..94ba677 --- /dev/null +++ b/pages/[wiki]/400/#page.md @@ -0,0 +1,9 @@ +%nocache + +400 +=== + +**Bad Request** +<> + +<> diff --git a/pages/[wiki]/403/#page.md b/pages/[wiki]/403/#page.md new file mode 100644 index 0000000..d15309b --- /dev/null +++ b/pages/[wiki]/403/#page.md @@ -0,0 +1,8 @@ +%nocache + +403 +=== + +**Forbidden** + +<> diff --git a/pages/[wiki]/404/#page.md b/pages/[wiki]/404/#page.md new file mode 100644 index 0000000..f54bad5 --- /dev/null +++ b/pages/[wiki]/404/#page.md @@ -0,0 +1,8 @@ +%nocache + +404 +=== + +**page not found** + +<> diff --git a/pages/[wiki]/409/#page.md b/pages/[wiki]/409/#page.md new file mode 100644 index 0000000..0727e7c --- /dev/null +++ b/pages/[wiki]/409/#page.md @@ -0,0 +1,9 @@ +%nocache + +409 +=== + +**Conflict** +<> + +<> diff --git a/pages/[wiki]/500/#page.md b/pages/[wiki]/500/#page.md new file mode 100644 index 0000000..0d56e0a --- /dev/null +++ b/pages/[wiki]/500/#page.md @@ -0,0 +1,9 @@ +%nocache + +500 +=== + +**Internal Server Error** +<> + +<> diff --git a/pages/[wiki]/footer/#page.md b/pages/[wiki]/footer/#page.md new file mode 100644 index 0000000..da2b20f --- /dev/null +++ b/pages/[wiki]/footer/#page.md @@ -0,0 +1,3 @@ +---- +Shellwiki +Edit the Footer [here](/[wiki]/footer/[edit]) diff --git a/pages/[wiki]/header/#page.md b/pages/[wiki]/header/#page.md new file mode 100644 index 0000000..7d50e32 --- /dev/null +++ b/pages/[wiki]/header/#page.md @@ -0,0 +1,10 @@ +# Shellwiki + +::: { .menu } + * [Login]([login]) + * [Register]([register]) +::: + +Edit the Header [here](/[wiki]/header/[edit]) + +---- diff --git a/pages/[wiki]/invite/#page.md b/pages/[wiki]/invite/#page.md new file mode 100644 index 0000000..114b208 --- /dev/null +++ b/pages/[wiki]/invite/#page.md @@ -0,0 +1,7 @@ +%nocache + +Set up an account +----------------- +<> + +[Return](./) diff --git a/pages/[wiki]/login/#page.md b/pages/[wiki]/login/#page.md new file mode 100644 index 0000000..ac715ae --- /dev/null +++ b/pages/[wiki]/login/#page.md @@ -0,0 +1,8 @@ +%nocache + +Login +----- +<> +[Account registration]([register] "Sign up for a new user account") + +[Return](./) diff --git a/pages/[wiki]/register/#page.md b/pages/[wiki]/register/#page.md new file mode 100644 index 0000000..b88149c --- /dev/null +++ b/pages/[wiki]/register/#page.md @@ -0,0 +1,7 @@ +%nocache + +Set up an account +----------------- +<> + +[Return](./) diff --git a/pages/[wiki]/settings/#page.md b/pages/[wiki]/settings/#page.md new file mode 100644 index 0000000..9dd280f --- /dev/null +++ b/pages/[wiki]/settings/#page.md @@ -0,0 +1,7 @@ +%nocache + +Change Your Password +----------------- +<> + +[Return](./) diff --git a/session_lock.sh b/session_lock.sh new file mode 100755 index 0000000..f55c9cd --- /dev/null +++ b/session_lock.sh @@ -0,0 +1,68 @@ +#!/bin/sh + +. "$_EXEC/cgilite/storage.sh" +. "$_EXEC/cgilite/session.sh" + +LOCK_TIMEOUT="${LOCK_TIMEOUT:-1200}" + +S_LOCK(){ + local file="$1" timeout="${2:-$LOCK_TIMEOUT}" + local date sid + + printf "%i %s\n" "$_DATE" "$SESSION_ID" >>"${file}.lock" + + if ! read date sid <"${file}.lock"; then + debug "Unable to access lock: ${file}.lock" + + elif [ $((date + timeout)) -lt $_DATE ]; then + # Override stale lock + if LOCK "${file}.lock" 1; then + debug "Overriding stale lock: ${file}.lock" + printf "%i %s\n" "$_DATE" "$SESSION_ID" >"${file}.lock" + RELEASE "${file}.lock" + return 0 + else + return 1 + fi + + elif [ "$sid" = "$SESSION_ID" -a "$date" -ne "$_DATE" ]; then + # Refresh aged lock + printf "%i %s\n" "$_DATE" "$SESSION_ID" >"${file}.lock" + return 0 + + elif [ "$sid" = "$SESSION_ID" ]; then + # Simple success + return 0 + + else + return 1 + fi +} + +S_RELEASE(){ + local file="$1" timeout="${2:-$LOCK_TIMEOUT}" + local date sid + + if ! read date sid <"${file}.lock"; then + # File was not locked + return 0 + + elif [ "$sid" = "$SESSION_ID" -a $((date + timeout)) -lt $_DATE ]; then + # if lock is stale, protect against stale override before release + if LOCK "${file}.lock" 1; then + rm -- "${file}.lock" + RELEASE "${file}.lock" + return 0 + else + return 1 + fi + + elif [ "$sid" = "$SESSION_ID" ]; then + # Simple success + rm -- "${file}.lock" + return 0 + + else + return 1 + fi +} diff --git a/themes/default.css b/themes/default.css new file mode 100644 index 0000000..90819a7 --- /dev/null +++ b/themes/default.css @@ -0,0 +1,120 @@ +html { min-height: 100%; } + +body { + position: absolute; + width: 100%; + min-height: 100%; + padding-bottom: 6em; + background-color: #EEE; +} + +header, footer { + background-color: #FFF; + box-shadow: 0 0 .75em; + width: 100%; + z-index: 1; +} + +footer { + position: absolute; + bottom: 0; +} + +header > :last-child, +main > :last-child { + margin-bottom: 0; +} + +header h2, +header .menu { + display: inline-block; +} + +header .menu { list-style: none; } + +header .menu li { + display: inline-block; + margin-right: .5em; +} + +main .pagemenu { + list-style: none; + background-color: #666; + margin: 0; + box-shadow: 0 0 .5em; + padding: .25em 2em; +} +main .pagemenu li { + display: inline-block; + margin-right: 1em; +} +main .pagemenu li a { color: #FFF; } + +main article, +[id$="/[attachment]/"] main form.upload { + margin: 1em; + padding: .125em 1em 1em 1em; + box-shadow: .25em .25em .75em; + background-color: #FFF; +} + +[id$="/[attachment]/"] main .attachment.list { + margin: 1em; + padding: 1em 2em; +} +[id$="/[attachment]/"] main .attachment.list:before { + content: ''; + position: absolute; + top: 0; bottom: 0; left: 0; right: 0; + background-color: #FFF; + box-shadow: .25em .25em .75em; +} + + +/* === Editor === */ + +body.editor textarea { + width: 100%; + min-height: 20em; +} + +/* === Attachments === */ + +.attachment.list button[name=delete] { + font-size: .75em; + line-height: 1.25em; + margin-right: 1.25em; +} +.attachment.list .size, +.attachment.list .date { + font-size: .875em; + top: -.25em; +} + +.attachment.list .name:after { + white-space: pre-line; + content: "\0a"; +} +.attachment.list .size { + margin-right: 1em; +} + + +/* === Macros === */ + +.macro.toc { + display: inline-block; + list-style-position: inside; + margin-left: 0; + background-color: #DDD; + background-color: rgba(0, 0, 0, .125); + padding: .75em 1em; + border: 1pt solid; + border-radius: 2pt; +} +.macro.toc li.h2 { margin-left: 1.25em; } +.macro.toc li.h3 { margin-left: 2.5em; } +.macro.toc li.h4 { margin-left: 3.75em; } +.macro.toc li.h5 { margin-left: 5em; } +.macro.toc li.h6 { margin-left: 6.25em; } + diff --git a/themes/default.sh b/themes/default.sh new file mode 100755 index 0000000..0d60fb3 --- /dev/null +++ b/themes/default.sh @@ -0,0 +1,181 @@ +#!/bin/sh + +. "$_EXEC/tools.sh" + +theme_head(){ + printf ' + + + + ' "$_BASE" +} + +theme_header(){ + printf '
%s
' "$(wiki '[wiki]/header/')" +} + +theme_footer(){ + printf '
%s
' "$(wiki '[wiki]/footer/' ||echo No footer)" +} + +theme_page(){ + local page="$1" title + title="${page%/}"; title="${title##*/}" + + if [ ! "$(mdfile "$page")" ]; then + theme_error 404 + return 0 + elif ! acl_read "$page"; then + theme_error 403 + return 0 + fi + + # Important! Web Server response including newline + printf "%s\r\n" "Content-Type: text/html; charset=utf-8" "" + + cat <<-EOF + + + $(theme_head) + $(HTML "${title}") + + $(theme_header) +
+ $(acl_write "$page" && printf %s \ + '' + ) +
+ $(wiki "$page" || printf 'Error while loading page
function "wiki" of index.sh returned with an error.') +
+
+ $(theme_footer) + + EOF +} + +theme_editor(){ + local page="$1" title + title="${page%/}"; title="${title##*/}" + + if ! acl_write "$page"; then + theme_error 403 + return 0 + fi + + # Important! Web Server response including newline + printf "%s\r\n" "Content-Type: text/html; charset=utf-8" "" + + cat <<-EOF + + + $(theme_head) + $(HTML "${title}") + + $(theme_header) +
+ + + + +
+ $(theme_footer) + + EOF +} + +theme_attachments(){ + local page="$1" title + title="${page%/}"; title="${title##*/}" + + if [ ! "$(mdfile "$page")" ]; then + theme_error 404 + return 0 + elif ! acl_read "$page"; then + theme_error 403 + return 0 + fi + + # Important! Web Server response including newline + printf "%s\r\n" "Content-Type: text/html; charset=utf-8" "" + + if acl_write "$page"; then + cat <<-EOF + + + $(theme_head) + Attachments $(HTML "${title}") + + $(theme_header) +
+
+ + + +
+ +
    + + $(for file in "$_EXEC/pages/$page/#attachments"/* "$_DATA/pages/$page/#attachments"/*; do + [ "$file" = "$_EXEC/pages/$page/#attachments/${file##*/}" \ + -a -f "$_DATA/pages/$page/#attachments/${file##*/}" ] && continue + stat="$(stat -c '%s %Y' -- "$file" 2>&-)" || continue + size="${stat% *}" date="${stat#* }" + + printf '
  • %s + %s%s
  • ' \ + "$(HTML "${file##*/}")" "$(HTML "${file##*/}")" "$(HTML "${file##*/}")" \ + "$(size_human "$size")" "$(date -d @"$date" +"%F %T")" + done) +
+
+ $(theme_footer) + + EOF + else + cat <<-EOF + + + $(theme_head) + Attachments $(HTML "${title}") + + $(theme_header) +
+
    + $(for file in "$_EXEC/pages/$page/#attachments"/* "$_DATA/pages/$page/#attachments"/*; do + [ "$file" = "$_EXEC/pages/$page/#attachments/${file##*/}" \ + -a -f "$_DATA/pages/$page/#attachments/${file##*/}" ] && continue + stat="$(stat -c '%s %Y' -- "$file" 2>&-)" || continue + size="${stat% *}" date="${stat#* }" + + printf '
  • %s + %s%s
  • ' \ + "$(HTML "${file##*/}")" "$(HTML "${file##*/}")" "$(size_human "$size")" "$(date -d @"$date" +"%F %T")" + done) +
+
+ $(theme_footer) + + EOF + fi +} + +theme_error(){ + local errno="$1" + + case $errno in + 400) printf "%s\r\n" "Status: 400 Bad Request";; + 403) printf "%s\r\n" "Status: 403 Forbidden";; + 404) printf "%s\r\n" "Status: 404 Not Found";; + 409) printf "%s\r\n" "Status: 409 Conflict";; + 500) printf "%s\r\n" "Status: 500 Internal Server Error";; + esac + + if [ "$(mdfile "/[wiki]/$errno/")" ]; then + theme_page "/[wiki]/$errno/" + else + printf "Content-Length: 0\r\n\r\n" + fi +} diff --git a/tools.sh b/tools.sh new file mode 100644 index 0000000..c88cc55 --- /dev/null +++ b/tools.sh @@ -0,0 +1,98 @@ +#!/bin/sh + +if [ "$(which awk)" ]; then + md() { awk -f "$_EXEC/md_macros.awk" -f "$_EXEC/cgilite/markdown.awk"; } +elif [ "$(which busybox)" ]; then + md() { busybox awk -f "$_EXEC/md_macros.awk" -f "$_EXEC/cgilite/markdown.awk"; } +else + md() { cat; } +fi + +mdfile(){ + local page="$(PATH "$1")" + + if [ -f "$_DATA/pages/$page/#page.md" ]; then + printf %s\\n "$_DATA/pages/$page/#page.md" + elif [ -f "$_EXEC/pages/$page/#page.md" ]; then + printf %s\\n "$_EXEC/pages/$page/#page.md" + else + return 1 + fi +} + +size_human(){ + local size="$1" + + if [ $size -gt $((1024 * 1024 * 1024)) ]; then + size=$((size / 1024 / 1024 / 1024 * 10 + size / 1024 / 1024 % 1024 / 100)) + printf "%i.%i GB" "$((size / 10))" "$((size % 10))" + + elif [ $size -gt $((1024 * 1024)) ]; then + size=$((size / 1024 / 1024 * 10 + size / 1024 % 1024 / 100)) + printf "%i.%i MB" "$((size / 10))" "$((size % 10))" + + elif [ $size -gt $((1024)) ]; then + size=$((size / 1024 * 10 + size % 1024 / 100)) + printf "%i.%i KB" "$((size / 10))" "$((size % 10))" + + else + printf "%i B" "$size" + fi +} + +page_glob(){ + local pattern="${1%/}/" IFS='' + local glob page pagedir + + case $pattern in + /*) + for glob in "$_DATA/pages"$pattern; do printf '%s\n' "${glob#"$_DATA/pages"}"; done + for glob in "$_EXEC/pages"$pattern; do printf '%s\n' "${glob#"$_EXEC/pages"}"; done + ;; + *) + for glob in "$_DATA/pages/$PATH_INFO"/$pattern; do printf '%s\n' "${glob#"$_DATA/pages/$PATH_INFO/"}"; done + for glob in "$_EXEC/pages/$PATH_INFO"/$pattern; do printf '%s\n' "${glob#"$_EXEC/pages/$PATH_INFO/"}"; done + ;; + esac \ + | sort -u \ + | while read -r page; do + case $page in + \#*|*/\#*) continue;; + /*) pagedir="$(PATH "$page")";; + *) pagedir="$(PATH "$PATH_INFO/$page")";; + esac + [ -d "$_DATA/pages/$pagedir" -o -d "$_EXEC/pages/$pagedir" ] \ + && printf '%s\n' "$page" + done +} + +page_children(){ + local page="${1:-${PATH_INFO}}" depth="${2:-1}" + local glob pagedir IFS='' + page="${page%/}/" + + [ "$depth" -eq 0 ] && return 0 + + case $page in + /*) + for glob in "$_DATA/pages/${page}"*/; do printf '%s\n' "${glob#"$_DATA/pages"}"; done + for glob in "$_EXEC/pages/${page}"*/; do printf '%s\n' "${glob#"$_EXEC/pages"}"; done + ;; + *) + for glob in "$_DATA/pages/$PATH_INFO/${page}"*/; do printf '%s\n' "${glob#"$_DATA/pages/$PATH_INFO/"}"; done + for glob in "$_EXEC/pages/$PATH_INFO/${page}"*/; do printf '%s\n' "${glob#"$_EXEC/pages/$PATH_INFO/"}"; done + ;; + esac \ + | sort -u \ + | while read -r page; do + case $page in + \#*|*/\#*) continue;; + /*) pagedir="$(PATH "$page")";; + *) pagedir="$(PATH "$PATH_INFO/$page")";; + esac + if [ -d "$_DATA/pages/$pagedir" -o -d "$_EXEC/pages/$pagedir" ]; then + printf %s\\n "$page" + page_children "$page" "$((depth - 1))" + fi + done +}